Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 126 additions & 126 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@next2d/player",
"version": "3.12.1",
"version": "3.13.0",
"description": "Experience the fast and beautiful anti-aliased rendering of WebGL/WebGPU. You can create rich, interactive graphics, cross-platform applications and games without worrying about browser or device compatibility.",
"author": "Toshiyuki Ienaga<[email protected]> (https://github.com/ienaga/)",
"license": "MIT",
Expand Down Expand Up @@ -61,11 +61,11 @@
"@typescript-eslint/eslint-plugin": "^8.71.0",
"@typescript-eslint/parser": "^8.71.0",
"@webgpu/types": "^0.1.74",
"eslint": "^10.11.0",
"eslint": "^10.12.0",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.13.0",
"jsdom": "^30.1.1",
"rollup": "^4.63.6",
"rollup": "^4.64.0",
"tslib": "^2.8.1",
"typescript": "^6.0.3",
"vite": "^8.3.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ describe("Shape bounds precision before culling", () =>
const expected = calcBounds(...rect as [number, number, number, number], matrix, new Float32Array(4));
const width = Math.ceil(Math.abs(expected[2] - expected[0]));
const height = Math.ceil(Math.abs(expected[3] - expected[1]));
// 片軸のスケールが0またはNaNの場合も面積0としてスキップされる
const xScale = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]);
const yScale = Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3]);
const culled = width === 0 || height === 0 || width === Infinity || height === Infinity
|| expected[0] + width < 0 || expected[1] + height < 0
|| expected[0] > viewport || expected[1] > viewport;
|| expected[0] > viewport || expected[1] > viewport
|| !xScale || !yScale;
renderQueue.offset = 0;
execute(shape, matrix, color, viewport, viewport);
expect(renderQueue.buffer[0]).toBe(culled ? 0 : 1);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { execute } from "./ShapeGenerateRenderQueueUseCase";
import { Shape } from "../../Shape";
import { renderQueue } from "@next2d/render-queue";
import { describe, expect, it } from "vitest";

describe("ShapeGenerateRenderQueueUseCase.js test", () =>
Expand Down Expand Up @@ -143,4 +144,24 @@ describe("ShapeGenerateRenderQueueUseCase.js test", () =>
expect(typeof 800).toBe("number");
expect(typeof 600).toBe("number");
});
it("execute test case - skips rotated shape whose one axis scale is zero", () =>
{
const enqueue = (matrix: Float32Array): number =>
{
const shape = new Shape();
shape.graphics.beginFill(0xff0000).drawRect(0, 0, 100, 100).endFill();

const offset = renderQueue.offset;
execute(shape, matrix, new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]), 800, 600);
return renderQueue.offset - offset;
};

// 45度回転 + scaleY=0: スクリーンboundsは0にならないが面積は0
const r = Math.SQRT1_2;
expect(enqueue(new Float32Array([r, r, 0, 0, 100, 100]))).toBe(1);
expect(renderQueue.buffer[renderQueue.offset - 1]).toBe(0);

// スケールが0でなければ描画キューに積まれる
expect(enqueue(new Float32Array([r, r, -r, r, 100, 100]))).toBeGreaterThan(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,19 @@ export const execute = (
);
}

// 回転・スキュー時はスクリーンboundsが0にならなくても、片軸のスケールが0なら
// 面積0で描画結果がなく、Worker側のノードサイズも0になるためスキップする
if (!renderXScale || !renderYScale) {
if (tColorTransform !== color_transform) {
ColorTransform.release(tColorTransform);
}
if (tMatrix !== matrix) {
Matrix.release(tMatrix);
}
renderQueue.push1(0);
return;
}

const xScaleRounded = Math.round(renderXScale * 100) / 100;
const yScaleRounded = Math.round(renderYScale * 100) / 100;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { execute } from "./TextFieldGenerateRenderQueueUseCase";
import { TextField } from "@next2d/text";
import { renderQueue } from "@next2d/render-queue";
import { describe, expect, it } from "vitest";

describe("TextFieldGenerateRenderQueueUseCase.js test", () =>
Expand Down Expand Up @@ -214,4 +215,17 @@ describe("TextFieldGenerateRenderQueueUseCase.js test", () =>
execute(textField, combinedMatrix, colorTransform, 800, 600);
}).not.toThrow();
});
it("execute test case - skips rotated TextField whose one axis scale is zero", () =>
{
const textField = new TextField();
textField.text = "Hello World";

// 45度回転 + scaleX=0: スクリーンboundsは0にならないが面積は0
const r = Math.SQRT1_2;
const offset = renderQueue.offset;
execute(textField, new Float32Array([0, 0, -r, r, 100, 100]), new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]), 800, 600);

expect(renderQueue.offset - offset).toBe(1);
expect(renderQueue.buffer[renderQueue.offset - 1]).toBe(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ export const execute = (
);
}

// 回転・スキュー時はスクリーンboundsが0にならなくても、片軸のスケールが0なら
// 面積0で描画結果がなく、Worker側のノードサイズも0になるためスキップする
if (!renderXScale || !renderYScale) {
if (tColorTransform !== color_transform) {
ColorTransform.release(tColorTransform);
}
if (tMatrix !== matrix) {
Matrix.release(tMatrix);
}
renderQueue.push1(0);
return;
}

const xScaleRounded = Math.round(renderXScale * 100) / 100;
const yScaleRounded = Math.round(renderYScale * 100) / 100;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ export const execute = async (
if (!device) {
throw new Error("WebGPU device not available");
}
void device.lost.then((info) => {
if (info.reason !== "destroyed") {
globalThis.postMessage({ "message": "rendererFailure", "reason": "device-lost" });
}
});
device.addEventListener("uncapturederror", (event) => {
if (event.error instanceof GPUOutOfMemoryError) {
globalThis.postMessage({ "message": "rendererFailure", "reason": "gpu-memory" });
}
});

const context = canvas.getContext("webgpu");
if (!context) {
Expand All @@ -68,4 +78,4 @@ export const execute = async (
// Set CanvasToWebGLContext
$setContext(new WebGLContext(gl, $samples, device_pixel_ratio));
}
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ describe("TextFieldGenerateFontStyleService.js test", () =>
size: 12,
font: "Arial",
} as ITextFormat;
expect(execute(mockTextFormat)).toBe("italic bold 12px 'Arial','sans-serif'");
expect(execute(mockTextFormat)).toBe("italic bold 12px 'Arial',sans-serif");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export const execute = (text_format: ITextFormat): string =>
fontStyle += "bold ";
}

return `${fontStyle}${text_format.size}px '${text_format.font}','sans-serif'`;
};
return `${fontStyle}${text_format.size}px '${text_format.font}',sans-serif`;
};
2 changes: 2 additions & 0 deletions packages/webgpu/src/AtlasManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IAttachmentObject } from "./interface/IAttachmentObject";
import type { TexturePacker } from "@next2d/texture-packer";
import { resetAtlasScratch } from "./AtlasScratch";

/**
* @description テクスチャアトラス境界の初期最大値
Expand Down Expand Up @@ -212,6 +213,7 @@ export const $getCurrentAtlasIndex = (): number =>
*/
export const $resetAtlas = (): void =>
{
resetAtlasScratch();
$rootNodes.length = 0;

$setActiveAtlasIndex(0);
Expand Down
36 changes: 36 additions & 0 deletions packages/webgpu/src/AtlasScratch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { beforeEach, expect, it, vi } from "vitest";
import { atlasScratch, needsAtlasRestore, resetAtlasScratch, restoreAtlas } from "./AtlasScratch";
import type { IAttachmentObject } from "./interface/IAttachmentObject";

beforeEach(() => {
resetAtlasScratch();
vi.stubGlobal("GPUTextureUsage", { RENDER_ATTACHMENT: 16 });
});

it("shares multisample storage while restoring independent resolved pages", () => {
const createTexture = vi.fn(() => ({ createView: () => ({}), destroy: vi.fn() }));
const device = { createTexture, createShaderModule: vi.fn(), createBindGroup: vi.fn(),
createRenderPipeline: vi.fn(() => ({ getBindGroupLayout: vi.fn() })) } as unknown as GPUDevice;
const scratch = atlasScratch(device, 4096, 4096);
expect(atlasScratch(device, 4096, 4096)).toBe(scratch);
expect(createTexture).toHaveBeenCalledTimes(2);
const attachment = (): IAttachmentObject => ({ width: 4096, height: 4096,
texture: { resource: {}, view: {} }, msaaTexture: { resource: scratch.color, view: {} }
} as unknown as IAttachmentObject);
const first = attachment(), second = attachment();
const pass = { setPipeline: vi.fn(), setBindGroup: vi.fn(), draw: vi.fn(), end: vi.fn() };
const beginRenderPass = vi.fn(() => pass);
const encoder = { beginRenderPass } as unknown as GPUCommandEncoder;
expect(needsAtlasRestore(device, first)).toBe(true);
restoreAtlas(device, encoder, first);
expect(needsAtlasRestore(device, first)).toBe(false);
expect(needsAtlasRestore(device, second)).toBe(true);
restoreAtlas(device, encoder, second);
expect(needsAtlasRestore(device, first)).toBe(true);
expect(beginRenderPass).toHaveBeenCalledTimes(2);
expect(beginRenderPass.mock.calls[0][0].colorAttachments[0].resolveTarget).toBeUndefined();
expect(pass.draw).toHaveBeenCalledWith(3);
resetAtlasScratch();
expect(atlasScratch(device, 4096, 4096)).not.toBe(scratch);
expect(createTexture).toHaveBeenCalledTimes(4);
});
64 changes: 64 additions & 0 deletions packages/webgpu/src/AtlasScratch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { IAttachmentObject } from "./interface/IAttachmentObject";
import { $samples } from "./WebGPUUtil";

interface IAtlasScratch {
color: GPUTexture;
stencil: GPUTexture;
owner: GPUTexture | null;
pipeline: GPURenderPipeline;
}
let pools = new WeakMap<GPUDevice, Map<string, IAtlasScratch>>();

/** Resolved cache pages stay resident; multisample working storage is shared. */
export function atlasScratch(device: GPUDevice, width: number, height: number): IAtlasScratch {
let pool = pools.get(device);
if (!pool) { pool = new Map(); pools.set(device, pool) }
const key = `${width}:${height}`;
const cached = pool.get(key);
if (cached) { return cached }
const shader = device.createShaderModule({ "code": `
@group(0) @binding(0) var image: texture_2d<f32>;
@vertex fn vertex(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
let points = array<vec2f, 3>(vec2f(-1,-1),vec2f(3,-1),vec2f(-1,3));
return vec4f(points[i],0,1);
}
@fragment fn fragment(@builtin(position) p: vec4f) -> @location(0) vec4f {
return textureLoad(image,vec2i(p.xy),0);
}` });
const value: IAtlasScratch = {
"color": device.createTexture({ "size": { width, height }, "format": "rgba8unorm",
"sampleCount": $samples, "usage": GPUTextureUsage.RENDER_ATTACHMENT }),
"stencil": device.createTexture({ "size": { width, height }, "format": "stencil8",
"sampleCount": $samples, "usage": GPUTextureUsage.RENDER_ATTACHMENT }),
"owner": null,
"pipeline": device.createRenderPipeline({ "layout": "auto",
"vertex": { "module": shader, "entryPoint": "vertex" },
"fragment": { "module": shader, "entryPoint": "fragment", "targets": [{ "format": "rgba8unorm" }] },
"multisample": { "count": $samples } })
};
pool.set(key, value);
return value;
}

export function needsAtlasRestore(device: GPUDevice, attachment: IAttachmentObject): boolean {
const scratch = pools.get(device)?.get(`${attachment.width}:${attachment.height}`);
return !!scratch && attachment.msaaTexture?.resource === scratch.color
&& scratch.owner !== attachment.texture?.resource;
}

export function restoreAtlas(device: GPUDevice, encoder: GPUCommandEncoder, attachment: IAttachmentObject): void {
const scratch = atlasScratch(device, attachment.width, attachment.height);
if (!attachment.texture || !attachment.msaaTexture) { return }
const pass = encoder.beginRenderPass({ "colorAttachments": [{
"view": attachment.msaaTexture.view, "loadOp": "clear", "storeOp": "store",
"clearValue": { "r": 0, "g": 0, "b": 0, "a": 0 }
}] });
pass.setPipeline(scratch.pipeline);
pass.setBindGroup(0, device.createBindGroup({ "layout": scratch.pipeline.getBindGroupLayout(0),
"entries": [{ "binding": 0, "resource": attachment.texture.view }] }));
pass.draw(3); pass.end();
scratch.owner = attachment.texture.resource;
}

/** AtlasManager destroys the shared resources along with the page attachments. */
export function resetAtlasScratch(): void { pools = new WeakMap() }
2 changes: 1 addition & 1 deletion packages/webgpu/src/Context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const mockDevice = {
createTexture: vi.fn().mockReturnValue(mockTexture),
createBuffer: vi.fn().mockReturnValue(mockBuffer),
createCommandEncoder: vi.fn().mockReturnValue(mockCommandEncoder),
createRenderPipeline: vi.fn().mockReturnValue({}),
createRenderPipeline: vi.fn().mockReturnValue({ getBindGroupLayout: vi.fn().mockReturnValue({}) }),
createBindGroup: vi.fn().mockReturnValue({}),
createBindGroupLayout: vi.fn().mockReturnValue({}),
createPipelineLayout: vi.fn().mockReturnValue({}),
Expand Down
Loading
Loading