(
$poolBoundsArray(bounds);
}
+ if (scaledMatrix) {
+ $poolFloat32Array6(scaledMatrix);
+ }
+
return $getBoundsArray(xMin, yMin, xMax, yMax);
};
\ No newline at end of file
diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts
index cdaa8ed7..ff0808f2 100644
--- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts
+++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts
@@ -1,8 +1,12 @@
import { execute } from "./DisplayObjectContainerGenerateRenderQueueUseCase";
import { describe, expect, it } from "vitest";
import { renderQueue } from "@next2d/render-queue";
+import { $cacheStore } from "@next2d/cache";
import { MovieClip } from "../../MovieClip";
+import { Shape } from "../../Shape";
import { $RENDERER_CONTAINER_TYPE } from "../../DisplayObjectUtil";
+import { Matrix } from "@next2d/geom";
+import { stage } from "../../Stage";
describe("DisplayObjectContainerGenerateRenderQueueUseCase.js test", () =>
{
@@ -38,4 +42,307 @@ describe("DisplayObjectContainerGenerateRenderQueueUseCase.js test", () =>
renderQueue.buffer.fill(0);
renderQueue.offset = 0;
});
+
+ it("cacheAsBitmap null の場合は通常パスを使用", () =>
+ {
+ const movieClip = new MovieClip();
+ movieClip.addChild(new MovieClip());
+ movieClip.cacheAsBitmap = null;
+
+ const matrix = new Float32Array([1, 0, 0, 1, 0, 0]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ renderQueue.offset = 0;
+ renderQueue.buffer.fill(0);
+
+ execute(movieClip, [], matrix, colorTransform, 0, 0);
+
+ // 通常パス: visible=1, CONTAINER_TYPE, blendMode(11=normal), useLayer=0
+ expect(renderQueue.buffer[0]).toBe(1);
+ expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE);
+ expect(renderQueue.buffer[2]).toBe(11);
+ expect(renderQueue.buffer[3]).toBe(0); // useLayer=0(no filter, normal blend)
+
+ renderQueue.buffer.fill(0);
+ renderQueue.offset = 0;
+ });
+
+ it("cacheAsBitmap 設定時に cache miss で専用形式を生成", () =>
+ {
+ const movieClip = new MovieClip();
+ const childShape = new Shape();
+ movieClip.addChild(childShape);
+
+ movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0);
+ stage.rendererScale = 1;
+
+ const matrix = new Float32Array([1, 0, 0, 1, 0, 0]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ // キャッシュをクリア
+ $cacheStore.removeById(`${movieClip.instanceId}`);
+
+ renderQueue.offset = 0;
+ renderQueue.buffer.fill(0);
+
+ execute(movieClip, [], matrix, colorTransform, 800, 600);
+
+ // visible=1, CONTAINER_TYPE
+ expect(renderQueue.buffer[0]).toBe(1);
+ expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE);
+ // blendMode (normal=11)
+ expect(renderQueue.buffer[2]).toBe(11);
+ // useLayer=1
+ expect(renderQueue.buffer[3]).toBe(1);
+
+ // layerType=2 (cacheAsBitmap), cacheHit=0
+ expect(renderQueue.buffer[6]).toBe(2);
+ expect(renderQueue.buffer[7]).toBe(0);
+
+ // instanceId
+ expect(renderQueue.buffer[8]).toBe(movieClip.instanceId);
+
+ // filterBounds (フィルターなし: すべて0)
+ expect(renderQueue.buffer[10]).toBe(0);
+ expect(renderQueue.buffer[11]).toBe(0);
+ expect(renderQueue.buffer[12]).toBe(0);
+ expect(renderQueue.buffer[13]).toBe(0);
+
+ // renderScaleX, renderScaleY
+ expect(renderQueue.buffer[14]).toBe(1); // renderScaleX
+ expect(renderQueue.buffer[15]).toBe(1); // renderScaleY
+ // parent matrix a, b, c, d
+ expect(renderQueue.buffer[16]).toBe(1); // matrix[0]
+ expect(renderQueue.buffer[17]).toBe(0); // matrix[1]
+ expect(renderQueue.buffer[18]).toBe(0); // matrix[2]
+ expect(renderQueue.buffer[19]).toBe(1); // matrix[3]
+
+ // メインスレッドのキャッシュストアにキャッシュキーが設定される
+ const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]);
+ const cached = $cacheStore.get(
+ `${movieClip.instanceId}`,
+ "bitmapKey"
+ );
+ expect(cached).toBe(cacheKey);
+
+ // cleanup
+ $cacheStore.removeById(`${movieClip.instanceId}`);
+ movieClip.cacheAsBitmap = null;
+ renderQueue.buffer.fill(0);
+ renderQueue.offset = 0;
+ });
+
+ it("cacheAsBitmap cache hit で子要素をスキップ", () =>
+ {
+ const movieClip = new MovieClip();
+ const childShape = new Shape();
+ movieClip.addChild(childShape);
+
+ movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0);
+ stage.rendererScale = 1;
+
+ const matrix = new Float32Array([1, 0, 0, 1, 0, 0]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ // キャッシュストアにキャッシュキーとローカルバウンズを事前設定(cache hitを模擬)
+ const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]);
+ $cacheStore.set(`${movieClip.instanceId}`, "bitmapKey", cacheKey);
+ $cacheStore.set(`${movieClip.instanceId}`, "bLocalBounds", new Float32Array([0, 0, 100, 80]));
+
+ renderQueue.offset = 0;
+ renderQueue.buffer.fill(0);
+
+ const offsetBefore = renderQueue.offset;
+ execute(movieClip, [], matrix, colorTransform, 800, 600);
+ const offsetAfter = renderQueue.offset;
+
+ // visible=1, CONTAINER_TYPE
+ expect(renderQueue.buffer[0]).toBe(1);
+ expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE);
+ // blendMode (normal=11)
+ expect(renderQueue.buffer[2]).toBe(11);
+ // useLayer=1
+ expect(renderQueue.buffer[3]).toBe(1);
+
+ // layerType=2 (cacheAsBitmap), cacheHit=1
+ expect(renderQueue.buffer[6]).toBe(2);
+ expect(renderQueue.buffer[7]).toBe(1);
+
+ // instanceId
+ expect(renderQueue.buffer[8]).toBe(movieClip.instanceId);
+
+ // cache hit の場合、子要素のデータは含まれない(offsetが小さい)
+ expect(offsetAfter - offsetBefore).toBeLessThan(35);
+
+ // cleanup
+ $cacheStore.removeById(`${movieClip.instanceId}`);
+ movieClip.cacheAsBitmap = null;
+ renderQueue.buffer.fill(0);
+ renderQueue.offset = 0;
+ });
+
+ it("cacheAsBitmap: 子要素変更後もキャッシュが維持される", () =>
+ {
+ const movieClip = new MovieClip();
+ const childShape = new Shape();
+ movieClip.addChild(childShape);
+
+ movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0);
+ stage.rendererScale = 1;
+
+ const matrix = new Float32Array([1, 0, 0, 1, 0, 0]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]);
+
+ // キャッシュヒット状態を作る
+ $cacheStore.set(`${movieClip.instanceId}`, "bitmapKey", cacheKey);
+ $cacheStore.set(`${movieClip.instanceId}`, "bLocalBounds", new Float32Array([0, 0, 100, 80]));
+
+ // 子要素を変更
+ movieClip.addChild(new Shape());
+
+ renderQueue.offset = 0;
+ renderQueue.buffer.fill(0);
+
+ execute(movieClip, [], matrix, colorTransform, 800, 600);
+
+ // layerType=2 (cacheAsBitmap), cacheHit=1 → キャッシュが維持されている
+ expect(renderQueue.buffer[6]).toBe(2);
+ expect(renderQueue.buffer[7]).toBe(1);
+
+ // cleanup
+ $cacheStore.removeById(`${movieClip.instanceId}`);
+ movieClip.cacheAsBitmap = null;
+ renderQueue.buffer.fill(0);
+ renderQueue.offset = 0;
+ });
+
+ it("cacheAsBitmap: cache miss時にmatrix a/b/c/dがrenderScaleを使用(rendererScaleを含む)", () =>
+ {
+ const movieClip = new MovieClip();
+ const childShape = new Shape();
+ movieClip.addChild(childShape);
+
+ movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0);
+ // rendererScale=2でテスト(parentScaleではなくrendererScaleを使用)
+ stage.rendererScale = 2;
+
+ const matrix = new Float32Array([2, 0, 0, 2, 100, 50]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ $cacheStore.removeById(`${movieClip.instanceId}`);
+
+ renderQueue.offset = 0;
+ renderQueue.buffer.fill(0);
+
+ execute(movieClip, [], matrix, colorTransform, 800, 600);
+
+ // renderScaleX, renderScaleY = 2, parent matrix a,b,c,d
+ expect(renderQueue.buffer[14]).toBe(2); // renderScaleX
+ expect(renderQueue.buffer[15]).toBe(2); // renderScaleY
+ expect(renderQueue.buffer[16]).toBe(2); // matrix[0]
+ expect(renderQueue.buffer[17]).toBe(0); // matrix[1]
+
+ // cleanup
+ $cacheStore.removeById(`${movieClip.instanceId}`);
+ movieClip.cacheAsBitmap = null;
+ renderQueue.buffer.fill(0);
+ renderQueue.offset = 0;
+ stage.rendererScale = 1;
+ });
+
+ it("cacheAsBitmap: cache hit時にもrenderScaleを使用", () =>
+ {
+ const movieClip = new MovieClip();
+ const childShape = new Shape();
+ movieClip.addChild(childShape);
+
+ movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0);
+ // rendererScale=2でテスト
+ stage.rendererScale = 2;
+
+ const matrix = new Float32Array([2, 0, 0, 2, 100, 50]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ // renderScale = cacheScale(1) * ownScale(1) * rendererScale(2) = 2
+ const cacheKey = $cacheStore.generateKeys(2, 2, colorTransform[7]);
+ $cacheStore.set(
+ `${movieClip.instanceId}`,
+ "bitmapKey", cacheKey
+ );
+ // HIT高速パスに必要なローカルバウンズ
+ $cacheStore.set(
+ `${movieClip.instanceId}`,
+ "bLocalBounds",
+ new Float32Array([0, 0, 100, 80])
+ );
+
+ renderQueue.offset = 0;
+ renderQueue.buffer.fill(0);
+
+ execute(movieClip, [], matrix, colorTransform, 800, 600);
+
+ // cache hit
+ expect(renderQueue.buffer[7]).toBe(1);
+
+ // renderScaleX, renderScaleY = cacheScale(1) * ownScale(1) * rendererScale(2) = 2
+ expect(renderQueue.buffer[14]).toBe(2); // renderScaleX
+ expect(renderQueue.buffer[15]).toBe(2); // renderScaleY
+ // parent matrix a, b, c, d
+ expect(renderQueue.buffer[16]).toBe(2); // matrix[0] = parentScale
+ expect(renderQueue.buffer[17]).toBe(0); // matrix[1]
+ expect(renderQueue.buffer[18]).toBe(0); // matrix[2]
+ expect(renderQueue.buffer[19]).toBe(2); // matrix[3] = parentScale
+
+ // cleanup
+ $cacheStore.removeById(`${movieClip.instanceId}`);
+ movieClip.cacheAsBitmap = null;
+ stage.rendererScale = 1;
+ renderQueue.buffer.fill(0);
+ renderQueue.offset = 0;
+ });
+
+ it("cacheAsBitmap: rendererScale変更でキャッシュキーが変わる", () =>
+ {
+ const movieClip = new MovieClip();
+ movieClip.addChild(new Shape());
+
+ movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0);
+
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ // 実パイプラインでは$renderMatrixにrendererScaleが含まれるため
+ // matrixパラメータもrendererScaleを反映する必要がある
+
+ // rendererScale=1 でmatrix=[1,0,0,1]($renderMatrixシミュレート)
+ stage.rendererScale = 1;
+ const matrix1 = new Float32Array([1, 0, 0, 1, 0, 0]);
+ const cacheKey1 = $cacheStore.generateKeys(1, 1, colorTransform[7]);
+ $cacheStore.set(
+ `${movieClip.instanceId}`,
+ "bitmapKey", cacheKey1
+ );
+
+ // rendererScale=2 でmatrix=[2,0,0,2]($renderMatrixがscale=2に変更)
+ stage.rendererScale = 2;
+ const matrix2 = new Float32Array([2, 0, 0, 2, 0, 0]);
+ const cacheKey2 = $cacheStore.generateKeys(2, 2, colorTransform[7]);
+ expect(cacheKey1).not.toBe(cacheKey2);
+
+ renderQueue.offset = 0;
+ renderQueue.buffer.fill(0);
+
+ execute(movieClip, [], matrix2, colorTransform, 800, 600);
+
+ // cache miss(cacheHit=0): parentScaleX=2(rendererScale含む)→ キャッシュキー変更
+ expect(renderQueue.buffer[7]).toBe(0);
+
+ // cleanup
+ stage.rendererScale = 1;
+ $cacheStore.removeById(`${movieClip.instanceId}`);
+ movieClip.cacheAsBitmap = null;
+ renderQueue.buffer.fill(0);
+ renderQueue.offset = 0;
+ });
});
\ No newline at end of file
diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts
index 8d536d70..396045ab 100644
--- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts
+++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts
@@ -14,6 +14,7 @@ import { execute as displayObjectContainerGenerateClipQueueUseCase } from "../..
import { execute as displayObjectBlendToNumberService } from "../../DisplayObject/service/DisplayObjectBlendToNumberService";
import { execute as displayObjectContainerGetLayerBoundsUseCase } from "./DisplayObjectContainerGetLayerBoundsUseCase";
import { execute as displayObjectContainerCalcBoundsMatrixUseCase } from "../../DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase";
+import { stage } from "../../Stage";
import { renderQueue } from "@next2d/render-queue";
import { $cacheStore } from "@next2d/cache";
import {
@@ -22,7 +23,8 @@ import {
$poolBoundsArray,
$RENDERER_CONTAINER_TYPE,
$getFloat32Array8,
- $getFloat32Array6
+ $getFloat32Array6,
+ $poolFloat32Array6
} from "../../DisplayObjectUtil";
import {
ColorTransform,
@@ -114,157 +116,404 @@ export const execute = (
const blendMode = display_object_container.blendMode;
renderQueue.push(displayObjectBlendToNumberService(blendMode));
- // filters
- const filters = display_object_container.filters;
- if (filters) {
- const filterKey = $cacheStore.generateFilterKeys(
- tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3]
+ // cacheAsBitmap: フィルターキャッシュ形式でコンテナ全体をビットマップキャッシュ
+ // キャッシュテクスチャは親のスケールを含むサイズで描画し、
+ // コンポジットは setTransform(1,0,0,1,x,y) で1:1描画されるため正しい画面サイズになる
+ // 親の移動はキャッシュヒット(位置だけ更新)、スケール変更はキャッシュミス(再描画)
+ const cacheMatrix = display_object_container.cacheAsBitmap;
+ if (cacheMatrix) {
+
+ const m = cacheMatrix.rawData;
+ const cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+ const cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+
+ const ownScaleX = rawMatrix
+ ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1])
+ : 1;
+ const ownScaleY = rawMatrix
+ ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3])
+ : 1;
+
+ // Shape/TextFieldと同様にstage.rendererScaleを使用
+ // matrixから抽出すると親のアニメーション(スケール変動)でキーが揺れて
+ // 毎フレームcache MISSが発生する原因になる
+ const rendererScale = stage.rendererScale;
+
+ // コンポジットスケール = cacheScale × ownScale × rendererScale
+ const renderScaleX = cacheScaleX * ownScaleX * rendererScale;
+ const renderScaleY = cacheScaleY * ownScaleY * rendererScale;
+ const xRounded = Math.round(renderScaleX * 100) / 100;
+ const yRounded = Math.round(renderScaleY * 100) / 100;
+
+ const bitmapCacheKey = $cacheStore.generateKeys(
+ xRounded, yRounded, tColorTransform[7]
);
- const filterCache = $cacheStore.get(
+
+ // 固定プロパティ名で最新のキーのみ保存(古いキーが蓄積されないようにする)
+ // レンダラーも最新のfKeyのみ保持するため、キーの一致を保証
+ const bitmapCache = $cacheStore.get(
`${display_object_container.instanceId}`,
- `${filterKey}`
- );
+ "bitmapKey"
+ ) === bitmapCacheKey;
+
+ // コンテナ自身のフィルター境界を収集
+ const cacheFilters = display_object_container.filters;
+ const cacheFilterBounds = $getBoundsArray(0, 0, 0, 0);
+ if (cacheFilters) {
+ for (let idx = 0; idx < cacheFilters.length; idx++) {
+ const filter = cacheFilters[idx];
+ if (!filter || !filter.canApplyFilter()) {
+ continue;
+ }
+ filter.getBounds(cacheFilterBounds);
+ }
+ }
+
+ if (bitmapCache) {
+
+ // cacheAsBitmap cache hit: 子要素走査をスキップして高速パス
+ // Shapeと同様に親matrixをレンダラーに渡し、描画時にcacheScaleで補正
+ const cachedLocalBounds = $cacheStore.get(
+ `${display_object_container.instanceId}`, "bLocalBounds"
+ ) as Float32Array | null;
+
+ if (cachedLocalBounds) {
+ // ローカルバウンズ原点のスクリーン座標を計算(Shapeと同じ方式)
+ const localOriginX = cachedLocalBounds[0] / (cacheScaleX * rendererScale);
+ const localOriginY = cachedLocalBounds[1] / (cacheScaleY * rendererScale);
+ const screenX = matrix[0] * localOriginX + matrix[2] * localOriginY + matrix[4];
+ const screenY = matrix[1] * localOriginX + matrix[3] * localOriginY + matrix[5];
+
+ // Shapeと同様にmatrixにcacheScaleを乗算して送る
+ // レンダラーで matrix/renderScale → cacheScale成分が反映される
+ renderQueue.push(1,
+ 0, 0, // layerWidth/Height: HIT時は未使用
+ 2, 1, display_object_container.instanceId, bitmapCacheKey,
+ cacheFilterBounds[0], cacheFilterBounds[1], cacheFilterBounds[2], cacheFilterBounds[3],
+ renderScaleX, renderScaleY,
+ matrix[0] * cacheScaleX, matrix[1] * cacheScaleX,
+ matrix[2] * cacheScaleY, matrix[3] * cacheScaleY,
+ screenX, screenY,
+ tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
+ tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7]
+ );
- let updated = false;
- const params = [];
- const bounds = $getBoundsArray(0, 0, 0, 0);
- for (let idx = 0; idx < filters.length; idx++) {
+ $poolBoundsArray(cacheFilterBounds);
- const filter = filters[idx];
- if (!filter || !filter.canApplyFilter()) {
- continue;
+ if (tColorTransform !== color_transform) {
+ ColorTransform.release(tColorTransform);
+ }
+ if (tMatrix !== matrix) {
+ Matrix.release(tMatrix);
+ }
+ return ;
}
+ // cachedLocalBoundsがない場合はMISSにフォールスルー
+ }
- // フィルターが更新されたかをチェック
- if (filter.$updated) {
- updated = true;
+ // cacheAsBitmap cache miss: 初回描画
+ // 親matrixのスケール成分のみを含むローカル空間でキャッシュを生成する
+ // (親の位置・回転は含まず、スケールのみ反映してテクスチャサイズを画面と一致させる)
+
+ // フィルターパラメータを収集
+ const cacheFilterParams: number[] = [];
+ if (cacheFilters) {
+ for (let idx = 0; idx < cacheFilters.length; idx++) {
+ const filter = cacheFilters[idx];
+ if (!filter || !filter.canApplyFilter()) {
+ continue;
+ }
+ const buffer = filter.toNumberArray();
+ for (let idx = 0; idx < buffer.length; idx += 4096) {
+ cacheFilterParams.push(...buffer.subarray(idx, idx + 4096));
+ }
}
- filter.$updated = false;
+ }
- filter.getBounds(bounds);
+ // キャッシュ描画用の親matrix: cacheScale × rendererScale(対角行列)
+ const cacheParentMatrix = $getFloat32Array6(
+ cacheScaleX * rendererScale, 0,
+ 0, cacheScaleY * rendererScale,
+ 0, 0
+ );
+ const localLayerBounds = displayObjectContainerGetLayerBoundsUseCase(
+ display_object_container, cacheParentMatrix
+ );
- const buffer = filter.toNumberArray();
+ const localLayerWidth = Math.ceil(Math.abs(localLayerBounds[2] - localLayerBounds[0]));
+ const localLayerHeight = Math.ceil(Math.abs(localLayerBounds[3] - localLayerBounds[1]));
+
+ // Shapeと同じ方式でスクリーン座標を計算
+ const localOriginX = localLayerBounds[0] / (cacheScaleX * rendererScale);
+ const localOriginY = localLayerBounds[1] / (cacheScaleY * rendererScale);
+ const missScreenX = matrix[0] * localOriginX + matrix[2] * localOriginY + matrix[4];
+ const missScreenY = matrix[1] * localOriginX + matrix[3] * localOriginY + matrix[5];
+
+ renderQueue.push(
+ 1,
+ localLayerWidth, localLayerHeight,
+ 2, 0, display_object_container.instanceId, bitmapCacheKey,
+ cacheFilterBounds[0], cacheFilterBounds[1], cacheFilterBounds[2], cacheFilterBounds[3],
+ renderScaleX, renderScaleY,
+ matrix[0] * cacheScaleX, matrix[1] * cacheScaleX,
+ matrix[2] * cacheScaleY, matrix[3] * cacheScaleY,
+ missScreenX, missScreenY,
+ tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
+ tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7],
+ cacheFilterParams.length
+ );
+ if (cacheFilterParams.length > 0) {
+ renderQueue.set(new Float32Array(cacheFilterParams));
+ }
- for (let idx = 0; idx < buffer.length; idx += 4096) {
- params.push(...buffer.subarray(idx, idx + 4096));
- }
+ // 子要素の描画マトリクスをローカル空間に切り替え(親matrixの位置・回転を除外)
+ // cacheParentMatrixにrawMatrixを乗算し、layerBoundsオフセットで原点を調整
+ let localMatrix: Float32Array;
+ if (rawMatrix) {
+ localMatrix = Matrix.multiply(cacheParentMatrix, rawMatrix);
+ } else {
+ localMatrix = $getFloat32Array6(
+ cacheParentMatrix[0], 0,
+ 0, cacheParentMatrix[3],
+ 0, 0
+ );
}
- const useFilfer = params.length > 0;
- if (useFilfer) {
+ const localTx = localMatrix[4] - localLayerBounds[0];
+ const localTy = localMatrix[5] - localLayerBounds[1];
- // 子の変更があった場合は親のフラグが立っているので更新
- if (!updated) {
- updated = display_object_container.changed;
- }
+ if (tMatrix !== matrix) {
+ Matrix.release(tMatrix);
+ }
+ tMatrix = $getFloat32Array6(
+ localMatrix[0], localMatrix[1],
+ localMatrix[2], localMatrix[3],
+ localTx, localTy
+ );
+
+ if (rawMatrix) {
+ Matrix.release(localMatrix);
+ } else {
+ $poolFloat32Array6(localMatrix);
+ }
+ Matrix.release(cacheParentMatrix);
+
+ if (tColorTransform !== color_transform) {
+ ColorTransform.release(tColorTransform);
+ }
+ tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0);
- const layerBounds = displayObjectContainerGetLayerBoundsUseCase(
- display_object_container, matrix
+ // HIT時の高速パス用にローカルバウンズをキャッシュ(poolする前に保存)
+ $cacheStore.set(
+ `${display_object_container.instanceId}`,
+ "bLocalBounds",
+ new Float32Array([localLayerBounds[0], localLayerBounds[1], localLayerBounds[2], localLayerBounds[3]])
+ );
+
+ $poolBoundsArray(localLayerBounds);
+ $poolBoundsArray(cacheFilterBounds);
+
+ $cacheStore.set(
+ `${display_object_container.instanceId}`,
+ "bitmapKey", bitmapCacheKey
+ );
+
+ } else {
+
+ // filters
+ const filters = display_object_container.filters;
+ if (filters) {
+ const filterKey = $cacheStore.generateFilterKeys(
+ tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3]
+ );
+ const filterCache = $cacheStore.get(
+ `${display_object_container.instanceId}`,
+ `${filterKey}`
);
- if (filterCache) {
+ let updated = false;
+ const params = [];
+ const bounds = $getBoundsArray(0, 0, 0, 0);
+ for (let idx = 0; idx < filters.length; idx++) {
+
+ const filter = filters[idx];
+ if (!filter || !filter.canApplyFilter()) {
+ continue;
+ }
+
+ // フィルターが更新されたかをチェック
+ if (filter.$updated) {
+ updated = true;
+ }
+ filter.$updated = false;
+
+ filter.getBounds(bounds);
- // キャッシュがあって、変更がなければキャッシュを使用
+ const buffer = filter.toNumberArray();
+
+ for (let idx = 0; idx < buffer.length; idx += 4096) {
+ params.push(...buffer.subarray(idx, idx + 4096));
+ }
+ }
+
+ const useFilfer = params.length > 0;
+ if (useFilfer) {
+
+ // 子の変更があった場合は親のフラグが立っているので更新
if (!updated) {
- renderQueue.push(1,
- Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])),
- Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])),
- 1, 1, display_object_container.instanceId, filterKey,
- bounds[0], bounds[1], bounds[2], bounds[3],
- tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1],
- tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
- tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7]
- );
+ updated = display_object_container.changed;
+ }
- $poolBoundsArray(layerBounds);
- $poolBoundsArray(bounds);
+ const layerBounds = displayObjectContainerGetLayerBoundsUseCase(
+ display_object_container, matrix
+ );
- if (tColorTransform !== color_transform) {
- ColorTransform.release(tColorTransform);
- }
- if (tMatrix !== matrix) {
- Matrix.release(tMatrix);
+ if (filterCache) {
+
+ // キャッシュがあって、変更がなければキャッシュを使用
+ if (!updated) {
+ renderQueue.push(1,
+ Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])),
+ Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])),
+ 1, 1, display_object_container.instanceId, filterKey,
+ bounds[0], bounds[1], bounds[2], bounds[3],
+ tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1],
+ tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
+ tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7]
+ );
+
+ $poolBoundsArray(layerBounds);
+ $poolBoundsArray(bounds);
+
+ if (tColorTransform !== color_transform) {
+ ColorTransform.release(tColorTransform);
+ }
+ if (tMatrix !== matrix) {
+ Matrix.release(tMatrix);
+ }
+ return ;
}
- return ;
+
+ // どこかで変更があったので、キャッシュを削除
+ $cacheStore.removeById(`${display_object_container.instanceId}`);
}
- // どこかで変更があったので、キャッシュを削除
- $cacheStore.removeById(`${display_object_container.instanceId}`);
- }
+ renderQueue.push(
+ 1,
+ Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])),
+ Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])),
+ 1, 0, display_object_container.instanceId, filterKey,
+ bounds[0], bounds[1], bounds[2], bounds[3],
+ tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1],
+ tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
+ tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7],
+ params.length
+ );
+ renderQueue.set(new Float32Array(params));
- renderQueue.push(
- 1,
- Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])),
- Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])),
- 1, 0, display_object_container.instanceId, filterKey,
- bounds[0], bounds[1], bounds[2], bounds[3],
- tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1],
- tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
- tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7],
- params.length
- );
- renderQueue.set(new Float32Array(params));
+ const fa0 = tMatrix[0];
+ const fa1 = tMatrix[1];
+ const fa2 = tMatrix[2];
+ const fa3 = tMatrix[3];
+ const faTx = tMatrix[4] - layerBounds[0];
+ const faTy = tMatrix[5] - layerBounds[1];
- const fa0 = tMatrix[0];
- const fa1 = tMatrix[1];
- const fa2 = tMatrix[2];
- const fa3 = tMatrix[3];
- const faTx = tMatrix[4] - layerBounds[0];
- const faTy = tMatrix[5] - layerBounds[1];
+ if (tMatrix !== matrix) {
+ Matrix.release(tMatrix);
+ }
+ tMatrix = $getFloat32Array6(fa0, fa1, fa2, fa3, faTx, faTy);
- if (tMatrix !== matrix) {
- Matrix.release(tMatrix);
- }
- tMatrix = $getFloat32Array6(fa0, fa1, fa2, fa3, faTx, faTy);
+ if (tColorTransform !== color_transform) {
+ ColorTransform.release(tColorTransform);
+ }
- if (tColorTransform !== color_transform) {
- ColorTransform.release(tColorTransform);
- }
+ tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0);
- tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0);
+ $poolBoundsArray(layerBounds);
- $poolBoundsArray(layerBounds);
+ $cacheStore.set(
+ `${display_object_container.instanceId}`,
+ `${filterKey}`, true
+ );
- $cacheStore.set(
- `${display_object_container.instanceId}`,
- `${filterKey}`, true
- );
+ } else {
+ if (blendMode === "normal") {
+ renderQueue.push(0);
+ } else {
+
+ // ブレンドモードのみのLayerモード
+ const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase(
+ display_object_container,
+ matrix
+ );
+ const layerXMin = layerBounds[0];
+ const layerYMin = layerBounds[1];
+
+ renderQueue.push(
+ 1,
+ Math.ceil(Math.abs(layerBounds[2] - layerXMin)),
+ Math.ceil(Math.abs(layerBounds[3] - layerYMin)),
+ 0, // not use filter,
+ tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin, layerYMin,
+ tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
+ tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7]
+ );
+
+ const a0 = tMatrix[0];
+ const a1 = tMatrix[1];
+ const a2 = tMatrix[2];
+ const a3 = tMatrix[3];
+ const adjustedTx1 = tMatrix[4] - layerXMin;
+ const adjustedTy1 = tMatrix[5] - layerYMin;
+
+ if (tMatrix !== matrix) {
+ Matrix.release(tMatrix);
+ }
+ tMatrix = $getFloat32Array6(a0, a1, a2, a3, adjustedTx1, adjustedTy1);
+ $poolBoundsArray(layerBounds);
+
+ if (tColorTransform !== color_transform) {
+ ColorTransform.release(tColorTransform);
+ }
+ tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0);
+ }
+ }
+
+ $poolBoundsArray(bounds);
} else {
if (blendMode === "normal") {
renderQueue.push(0);
} else {
-
- // ブレンドモードのみのLayerモード
const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase(
display_object_container,
matrix
);
- const layerXMin = layerBounds[0];
- const layerYMin = layerBounds[1];
+ const layerXMin2 = layerBounds[0];
+ const layerYMin2 = layerBounds[1];
renderQueue.push(
1,
- Math.ceil(Math.abs(layerBounds[2] - layerXMin)),
- Math.ceil(Math.abs(layerBounds[3] - layerYMin)),
+ Math.ceil(Math.abs(layerBounds[2] - layerXMin2)),
+ Math.ceil(Math.abs(layerBounds[3] - layerYMin2)),
0, // not use filter,
- tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin, layerYMin,
+ tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin2, layerYMin2,
tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7]
);
- const a0 = tMatrix[0];
- const a1 = tMatrix[1];
- const a2 = tMatrix[2];
- const a3 = tMatrix[3];
- const adjustedTx1 = tMatrix[4] - layerXMin;
- const adjustedTy1 = tMatrix[5] - layerYMin;
+ const b0 = tMatrix[0];
+ const b1 = tMatrix[1];
+ const b2 = tMatrix[2];
+ const b3 = tMatrix[3];
+ const adjustedTx2 = tMatrix[4] - layerXMin2;
+ const adjustedTy2 = tMatrix[5] - layerYMin2;
if (tMatrix !== matrix) {
Matrix.release(tMatrix);
}
- tMatrix = $getFloat32Array6(a0, a1, a2, a3, adjustedTx1, adjustedTy1);
+ tMatrix = $getFloat32Array6(b0, b1, b2, b3, adjustedTx2, adjustedTy2);
$poolBoundsArray(layerBounds);
if (tColorTransform !== color_transform) {
@@ -274,48 +523,7 @@ export const execute =
(
}
}
- $poolBoundsArray(bounds);
- } else {
- if (blendMode === "normal") {
- renderQueue.push(0);
- } else {
- const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase(
- display_object_container,
- matrix
- );
-
- const layerXMin2 = layerBounds[0];
- const layerYMin2 = layerBounds[1];
-
- renderQueue.push(
- 1,
- Math.ceil(Math.abs(layerBounds[2] - layerXMin2)),
- Math.ceil(Math.abs(layerBounds[3] - layerYMin2)),
- 0, // not use filter,
- tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin2, layerYMin2,
- tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
- tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7]
- );
-
- const b0 = tMatrix[0];
- const b1 = tMatrix[1];
- const b2 = tMatrix[2];
- const b3 = tMatrix[3];
- const adjustedTx2 = tMatrix[4] - layerXMin2;
- const adjustedTy2 = tMatrix[5] - layerYMin2;
-
- if (tMatrix !== matrix) {
- Matrix.release(tMatrix);
- }
- tMatrix = $getFloat32Array6(b0, b1, b2, b3, adjustedTx2, adjustedTy2);
- $poolBoundsArray(layerBounds);
-
- if (tColorTransform !== color_transform) {
- ColorTransform.release(tColorTransform);
- }
- tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0);
- }
- }
+ } // end cacheAsBitmap else
// mask
const maskDisplayObject = display_object_container.mask;
diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerMouseHitUseCase.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerMouseHitUseCase.ts
index 357c8d07..5a8e393a 100644
--- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerMouseHitUseCase.ts
+++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerMouseHitUseCase.ts
@@ -15,7 +15,9 @@ import {
$getArray,
$poolArray,
$getMap,
- $poolMap
+ $poolMap,
+ $getFloat32Array6,
+ $poolFloat32Array6
} from "../../DisplayObjectUtil";
/**
@@ -44,11 +46,35 @@ export const execute =
=>
{
if (object.type === "zlib") {
- await new Promise((resolve): void =>
+ await new Promise((resolve, reject): void =>
{
$unzipWorker.onmessage = (event: MessageEvent): void =>
{
+ if (event.data && event.data.error) {
+ reject(new Error(event.data.error));
+ return;
+ }
loaderBuildService(loader, event.data as IAnimationToolData);
resolve();
};
+ $unzipWorker.onerror = (event: ErrorEvent): void =>
+ {
+ reject(new Error(event.message));
+ };
+
const buffer: Uint8Array = new Uint8Array(object.buffer);
$unzipWorker.postMessage(buffer, [buffer.buffer]);
});
diff --git a/packages/display/src/Loader/worker/ZlibInflateWorker.ts b/packages/display/src/Loader/worker/ZlibInflateWorker.ts
index 6653e1d6..f8f98825 100644
--- a/packages/display/src/Loader/worker/ZlibInflateWorker.ts
+++ b/packages/display/src/Loader/worker/ZlibInflateWorker.ts
@@ -1,26 +1,416 @@
"use strict";
-import { decompressSync } from "fflate";
+/**
+ * @description fflate非依存の高速 zlib/DEFLATE 解凍ワーカー (RFC 1950 / RFC 1951)
+ * High-performance zlib/DEFLATE decompression worker without fflate dependency.
+ */
+
+const _$LEN_BASE = new Uint16Array([
+ 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,
+ 35,43,51,59,67,83,99,115,131,163,195,227,258
+]);
+const _$LEN_EXTRA = new Uint8Array([
+ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,
+ 3,3,3,3,4,4,4,4,5,5,5,5,0
+]);
+const _$DIST_BASE = new Uint16Array([
+ 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,
+ 257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577
+]);
+const _$DIST_EXTRA = new Uint8Array([
+ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,
+ 7,7,8,8,9,9,10,10,11,11,12,12,13,13
+]);
+const _$CL_ORDER = new Uint8Array([
+ 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15
+]);
+
+const _$MASK = new Uint32Array(17);
+for (let i = 1; i < 17; i++) { _$MASK[i] = (1 << i) - 1 }
+
+const _$FIXED_LIT_CL = new Uint8Array(288);
+for (let i = 0; i <= 143; i++) { _$FIXED_LIT_CL[i] = 8 }
+for (let i = 144; i <= 255; i++) { _$FIXED_LIT_CL[i] = 9 }
+for (let i = 256; i <= 279; i++) { _$FIXED_LIT_CL[i] = 7 }
+for (let i = 280; i <= 287; i++) { _$FIXED_LIT_CL[i] = 8 }
+
+const _$FIXED_DIST_CL = new Uint8Array(32).fill(5);
+
+const _$decoder = new TextDecoder();
+
+let _$outBuf = new Uint8Array(65536);
+
+const _$blCount = new Uint16Array(16);
+const _$nextCode = new Uint16Array(16);
+const _$clLens = new Uint8Array(19);
+const _$codeLens = new Uint8Array(320);
+
+let _$dynClTbl: Uint32Array = new Uint32Array(128);
+let _$dynLitTbl: Uint32Array = new Uint32Array(2048);
+let _$dynDistTbl: Uint32Array = new Uint32Array(1024);
+
+/**
+ * @description canonical Huffmanルックアップテーブルを構築する。
+ * 各エントリは (symbol << 4) | codeLength の形式で格納される。
+ * Builds a canonical Huffman lookup table. Each entry stores (symbol << 4) | codeLength.
+ *
+ * @param {Uint8Array} codeLens - シンボルごとのコード長配列
+ * @param {number} n - codeLensの有効要素数
+ * @param {Uint32Array} [reuse] - 再利用するテーブルバッファ(GC回避用)
+ * @return {[Uint32Array, number]} [テーブル, 最大コード長]
+ * @method
+ * @private
+ */
+const _$buildTable = (codeLens: Uint8Array, n: number, reuse?: Uint32Array): [Uint32Array, number] =>
+{
+ let max = 0;
+ _$blCount.fill(0);
+ for (let i = 0; i < n; i++) {
+ const l = codeLens[i];
+ if (l) {
+ _$blCount[l]++;
+ if (l > max) { max = l }
+ }
+ }
+ if (!max) {
+ if (reuse) {
+ reuse[0] = 0;
+ return [reuse, 1];
+ }
+ return [new Uint32Array(2), 1];
+ }
+
+ _$nextCode.fill(0);
+ for (let b = 1, c = 0; b <= max; b++) {
+ c = c + _$blCount[b - 1] << 1;
+ _$nextCode[b] = c;
+ }
+
+ const size = 1 << max;
+ let tbl: Uint32Array;
+ if (reuse && reuse.length >= size) {
+ tbl = reuse;
+ tbl.fill(0, 0, size);
+ } else {
+ tbl = new Uint32Array(size);
+ }
+
+ for (let s = 0; s < n; s++) {
+ const l = codeLens[s];
+ if (!l) { continue }
+ let c = _$nextCode[l]++;
+ let r = 0;
+ for (let i = 0; i < l; i++) {
+ r = r << 1 | c & 1;
+ c >>= 1;
+ }
+ const entry = s << 4 | l;
+ for (let j = r; j < size; j += 1 << l) {
+ tbl[j] = entry;
+ }
+ }
+
+ return [tbl, max];
+};
+
+const [_$FIXED_LIT_TBL, _$FIXED_LIT_BITS] = _$buildTable(_$FIXED_LIT_CL, 288);
+const [_$FIXED_DIST_TBL, _$FIXED_DIST_BITS] = _$buildTable(_$FIXED_DIST_CL, 32);
+const _$FIXED_LIT_MASK = _$MASK[_$FIXED_LIT_BITS];
+const _$FIXED_DIST_MASK = _$MASK[_$FIXED_DIST_BITS];
+
+let _$src: Uint8Array;
+let _$pos: number;
+let _$buf: number;
+let _$cnt: number;
+
+/**
+ * @description ビットストリームからnビットを読み取って返す。
+ * Reads n bits from the bit stream and returns the value.
+ *
+ * @param {number} n - 読み取るビット数
+ * @return {number} 読み取った値
+ * @method
+ * @private
+ */
+const _$bits = (n: number): number =>
+{
+ while (_$cnt < n) {
+ _$buf |= _$src[_$pos++] << _$cnt;
+ _$cnt += 8;
+ }
+ const v = _$buf & _$MASK[n];
+ _$buf >>>= n;
+ _$cnt -= n;
+ return v;
+};
+
+/**
+ * @description Huffmanルックアップテーブルから1シンボルをデコードする。
+ * Decodes one symbol from the Huffman lookup table.
+ *
+ * @param {Uint32Array} tbl - Huffmanルックアップテーブル
+ * @param {number} maxBits - テーブルの最大コード長
+ * @param {number} mask - ビットマスク ((1 << maxBits) - 1)
+ * @return {number} デコードされたシンボル値
+ * @method
+ * @private
+ */
+const _$huf = (tbl: Uint32Array, maxBits: number, mask: number): number =>
+{
+ while (_$cnt < maxBits) {
+ _$buf |= _$src[_$pos++] << _$cnt;
+ _$cnt += 8;
+ }
+ const e = tbl[_$buf & mask];
+ _$buf >>>= e & 0xF;
+ _$cnt -= e & 0xF;
+ return e >> 4;
+};
+
+/**
+ * @description DEFLATEストリームを解凍する (RFC 1951)。
+ * stored / fixed Huffman / dynamic Huffman の全ブロックタイプに対応。
+ * Decompresses a DEFLATE stream. Supports all block types: stored, fixed and dynamic Huffman.
+ *
+ * @param {Uint8Array} data - 圧縮データ
+ * @param {number} offset - DEFLATEストリームの開始オフセット
+ * @return {Uint8Array} 解凍されたバイト列
+ * @method
+ * @private
+ */
+const _$inflate = (data: Uint8Array, offset: number): Uint8Array =>
+{
+ _$src = data;
+ _$pos = offset;
+ _$buf = 0;
+ _$cnt = 0;
+
+ const estimatedSize = Math.max(data.length * 6, 4096);
+ if (_$outBuf.length < estimatedSize) {
+ _$outBuf = new Uint8Array(estimatedSize);
+ }
+ let out = _$outBuf;
+ let op = 0;
+
+ let fin = 0;
+ while (!fin) {
+ fin = _$bits(1);
+ const bt = _$bits(2);
+
+ if (bt === 0) {
+ const skip = _$cnt & 7;
+ _$buf >>>= skip;
+ _$cnt -= skip;
+ const len = _$bits(16);
+ _$bits(16);
+
+ if (op + len > out.length) {
+ let sz = out.length;
+ while (sz < op + len) { sz <<= 1 }
+ const nb = new Uint8Array(sz);
+ nb.set(out);
+ out = nb;
+ }
+
+ out.set(_$src.subarray(_$pos, _$pos + len), op);
+ _$pos += len;
+ op += len;
+
+ } else if (bt === 3) {
+ throw new Error("Invalid DEFLATE block type");
+
+ } else {
+ let lt: Uint32Array, lm: number, lb: number;
+ let dt: Uint32Array, dm: number, db: number;
+
+ if (bt === 1) {
+ lt = _$FIXED_LIT_TBL;
+ lb = _$FIXED_LIT_BITS;
+ lm = _$FIXED_LIT_MASK;
+ dt = _$FIXED_DIST_TBL;
+ db = _$FIXED_DIST_BITS;
+ dm = _$FIXED_DIST_MASK;
+ } else {
+ const hlit = _$bits(5) + 257;
+ const hdist = _$bits(5) + 1;
+ const hclen = _$bits(4) + 4;
+
+ _$clLens.fill(0);
+ for (let i = 0; i < hclen; i++) {
+ _$clLens[_$CL_ORDER[i]] = _$bits(3);
+ }
+ let clb: number;
+ [_$dynClTbl, clb] = _$buildTable(_$clLens, 19, _$dynClTbl);
+ const clm = _$MASK[clb];
+
+ const total = hlit + hdist;
+ _$codeLens.fill(0, 0, total);
+ for (let i = 0; i < total;) {
+ const s = _$huf(_$dynClTbl, clb, clm);
+ if (s < 16) {
+ _$codeLens[i++] = s;
+ } else if (s === 16) {
+ const p = _$codeLens[i - 1];
+ for (let r = _$bits(2) + 3; r > 0; r--) { _$codeLens[i++] = p }
+ } else if (s === 17) {
+ i += _$bits(3) + 3;
+ } else {
+ i += _$bits(7) + 11;
+ }
+ }
+
+ [_$dynLitTbl, lb] = _$buildTable(_$codeLens.subarray(0, hlit), hlit, _$dynLitTbl);
+ lm = _$MASK[lb];
+ lt = _$dynLitTbl;
+ [_$dynDistTbl, db] = _$buildTable(_$codeLens.subarray(hlit, total), hdist, _$dynDistTbl);
+ dm = _$MASK[db];
+ dt = _$dynDistTbl;
+ }
+
+ for (;;) {
+
+ while (_$cnt < lb) {
+ _$buf |= _$src[_$pos++] << _$cnt;
+ _$cnt += 8;
+ }
+ const le = lt[_$buf & lm];
+ const sl = le & 0xF;
+ _$buf >>>= sl;
+ _$cnt -= sl;
+ const sym = le >> 4;
+
+ if (sym < 256) {
+ if (op >= out.length) {
+ const nb = new Uint8Array(out.length << 1);
+ nb.set(out);
+ out = nb;
+ }
+ out[op++] = sym;
+
+ } else if (sym === 256) {
+ break;
+
+ } else {
+ const li = sym - 257;
+ let length = _$LEN_BASE[li];
+ const le2 = _$LEN_EXTRA[li];
+ if (le2) {
+ while (_$cnt < le2) {
+ _$buf |= _$src[_$pos++] << _$cnt;
+ _$cnt += 8;
+ }
+ length += _$buf & _$MASK[le2];
+ _$buf >>>= le2;
+ _$cnt -= le2;
+ }
+
+ while (_$cnt < db) {
+ _$buf |= _$src[_$pos++] << _$cnt;
+ _$cnt += 8;
+ }
+ const de = dt[_$buf & dm];
+ const dl = de & 0xF;
+ _$buf >>>= dl;
+ _$cnt -= dl;
+ const di = de >> 4;
+
+ let dist = _$DIST_BASE[di];
+ const de2 = _$DIST_EXTRA[di];
+ if (de2) {
+ while (_$cnt < de2) {
+ _$buf |= _$src[_$pos++] << _$cnt;
+ _$cnt += 8;
+ }
+ dist += _$buf & _$MASK[de2];
+ _$buf >>>= de2;
+ _$cnt -= de2;
+ }
+
+ if (op + length > out.length) {
+ let sz = out.length;
+ while (sz < op + length) { sz <<= 1 }
+ const nb = new Uint8Array(sz);
+ nb.set(out);
+ out = nb;
+ }
+
+ const sp = op - dist;
+ if (dist === 1) {
+ out.fill(out[sp], op, op + length);
+ op += length;
+ } else if (dist >= length) {
+ out.copyWithin(op, sp, sp + length);
+ op += length;
+ } else {
+ out.copyWithin(op, sp, sp + dist);
+ let copied = dist;
+ while (copied < length) {
+ const chunk = Math.min(copied, length - copied);
+ out.copyWithin(op + copied, op, op + chunk);
+ copied += chunk;
+ }
+ op += length;
+ }
+ }
+ }
+ }
+ }
+
+ _$outBuf = out;
+
+ return out.subarray(0, op);
+};
/**
- * @description zilbの圧縮されたデータを解凍します。
- * Unzips zlib-compressed data.
+ * @description zlibラッパー付きデータを解凍する (RFC 1950)。
+ * zlibヘッダーを検出した場合は2バイトスキップし、それ以外は生DEFLATEとして処理する。
+ * Decompresses zlib-wrapped data. Skips the 2-byte zlib header if detected,
+ * otherwise treats input as raw DEFLATE.
*
- * @param {MessageEvent} event
+ * @param {Uint8Array} input - zlib圧縮データまたは生DEFLATEストリーム
+ * @return {Uint8Array} 解凍されたバイト列
+ * @method
+ * @private
+ */
+const _$zlibDecompress = (input: Uint8Array): Uint8Array =>
+{
+ const cmf = input[0];
+ const flg = input[1];
+
+ const isZlib = (cmf & 0x0F) === 8
+ && (cmf * 256 + flg) % 31 === 0
+ && !(flg & 0x20);
+
+ return _$inflate(input, isZlib ? 2 : 0);
+};
+
+/**
+ * @description zlibの圧縮されたデータを解凍し、JSONとしてパースして返すワーカーエントリポイント。
+ * Worker entry point that decompresses zlib data, decodes as URI-encoded string,
+ * parses as JSON and posts the result back.
+ *
+ * @param {MessageEvent} event - 圧縮データ (Uint8Array) を含むメッセージイベント
* @return {void}
* @method
* @public
*/
self.addEventListener("message", (event: MessageEvent): void =>
{
- const buffer = decompressSync(event.data);
+ try {
- let json = "";
- for (let idx: number = 0; idx < buffer.length; idx += 4096) {
- json += String.fromCharCode(...buffer.subarray(idx, idx + 4096));
- }
+ const buffer = _$zlibDecompress(event.data);
+
+ self.postMessage(JSON.parse(
+ decodeURIComponent(_$decoder.decode(buffer))
+ ));
+
+ } catch (e) {
- self.postMessage(JSON.parse(decodeURIComponent(json)));
+ self.postMessage({
+ "error": e instanceof Error ? e.message : "Unknown decompression error"
+ });
+
+ }
});
export default {};
\ No newline at end of file
diff --git a/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts b/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts
index d8691b7e..84a5f3d7 100644
--- a/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts
+++ b/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts
@@ -3,7 +3,11 @@ import { Matrix } from "@next2d/geom";
import { execute as displayObjectCalcBoundsMatrixService } from "../../DisplayObject/service/DisplayObjectCalcBoundsMatrixService";
import { execute as shapeGetRawBoundsService } from "../service/ShapeGetRawBoundsService";
import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase";
-import { $poolBoundsArray } from "../../DisplayObjectUtil";
+import {
+ $poolBoundsArray,
+ $getFloat32Array6,
+ $poolFloat32Array6
+} from "../../DisplayObjectUtil";
/**
* @description Shapeの描画範囲を計算します。
@@ -19,7 +23,27 @@ export const execute = (shape: Shape, matrix: Float32Array | null = null): Float
{
const rawBounds = shapeGetRawBoundsService(shape);
- const rawMatrix = displayObjectGetRawMatrixUseCase(shape);
+ let rawMatrix = displayObjectGetRawMatrixUseCase(shape);
+
+ // cacheAsBitmap倍率をrawMatrixに適用
+ const cacheMatrix = shape.cacheAsBitmap;
+ let scaledMatrix: Float32Array | null = null;
+ if (cacheMatrix) {
+ const m = cacheMatrix.rawData;
+ const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+ const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+ if (rawMatrix) {
+ scaledMatrix = $getFloat32Array6(
+ rawMatrix[0] * csx, rawMatrix[1] * csx,
+ rawMatrix[2] * csy, rawMatrix[3] * csy,
+ rawMatrix[4], rawMatrix[5]
+ );
+ } else {
+ scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0);
+ }
+ rawMatrix = scaledMatrix;
+ }
+
if (!rawMatrix) {
if (matrix) {
const calcBounds = displayObjectCalcBoundsMatrixService(
@@ -41,6 +65,9 @@ export const execute = (shape: Shape, matrix: Float32Array | null = null): Float
matrix ? Matrix.multiply(matrix, rawMatrix) : rawMatrix
);
+ if (scaledMatrix) {
+ $poolFloat32Array6(scaledMatrix);
+ }
$poolBoundsArray(rawBounds);
return calcBounds;
diff --git a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts
index ccdac919..c3af0def 100644
--- a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts
+++ b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts
@@ -92,10 +92,10 @@ export const execute = (
tMatrix
);
- const xMin = bounds[0];
- const yMin = bounds[1];
- const xMax = bounds[2];
- const yMax = bounds[3];
+ let xMin = bounds[0];
+ let yMin = bounds[1];
+ let xMax = bounds[2];
+ let yMax = bounds[3];
$poolBoundsArray(bounds);
const width = Math.ceil(Math.abs(xMax - xMin));
@@ -156,20 +156,67 @@ export const execute = (
}
}
- const xScale = Math.sqrt(
- tMatrix[0] * tMatrix[0]
- + tMatrix[1] * tMatrix[1]
- );
-
- const yScale = Math.sqrt(
- tMatrix[2] * tMatrix[2]
- + tMatrix[3] * tMatrix[3]
- );
+ // cacheAsBitmap: 指定Matrix × 自身のスケール × stageのrendererScaleでキャッシュ品質を決定
+ // 1.0基準: Matrix(1,0,0,1)はdisplayObjectの等倍スケールを意味する
+ const cacheMatrix = shape.cacheAsBitmap;
+ let renderXScale: number;
+ let renderYScale: number;
+ let cacheScaleX = 1;
+ let cacheScaleY = 1;
+ if (cacheMatrix) {
+ const m = cacheMatrix.rawData;
+ cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+ cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+
+ const ownScaleX = rawMatrix
+ ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1])
+ : 1;
+ const ownScaleY = rawMatrix
+ ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3])
+ : 1;
+
+ renderXScale = cacheScaleX * ownScaleX * stage.rendererScale;
+ renderYScale = cacheScaleY * ownScaleY * stage.rendererScale;
+
+ // cacheMatrix倍率をスクリーン座標のboundsにも反映
+ if (cacheScaleX !== 1 || cacheScaleY !== 1) {
+ const modMatrix = $getFloat32Array6(
+ tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX,
+ tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY,
+ tMatrix[4], tMatrix[5]
+ );
+ const modBounds = displayObjectCalcBoundsMatrixService(
+ graphics.xMin, graphics.yMin,
+ graphics.xMax, graphics.yMax,
+ modMatrix
+ );
+ xMin = modBounds[0];
+ yMin = modBounds[1];
+ xMax = modBounds[2];
+ yMax = modBounds[3];
+ $poolBoundsArray(modBounds);
+ $poolFloat32Array6(modMatrix);
+ }
+ } else {
+ renderXScale = Math.sqrt(
+ tMatrix[0] * tMatrix[0]
+ + tMatrix[1] * tMatrix[1]
+ );
+ renderYScale = Math.sqrt(
+ tMatrix[2] * tMatrix[2]
+ + tMatrix[3] * tMatrix[3]
+ );
+ }
- const xScaleRounded = Math.round(xScale * 100) / 100;
- const yScaleRounded = Math.round(yScale * 100) / 100;
+ const xScaleRounded = Math.round(renderXScale * 100) / 100;
+ const yScaleRounded = Math.round(renderYScale * 100) / 100;
- if (!shape.isBitmap
+ if (cacheMatrix && shape.cacheKey
+ && shape.cacheParams[0] === xScaleRounded
+ && shape.cacheParams[1] === yScaleRounded
+ ) {
+ // cacheAsBitmap: スケール未変更のためキャッシュキーを維持
+ } else if (!shape.isBitmap
&& !shape.cacheKey
|| shape.cacheParams[0] !== xScaleRounded
|| shape.cacheParams[1] !== yScaleRounded
@@ -188,15 +235,17 @@ export const execute = (
// rennder on
renderQueue.pushShapeBuffer(
1, $RENDERER_SHAPE_TYPE,
- tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], tMatrix[4], tMatrix[5],
+ tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX,
+ tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY,
+ tMatrix[4], tMatrix[5],
tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7],
xMin, yMin, xMax, yMax,
graphics.xMin, graphics.yMin,
graphics.xMax, graphics.yMax,
- +isGridEnabled, +isDrawable, +shape.isBitmap,
+ +isGridEnabled, +isDrawable, shape.isBitmap ? 1 : cacheMatrix ? 2 : 0,
+shape.uniqueKey, cacheKey,
- xScale, yScale,
+ renderXScale, renderYScale,
shape.instanceId // フィルターキャッシュ用のユニークキー
);
diff --git a/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts b/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts
index ea8d0320..c7072439 100644
--- a/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts
+++ b/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts
@@ -3,6 +3,10 @@ import type { Shape } from "../../Shape";
import { Matrix } from "@next2d/geom";
import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase";
import { execute as graphicsHitTestService } from "../../Graphics/service/GraphicsHitTestService";
+import {
+ $getFloat32Array6,
+ $poolFloat32Array6
+} from "../../DisplayObjectUtil";
/**
* @description Shape のヒット判定
@@ -31,11 +35,35 @@ export const execute = (
return false;
}
- const rawMatrix = displayObjectGetRawMatrixUseCase(shape);
+ let rawMatrix = displayObjectGetRawMatrixUseCase(shape);
+
+ // cacheAsBitmap倍率をrawMatrixに適用
+ const cacheMatrix = shape.cacheAsBitmap;
+ let scaledMatrix: Float32Array | null = null;
+ if (cacheMatrix) {
+ const m = cacheMatrix.rawData;
+ const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+ const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+ if (rawMatrix) {
+ scaledMatrix = $getFloat32Array6(
+ rawMatrix[0] * csx, rawMatrix[1] * csx,
+ rawMatrix[2] * csy, rawMatrix[3] * csy,
+ rawMatrix[4], rawMatrix[5]
+ );
+ } else {
+ scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0);
+ }
+ rawMatrix = scaledMatrix;
+ }
+
const tMatrix = rawMatrix
? Matrix.multiply(matrix, rawMatrix)
: matrix;
+ if (scaledMatrix) {
+ $poolFloat32Array6(scaledMatrix);
+ }
+
hit_context.beginPath();
hit_context.setTransform(
tMatrix[0], tMatrix[1], tMatrix[2],
diff --git a/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts b/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts
index 26e4efa9..3d2e1188 100644
--- a/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts
+++ b/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts
@@ -3,7 +3,11 @@ import { Matrix } from "@next2d/geom";
import { execute as displayObjectCalcBoundsMatrixService } from "../../DisplayObject/service/DisplayObjectCalcBoundsMatrixService";
import { execute as textFieldGetRawBoundsService } from "../service/TextFieldGetRawBoundsService";
import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase";
-import { $poolBoundsArray } from "../../DisplayObjectUtil";
+import {
+ $poolBoundsArray,
+ $getFloat32Array6,
+ $poolFloat32Array6
+} from "../../DisplayObjectUtil";
/**
* @description TextFieldの描画範囲を計算します。
@@ -19,7 +23,27 @@ export const execute = (text_field: TextField, matrix: Float32Array | null = nul
{
const rawBounds = textFieldGetRawBoundsService(text_field);
- const rawMatrix = displayObjectGetRawMatrixUseCase(text_field);
+ let rawMatrix = displayObjectGetRawMatrixUseCase(text_field);
+
+ // cacheAsBitmap倍率をrawMatrixに適用
+ const cacheMatrix = (text_field as any).cacheAsBitmap;
+ let scaledMatrix: Float32Array | null = null;
+ if (cacheMatrix) {
+ const m = cacheMatrix.rawData;
+ const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+ const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+ if (rawMatrix) {
+ scaledMatrix = $getFloat32Array6(
+ rawMatrix[0] * csx, rawMatrix[1] * csx,
+ rawMatrix[2] * csy, rawMatrix[3] * csy,
+ rawMatrix[4], rawMatrix[5]
+ );
+ } else {
+ scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0);
+ }
+ rawMatrix = scaledMatrix;
+ }
+
if (!rawMatrix) {
if (matrix) {
const calcBounds = displayObjectCalcBoundsMatrixService(
@@ -41,6 +65,9 @@ export const execute = (text_field: TextField, matrix: Float32Array | null = nul
matrix ? Matrix.multiply(matrix, rawMatrix) : rawMatrix
);
+ if (scaledMatrix) {
+ $poolFloat32Array6(scaledMatrix);
+ }
$poolBoundsArray(rawBounds);
return calcBounds;
diff --git a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts
index 31bacd25..3196a30c 100644
--- a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts
+++ b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts
@@ -6,13 +6,16 @@ import { execute as displayObjectBlendToNumberService } from "../../DisplayObjec
import { execute as displayObjectGenerateHashService } from "../../DisplayObject/service/DisplayObjectGenerateHashService";
import { $cacheStore } from "@next2d/cache";
import { renderQueue } from "@next2d/render-queue";
+import { stage } from "../../Stage";
import {
$clamp,
$RENDERER_TEXT_TYPE,
$getArray,
$poolArray,
$poolBoundsArray,
- $getBoundsArray
+ $getBoundsArray,
+ $getFloat32Array6,
+ $poolFloat32Array6
} from "../../DisplayObjectUtil";
import {
ColorTransform,
@@ -85,10 +88,10 @@ export const execute = (
tMatrix
);
- const xMin = bounds[0];
- const yMin = bounds[1];
- const xMax = bounds[2];
- const yMax = bounds[3];
+ let xMin = bounds[0];
+ let yMin = bounds[1];
+ let xMax = bounds[2];
+ let yMax = bounds[3];
$poolBoundsArray(bounds);
const width = Math.ceil(Math.abs(xMax - xMin));
@@ -148,20 +151,67 @@ export const execute = (
}
}
- const xScale = Math.sqrt(
- tMatrix[0] * tMatrix[0]
- + tMatrix[1] * tMatrix[1]
- );
-
- const yScale = Math.sqrt(
- tMatrix[2] * tMatrix[2]
- + tMatrix[3] * tMatrix[3]
- );
+ // cacheAsBitmap: 指定Matrix × 自身のスケール × stageのrendererScaleでキャッシュ品質を決定
+ // 1.0基準: Matrix(1,0,0,1)はdisplayObjectの等倍スケールを意味する
+ const cacheMatrix = (text_field as any).cacheAsBitmap;
+ let renderXScale: number;
+ let renderYScale: number;
+ let cacheScaleX = 1;
+ let cacheScaleY = 1;
+ if (cacheMatrix) {
+ const m = cacheMatrix.rawData;
+ cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+ cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+
+ const ownScaleX = rawMatrix
+ ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1])
+ : 1;
+ const ownScaleY = rawMatrix
+ ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3])
+ : 1;
+
+ renderXScale = cacheScaleX * ownScaleX * stage.rendererScale;
+ renderYScale = cacheScaleY * ownScaleY * stage.rendererScale;
+
+ // cacheMatrix倍率をスクリーン座標のboundsにも反映
+ if (cacheScaleX !== 1 || cacheScaleY !== 1) {
+ const modMatrix = $getFloat32Array6(
+ tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX,
+ tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY,
+ tMatrix[4], tMatrix[5]
+ );
+ const modBounds = displayObjectCalcBoundsMatrixService(
+ text_field.xMin, text_field.yMin,
+ text_field.xMax, text_field.yMax,
+ modMatrix
+ );
+ xMin = modBounds[0];
+ yMin = modBounds[1];
+ xMax = modBounds[2];
+ yMax = modBounds[3];
+ $poolBoundsArray(modBounds);
+ $poolFloat32Array6(modMatrix);
+ }
+ } else {
+ renderXScale = Math.sqrt(
+ tMatrix[0] * tMatrix[0]
+ + tMatrix[1] * tMatrix[1]
+ );
+ renderYScale = Math.sqrt(
+ tMatrix[2] * tMatrix[2]
+ + tMatrix[3] * tMatrix[3]
+ );
+ }
- const xScaleRounded = Math.round(xScale * 100) / 100;
- const yScaleRounded = Math.round(yScale * 100) / 100;
+ const xScaleRounded = Math.round(renderXScale * 100) / 100;
+ const yScaleRounded = Math.round(renderYScale * 100) / 100;
- if (text_field.changed
+ if (cacheMatrix && text_field.cacheKey
+ && text_field.cacheParams[0] === xScaleRounded
+ && text_field.cacheParams[1] === yScaleRounded
+ ) {
+ // cacheAsBitmap: スケール未変更のためキャッシュキーを維持
+ } else if (text_field.changed
&& !text_field.cacheKey
|| text_field.cacheParams[0] !== xScaleRounded
|| text_field.cacheParams[1] !== yScaleRounded
@@ -178,14 +228,16 @@ export const execute = (
// rennder on
renderQueue.pushTextFieldBuffer(
1, $RENDERER_TEXT_TYPE,
- tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], tMatrix[4], tMatrix[5],
+ tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX,
+ tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY,
+ tMatrix[4], tMatrix[5],
tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3],
tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7],
xMin, yMin, xMax, yMax,
text_field.xMin, text_field.yMin,
text_field.xMax, text_field.yMax,
- +text_field.uniqueKey, cacheKey, +text_field.changed,
- xScale, yScale,
+ +text_field.uniqueKey, cacheKey, +text_field.changed | (cacheMatrix ? 2 : 0),
+ renderXScale, renderYScale,
text_field.instanceId // フィルターキャッシュ用のユニークキー
);
diff --git a/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts b/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts
index 986be8ea..c6f51ab8 100644
--- a/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts
+++ b/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts
@@ -2,6 +2,10 @@ import type { IPlayerHitObject } from "../../interface/IPlayerHitObject";
import type { TextField } from "@next2d/text";
import { Matrix } from "@next2d/geom";
import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase";
+import {
+ $getFloat32Array6,
+ $poolFloat32Array6
+} from "../../DisplayObjectUtil";
/**
* @description TextField のヒット判定
@@ -28,11 +32,35 @@ export const execute = (
return false;
}
- const rawMatrix = displayObjectGetRawMatrixUseCase(text_field);
+ let rawMatrix = displayObjectGetRawMatrixUseCase(text_field);
+
+ // cacheAsBitmap倍率をrawMatrixに適用
+ const cacheMatrix = (text_field as any).cacheAsBitmap;
+ let scaledMatrix: Float32Array | null = null;
+ if (cacheMatrix) {
+ const m = cacheMatrix.rawData;
+ const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
+ const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
+ if (rawMatrix) {
+ scaledMatrix = $getFloat32Array6(
+ rawMatrix[0] * csx, rawMatrix[1] * csx,
+ rawMatrix[2] * csy, rawMatrix[3] * csy,
+ rawMatrix[4], rawMatrix[5]
+ );
+ } else {
+ scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0);
+ }
+ rawMatrix = scaledMatrix;
+ }
+
const tMatrix = rawMatrix
? Matrix.multiply(matrix, rawMatrix)
: matrix;
+ if (scaledMatrix) {
+ $poolFloat32Array6(scaledMatrix);
+ }
+
hit_context.setTransform(
tMatrix[0], tMatrix[1], tMatrix[2],
tMatrix[3], tMatrix[4], tMatrix[5]
diff --git a/packages/media/src/Sound/service/SoundDecodeService.test.ts b/packages/media/src/Sound/service/SoundDecodeService.test.ts
new file mode 100644
index 00000000..251ad72d
--- /dev/null
+++ b/packages/media/src/Sound/service/SoundDecodeService.test.ts
@@ -0,0 +1,26 @@
+import { execute } from "./SoundDecodeService";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("../../MediaUtil", () => ({
+ "$getAudioContext": vi.fn(() => ({
+ "decodeAudioData": vi.fn(() => Promise.resolve({ "length": 100 }))
+ }))
+}));
+
+describe("SoundDecodeService.js test", () => {
+
+ it("execute test case1 - empty buffer returns undefined", async () =>
+ {
+ const emptyBuffer = new ArrayBuffer(0);
+ const result = await execute(emptyBuffer);
+ expect(result).toBeUndefined();
+ });
+
+ it("execute test case2 - successful decode", async () =>
+ {
+ const buffer = new ArrayBuffer(10);
+ new Uint8Array(buffer).set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
+ const result = await execute(buffer);
+ expect(result).toEqual({ "length": 100 });
+ });
+});
diff --git a/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts b/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts
new file mode 100644
index 00000000..a472cded
--- /dev/null
+++ b/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts
@@ -0,0 +1,77 @@
+import { execute } from "./VideoBuildFromCharacterUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+describe("VideoBuildFromCharacterUseCase.js test", () => {
+
+ it("execute test case1 - build video from character with buffer", () =>
+ {
+ const mockVideo = {
+ "loop": false,
+ "autoPlay": false,
+ "videoWidth": 0,
+ "videoHeight": 0,
+ "volume": 0,
+ "src": ""
+ } as any;
+
+ const character = {
+ "loop": true,
+ "autoPlay": true,
+ "bounds": { "xMax": 320, "yMax": 240 },
+ "volume": 0.8,
+ "buffer": [0, 1, 2, 3],
+ "videoData": null
+ } as any;
+
+ // Mock URL.createObjectURL
+ const originalCreateObjectURL = URL.createObjectURL;
+ URL.createObjectURL = vi.fn(() => "blob:mock-url");
+
+ execute(mockVideo, character);
+
+ expect(mockVideo.loop).toBe(true);
+ expect(mockVideo.autoPlay).toBe(true);
+ expect(mockVideo.videoWidth).toBe(320);
+ expect(mockVideo.videoHeight).toBe(240);
+ expect(mockVideo.volume).toBe(0.8);
+ expect(mockVideo.src).toBe("blob:mock-url");
+ expect(character.videoData).toBeInstanceOf(Uint8Array);
+ expect(character.buffer).toBeNull();
+
+ URL.createObjectURL = originalCreateObjectURL;
+ });
+
+ it("execute test case2 - reuse existing videoData", () =>
+ {
+ const mockVideo = {
+ "loop": false,
+ "autoPlay": false,
+ "videoWidth": 0,
+ "videoHeight": 0,
+ "volume": 0,
+ "src": ""
+ } as any;
+
+ const existingVideoData = new Uint8Array([10, 20, 30]);
+ const character = {
+ "loop": false,
+ "autoPlay": false,
+ "bounds": { "xMax": 640, "yMax": 480 },
+ "volume": 1.0,
+ "buffer": [5, 6, 7],
+ "videoData": existingVideoData
+ } as any;
+
+ const originalCreateObjectURL = URL.createObjectURL;
+ URL.createObjectURL = vi.fn(() => "blob:mock-url-2");
+
+ execute(mockVideo, character);
+
+ expect(character.videoData).toBe(existingVideoData);
+ expect(character.buffer).not.toBeNull();
+ expect(mockVideo.videoWidth).toBe(640);
+ expect(mockVideo.videoHeight).toBe(480);
+
+ URL.createObjectURL = originalCreateObjectURL;
+ });
+});
diff --git a/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts b/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts
new file mode 100644
index 00000000..877b6efd
--- /dev/null
+++ b/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts
@@ -0,0 +1,70 @@
+import { execute } from "./CommandRemoveCacheService";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("@next2d/cache", () => {
+ const store = new Map>();
+ return {
+ "$cacheStore": {
+ "has": vi.fn((key: string) => store.has(key)),
+ "getById": vi.fn((key: string) => store.get(key)),
+ "removeById": vi.fn((key: string) => store.delete(key)),
+ "_store": store
+ }
+ };
+});
+
+vi.mock("../../RendererUtil", () => ({
+ "$context": {
+ "removeNode": vi.fn()
+ }
+}));
+
+describe("CommandRemoveCacheService.js test", () => {
+
+ it("execute test case1 - remove existing cache entries", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+ const { $context } = await import("../../RendererUtil");
+
+ const node1 = { "id": 1 };
+ const node2 = { "id": 2 };
+ const cache = new Map();
+ cache.set("a", node1);
+ cache.set("b", node2);
+
+ ($cacheStore as any)._store.set("1", cache);
+
+ const keys = new Float32Array([1]);
+ execute(keys);
+
+ expect($cacheStore.has).toHaveBeenCalledWith("1");
+ expect($cacheStore.getById).toHaveBeenCalledWith("1");
+ expect($context.removeNode).toHaveBeenCalledTimes(2);
+ expect($cacheStore.removeById).toHaveBeenCalledWith("1");
+ });
+
+ it("execute test case2 - skip non-existing cache keys", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+
+ vi.mocked($cacheStore.has).mockReturnValue(false as any);
+
+ const keys = new Float32Array([999]);
+ execute(keys);
+
+ expect($cacheStore.has).toHaveBeenCalledWith("999");
+ expect($cacheStore.getById).not.toHaveBeenCalledWith("999");
+ });
+
+ it("execute test case3 - empty keys array", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+
+ vi.mocked($cacheStore.has).mockClear();
+
+ const keys = new Float32Array([]);
+ execute(keys);
+
+ expect($cacheStore.has).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts
new file mode 100644
index 00000000..ca8f7aa8
--- /dev/null
+++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts
@@ -0,0 +1,77 @@
+import { execute } from "./DisplayObjectContainerClipRenderUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("../../Shape/usecase/ShapeClipRenderUseCase", () => ({
+ "execute": vi.fn((render_queue: Float32Array, index: number) => {
+ // Skip matrix(6) + isGridEnabled(1) + length(1) + commands(n)
+ index += 6; // matrix
+ const isGrid = Boolean(render_queue[index++]);
+ if (isGrid) { index += 24; }
+ const len = render_queue[index++];
+ index += len;
+ return index;
+ })
+}));
+
+describe("DisplayObjectContainerClipRenderUseCase.js test", () => {
+
+ it("execute test case1 - empty container", () =>
+ {
+ const renderQueue = new Float32Array([0]); // length = 0
+ const result = execute(renderQueue, 0);
+ expect(result).toBe(1);
+ });
+
+ it("execute test case2 - shape clip child", async () =>
+ {
+ const shapeClipMod = await import("../../Shape/usecase/ShapeClipRenderUseCase");
+ vi.mocked(shapeClipMod.execute).mockClear();
+
+ // length(1) + type(1) + shape data
+ const data: number[] = [];
+ data.push(1); // 1 child
+ data.push(0x01); // type = shape
+ // matrix (6)
+ data.push(1, 0, 0, 1, 0, 0);
+ // isGridEnabled
+ data.push(0);
+ // command length
+ data.push(2);
+ // commands
+ data.push(9, 12);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0);
+
+ expect(shapeClipMod.execute).toHaveBeenCalledTimes(1);
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case3 - nested container clip", () =>
+ {
+ // outer: length=1, type=container(0x00)
+ // inner: length=0
+ const data: number[] = [];
+ data.push(1); // 1 child
+ data.push(0x00); // type = container
+ data.push(0); // inner length = 0
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0);
+
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case4 - unknown type is skipped", () =>
+ {
+ const data: number[] = [];
+ data.push(1); // 1 child
+ data.push(0x02); // type = text (not handled in clip)
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0);
+
+ // Should consume length + type but nothing else
+ expect(result).toBe(2);
+ });
+});
diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts
new file mode 100644
index 00000000..5b75a1e9
--- /dev/null
+++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts
@@ -0,0 +1,265 @@
+import { execute } from "./DisplayObjectContainerRenderUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("../../RendererUtil", () => ({
+ "$context": {
+ "drawArraysInstanced": vi.fn(),
+ "save": vi.fn(),
+ "restore": vi.fn(),
+ "beginMask": vi.fn(),
+ "endMask": vi.fn(),
+ "leaveMask": vi.fn(),
+ "setMaskBounds": vi.fn(),
+ "containerBeginLayer": vi.fn(),
+ "containerEndLayer": vi.fn(),
+ "containerBeginAtlasNode": vi.fn(() => ({ "x": 0, "y": 0, "w": 100, "h": 80, "index": 0 })),
+ "containerEndAtlasNode": vi.fn(),
+ "containerDrawCachedFilter": vi.fn(),
+ "removeNode": vi.fn(),
+ "setTransform": vi.fn(),
+ "drawDisplayObject": vi.fn(),
+ "globalAlpha": 1,
+ "imageSmoothingEnabled": true,
+ "globalCompositeOperation": "normal"
+ }
+}));
+
+vi.mock("@next2d/cache", () => ({
+ "$cacheStore": {
+ "get": vi.fn(() => null),
+ "set": vi.fn()
+ }
+}));
+
+vi.mock("../../Shape/usecase/ShapeRenderUseCase", () => ({
+ "execute": vi.fn((_rq: Float32Array, index: number) => index)
+}));
+
+vi.mock("../../Shape/usecase/ShapeClipRenderUseCase", () => ({
+ "execute": vi.fn((_rq: Float32Array, index: number) => index)
+}));
+
+vi.mock("../../TextField/usecase/TextFieldRenderUseCase", () => ({
+ "execute": vi.fn((_rq: Float32Array, index: number) => index)
+}));
+
+vi.mock("../../Video/usecase/VideoRenderUseCase", () => ({
+ "execute": vi.fn((_rq: Float32Array, index: number) => index)
+}));
+
+vi.mock("./DisplayObjectContainerClipRenderUseCase", () => ({
+ "execute": vi.fn((_rq: Float32Array, index: number) => index)
+}));
+
+vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({
+ "execute": vi.fn(() => "normal")
+}));
+
+describe("DisplayObjectContainerRenderUseCase.js test", () => {
+
+ it("execute test case1 - simple container no layer, no mask, no children", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.containerBeginLayer).mockClear();
+ vi.mocked($context.containerEndLayer).mockClear();
+
+ const data: number[] = [];
+ // blendMode
+ data.push(0);
+ // useLayer = false
+ data.push(0);
+ // useMaskDisplayObject = false
+ data.push(0);
+ // children length = 0
+ data.push(0);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, null);
+
+ expect($context.containerBeginLayer).not.toHaveBeenCalled();
+ expect($context.containerEndLayer).not.toHaveBeenCalled();
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case2 - container with useLayer and blend only", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.containerBeginLayer).mockClear();
+ vi.mocked($context.containerEndLayer).mockClear();
+
+ const data: number[] = [];
+ // blendMode
+ data.push(0);
+ // useLayer = true
+ data.push(1);
+ // layerWidth, layerHeight
+ data.push(800, 600);
+ // useFilter = false
+ data.push(0);
+ // matrix (6)
+ data.push(1, 0, 0, 1, 0, 0);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // useMaskDisplayObject = false
+ data.push(0);
+ // children length = 0
+ data.push(0);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, null);
+
+ expect($context.containerBeginLayer).toHaveBeenCalledWith(800, 600);
+ expect($context.containerEndLayer).toHaveBeenCalledTimes(1);
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case3 - container with filter cache hit returns early", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.containerDrawCachedFilter).mockClear();
+
+ const data: number[] = [];
+ // blendMode
+ data.push(0);
+ // useLayer = true
+ data.push(1);
+ // layerWidth, layerHeight
+ data.push(800, 600);
+ // useFilter = true
+ data.push(1);
+ // filterCache = true
+ data.push(1);
+ // uniqueKey
+ data.push(42);
+ // filterKey
+ data.push(99);
+ // filterBounds (4)
+ data.push(-10, -10, 110, 110);
+ // matrix (6)
+ data.push(1, 0, 0, 1, 0, 0);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, null);
+
+ expect($context.containerDrawCachedFilter).toHaveBeenCalledTimes(1);
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case4 - cacheAsBitmap cache hit returns early", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ const { $cacheStore } = await import("@next2d/cache");
+ vi.mocked($context.containerBeginLayer).mockClear();
+ vi.mocked($context.containerEndLayer).mockClear();
+ vi.mocked($context.drawDisplayObject).mockClear();
+ vi.mocked($context.setTransform).mockClear();
+
+ // キャッシュされたノードを返すようにモック
+ const mockNode = { "x": 0, "y": 0, "w": 100, "h": 80, "index": 0 };
+ vi.mocked($cacheStore.get).mockReturnValueOnce(mockNode as any);
+
+ const data: number[] = [];
+ // blendMode
+ data.push(0);
+ // useLayer = true
+ data.push(1);
+ // layerWidth, layerHeight
+ data.push(0, 0);
+ // layerType = 2 (cacheAsBitmap)
+ data.push(2);
+ // cacheHit = true
+ data.push(1);
+ // uniqueKey (instanceId)
+ data.push(10);
+ // cacheKey
+ data.push(500);
+ // filterBounds (4)
+ data.push(0, 0, 100, 80);
+ // renderScaleX, renderScaleY
+ data.push(1, 1);
+ // parent matrix a, b, c, d, screenX, screenY
+ data.push(1, 0, 0, 1, 30, 40);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, null);
+
+ // cache hit → drawDisplayObject でアトラスから描画して即return
+ expect($context.drawDisplayObject).toHaveBeenCalledTimes(1);
+ // matrix/renderScale = 1/1 → setTransform(1, 0, 0, 1, 30, 40)
+ expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 30, 40);
+ // containerBeginLayer/EndLayerは呼ばれない(子要素の描画不要)
+ expect($context.containerBeginLayer).not.toHaveBeenCalled();
+ expect($context.containerEndLayer).not.toHaveBeenCalled();
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case5 - cacheAsBitmap cache miss processes children and caches", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ const { $cacheStore } = await import("@next2d/cache");
+ vi.mocked($context.containerBeginLayer).mockClear();
+ vi.mocked($context.containerEndLayer).mockClear();
+ vi.mocked($context.containerBeginAtlasNode).mockClear();
+ vi.mocked($context.containerEndAtlasNode).mockClear();
+ vi.mocked($context.drawDisplayObject).mockClear();
+ vi.mocked($context.setTransform).mockClear();
+ vi.mocked($context.removeNode).mockClear();
+ vi.mocked($cacheStore.get).mockReturnValue(null as any);
+ vi.mocked($cacheStore.set).mockClear();
+
+ const mockNode = { "x": 0, "y": 0, "w": 100, "h": 80, "index": 0 };
+ vi.mocked($context.containerBeginAtlasNode).mockReturnValueOnce(mockNode as any);
+
+ const data: number[] = [];
+ // blendMode
+ data.push(0);
+ // useLayer = true
+ data.push(1);
+ // layerWidth, layerHeight
+ data.push(100, 80);
+ // layerType = 2 (cacheAsBitmap)
+ data.push(2);
+ // cacheHit = false
+ data.push(0);
+ // uniqueKey (instanceId)
+ data.push(10);
+ // cacheKey
+ data.push(500);
+ // filterBounds (4)
+ data.push(0, 0, 100, 80);
+ // renderScaleX, renderScaleY
+ data.push(1, 1);
+ // parent matrix a, b, c, d, screenX, screenY
+ data.push(1, 0, 0, 1, 30, 40);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // paramsLength = 0 (no filter params)
+ data.push(0);
+ // useMaskDisplayObject = false
+ data.push(0);
+ // children length = 0
+ data.push(0);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, null);
+
+ // cache miss → containerBeginAtlasNode → 子要素描画 → containerEndAtlasNode
+ expect($context.containerBeginAtlasNode).toHaveBeenCalledWith(100, 80);
+ expect($context.containerEndAtlasNode).toHaveBeenCalledTimes(1);
+ // containerBeginLayer/containerEndLayer は呼ばれない(直接アトラス描画)
+ expect($context.containerBeginLayer).not.toHaveBeenCalled();
+ expect($context.containerEndLayer).not.toHaveBeenCalled();
+ // ノードをキャッシュに保存
+ expect($cacheStore.set).toHaveBeenCalledWith("10", "bNode", mockNode);
+ expect($cacheStore.set).toHaveBeenCalledWith("10", "bKey", "500");
+ // アトラスからインスタンス描画
+ expect($context.drawDisplayObject).toHaveBeenCalledTimes(1);
+ // matrix/renderScale = 1/1 → setTransform(1, 0, 0, 1, 30, 40)
+ expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 30, 40);
+ expect(result).toBe(data.length);
+ });
+});
diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts
index 04e29959..fa08b0c9 100644
--- a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts
+++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts
@@ -1,4 +1,6 @@
import { $context } from "../../RendererUtil";
+import { $cacheStore } from "@next2d/cache";
+import type { Node } from "@next2d/texture-packer";
import { execute as shapeRenderUseCase } from "../../Shape/usecase/ShapeRenderUseCase";
import { execute as shapeClipRenderUseCase } from "../../Shape/usecase/ShapeClipRenderUseCase";
import { execute as textFieldRenderUseCase } from "../../TextField/usecase/TextFieldRenderUseCase";
@@ -35,8 +37,11 @@ export const execute = (
let layerHeight = 0;
let useFilter = false;
+ let useCacheAsBitmap = false;
let uniqueKey = "";
let filterKey = "";
+ let cacheRenderScaleX = 1;
+ let cacheRenderScaleY = 1;
let filterBounds: Float32Array | null = null;
let filterParams: Float32Array | null = null;
let matrix: Float32Array | null = null;
@@ -46,9 +51,57 @@ export const execute = (
layerWidth = render_queue[index++];
layerHeight = render_queue[index++];
- useFilter = Boolean(render_queue[index++]);
+ const layerType = render_queue[index++]; // 0=blend, 1=filter, 2=cacheAsBitmap
+ useFilter = layerType === 1;
+ useCacheAsBitmap = layerType === 2;
- if (useFilter) {
+ if (useCacheAsBitmap) {
+
+ // cacheAsBitmapパス
+ const cacheHit = Boolean(render_queue[index++]);
+ uniqueKey = `${render_queue[index++]}`;
+ filterKey = `${render_queue[index++]}`;
+
+ // フィルター境界を読み取り
+ filterBounds = render_queue.subarray(index, index + 4);
+ index += 4;
+
+ // renderScale(キャッシュ描画時のスケール)
+ cacheRenderScaleX = render_queue[index++];
+ cacheRenderScaleY = render_queue[index++];
+
+ // 親matrix + スクリーン座標 (a, b, c, d, screenX, screenY)
+ matrix = render_queue.subarray(index, index + 6);
+ index += 6;
+
+ colorTransform = render_queue.subarray(index, index + 8);
+ index += 8;
+
+ if (cacheHit) {
+ // キャッシュ済み: Shapeと同様にmatrix/renderScaleで描画
+ const cachedNode = $cacheStore.get(uniqueKey, "bNode") as Node;
+ if (cachedNode) {
+ $context.globalAlpha = Math.min(Math.max(0, colorTransform[3] + colorTransform[7] / 255), 1);
+ $context.globalCompositeOperation = blendMode;
+ $context.setTransform(
+ matrix[0] / cacheRenderScaleX, matrix[1] / cacheRenderScaleX,
+ matrix[2] / cacheRenderScaleY, matrix[3] / cacheRenderScaleY,
+ matrix[4], matrix[5]
+ );
+ $context.drawDisplayObject(
+ cachedNode,
+ filterBounds[0], filterBounds[1],
+ filterBounds[2], filterBounds[3],
+ colorTransform
+ );
+ }
+ return index;
+ }
+
+ // 初回描画: フィルターパラメータをスキップ(アトラスパスでは不使用)
+ index += render_queue[index] + 1;
+
+ } else if (useFilter) {
// フィルターパス: filterCache/uniqueKey/filterKey を読む
const filterCache = Boolean(render_queue[index++]);
uniqueKey = `${render_queue[index++]}`;
@@ -95,7 +148,11 @@ export const execute = (
}
// コンテナのフィルター/ブレンド用にレイヤーを開始
- if (useLayer) {
+ let cacheNode: Node | null = null;
+ if (useCacheAsBitmap) {
+ // cacheAsBitmap: temp FBO作成→子要素描画→アトラスノードへコピー
+ cacheNode = $context.containerBeginAtlasNode(layerWidth, layerHeight);
+ } else if (useLayer) {
$context.containerBeginLayer(layerWidth, layerHeight);
}
@@ -244,7 +301,33 @@ export const execute = (
}
// コンテナのフィルター/ブレンド結果をメインに合成
- if (useLayer) {
+ if (cacheNode) {
+ // cacheAsBitmap: アトラス描画終了→キャッシュ→インスタンス描画
+ $context.containerEndAtlasNode();
+
+ // 古いノードを解放
+ const oldNode = $cacheStore.get(uniqueKey, "bNode") as Node;
+ if (oldNode) {
+ $context.removeNode(oldNode);
+ }
+ $cacheStore.set(uniqueKey, "bNode", cacheNode);
+ $cacheStore.set(uniqueKey, "bKey", filterKey);
+
+ // アトラスからインスタンス描画(Shapeと同様にmatrix/renderScaleで描画)
+ $context.globalAlpha = Math.min(Math.max(0, colorTransform![3] + colorTransform![7] / 255), 1);
+ $context.globalCompositeOperation = blendMode;
+ $context.setTransform(
+ matrix![0] / cacheRenderScaleX, matrix![1] / cacheRenderScaleX,
+ matrix![2] / cacheRenderScaleY, matrix![3] / cacheRenderScaleY,
+ matrix![4], matrix![5]
+ );
+ $context.drawDisplayObject(
+ cacheNode,
+ filterBounds![0], filterBounds![1],
+ filterBounds![2], filterBounds![3],
+ colorTransform!
+ );
+ } else if (useLayer) {
$context.containerEndLayer(
blendMode, matrix!, colorTransform,
useFilter, filterBounds, filterParams,
diff --git a/packages/renderer/src/Shape/service/ShapeCommandService.test.ts b/packages/renderer/src/Shape/service/ShapeCommandService.test.ts
new file mode 100644
index 00000000..4cab1305
--- /dev/null
+++ b/packages/renderer/src/Shape/service/ShapeCommandService.test.ts
@@ -0,0 +1,192 @@
+import { execute } from "./ShapeCommandService";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("../../RendererUtil", () => ({
+ "$context": {
+ "beginPath": vi.fn(),
+ "moveTo": vi.fn(),
+ "lineTo": vi.fn(),
+ "quadraticCurveTo": vi.fn(),
+ "bezierCurveTo": vi.fn(),
+ "arc": vi.fn(),
+ "closePath": vi.fn(),
+ "fillStyle": vi.fn(),
+ "strokeStyle": vi.fn(),
+ "fill": vi.fn(),
+ "stroke": vi.fn(),
+ "gradientFill": vi.fn(),
+ "gradientStroke": vi.fn(),
+ "bitmapFill": vi.fn(),
+ "bitmapStroke": vi.fn(),
+ "thickness": 0,
+ "caps": 0,
+ "joints": 0,
+ "miterLimit": 0
+ },
+ "$getArray": vi.fn(() => [])
+}));
+
+describe("ShapeCommandService.js test", () => {
+
+ it("execute test case1 - BEGIN_PATH command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.beginPath).mockClear();
+
+ const commands = new Float32Array([9]); // BEGIN_PATH = 9
+ execute(commands);
+
+ expect($context.beginPath).toHaveBeenCalledTimes(1);
+ });
+
+ it("execute test case2 - MOVE_TO command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.moveTo).mockClear();
+
+ const commands = new Float32Array([0, 10, 20]); // MOVE_TO = 0
+ execute(commands);
+
+ expect($context.moveTo).toHaveBeenCalledWith(10, 20);
+ });
+
+ it("execute test case3 - LINE_TO command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.lineTo).mockClear();
+
+ const commands = new Float32Array([2, 30, 40]); // LINE_TO = 2
+ execute(commands);
+
+ expect($context.lineTo).toHaveBeenCalledWith(30, 40);
+ });
+
+ it("execute test case4 - CURVE_TO command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.quadraticCurveTo).mockClear();
+
+ const commands = new Float32Array([1, 10, 20, 30, 40]); // CURVE_TO = 1
+ execute(commands);
+
+ expect($context.quadraticCurveTo).toHaveBeenCalledWith(10, 20, 30, 40);
+ });
+
+ it("execute test case5 - CUBIC command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.bezierCurveTo).mockClear();
+
+ const commands = new Float32Array([3, 1, 2, 3, 4, 5, 6]); // CUBIC = 3
+ execute(commands);
+
+ expect($context.bezierCurveTo).toHaveBeenCalledWith(1, 2, 3, 4, 5, 6);
+ });
+
+ it("execute test case6 - ARC command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.arc).mockClear();
+
+ const commands = new Float32Array([4, 50, 60, 25]); // ARC = 4
+ execute(commands);
+
+ expect($context.arc).toHaveBeenCalledWith(50, 60, 25);
+ });
+
+ it("execute test case7 - FILL_STYLE and END_FILL commands", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.fillStyle).mockClear();
+ vi.mocked($context.fill).mockClear();
+
+ // FILL_STYLE(5) r g b a, END_FILL(7)
+ const commands = new Float32Array([5, 255, 128, 0, 255, 7]);
+ execute(commands);
+
+ expect($context.fillStyle).toHaveBeenCalledWith(1, 128 / 255, 0, 1);
+ expect($context.fill).toHaveBeenCalledTimes(1);
+ });
+
+ it("execute test case8 - FILL_STYLE skipped in clip mode", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.fillStyle).mockClear();
+
+ const commands = new Float32Array([5, 255, 128, 0, 255]);
+ execute(commands, true);
+
+ expect($context.fillStyle).not.toHaveBeenCalled();
+ });
+
+ it("execute test case9 - STROKE_STYLE command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.strokeStyle).mockClear();
+
+ // STROKE_STYLE(6) thickness caps joints miterLimit r g b a
+ const commands = new Float32Array([6, 2, 1, 0, 10, 255, 0, 0, 255]);
+ execute(commands);
+
+ expect($context.thickness).toBe(2);
+ expect($context.caps).toBe(1);
+ expect($context.joints).toBe(0);
+ expect($context.miterLimit).toBe(10);
+ expect($context.strokeStyle).toHaveBeenCalledWith(1, 0, 0, 1);
+ });
+
+ it("execute test case10 - STROKE_STYLE skipped in clip mode", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.strokeStyle).mockClear();
+
+ const commands = new Float32Array([6, 2, 1, 0, 10, 255, 0, 0, 255]);
+ execute(commands, true);
+
+ expect($context.strokeStyle).not.toHaveBeenCalled();
+ });
+
+ it("execute test case11 - END_STROKE command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.stroke).mockClear();
+
+ const commands = new Float32Array([8]); // END_STROKE = 8
+ execute(commands);
+
+ expect($context.stroke).toHaveBeenCalledTimes(1);
+ });
+
+ it("execute test case12 - END_STROKE skipped in clip mode", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.stroke).mockClear();
+
+ const commands = new Float32Array([8]); // END_STROKE = 8
+ execute(commands, true);
+
+ expect($context.stroke).not.toHaveBeenCalled();
+ });
+
+ it("execute test case13 - CLOSE_PATH command", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.closePath).mockClear();
+
+ const commands = new Float32Array([12]); // CLOSE_PATH = 12
+ execute(commands);
+
+ expect($context.closePath).toHaveBeenCalledTimes(1);
+ });
+
+ it("execute test case14 - empty commands", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.beginPath).mockClear();
+
+ const commands = new Float32Array([]);
+ execute(commands);
+
+ expect($context.beginPath).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts b/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts
new file mode 100644
index 00000000..9025a36c
--- /dev/null
+++ b/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts
@@ -0,0 +1,86 @@
+import { execute } from "./ShapeClipRenderUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("../../RendererUtil", () => ({
+ "$context": {
+ "reset": vi.fn(),
+ "setTransform": vi.fn(),
+ "useGrid": vi.fn(),
+ "clip": vi.fn()
+ }
+}));
+
+vi.mock("../service/ShapeCommandService", () => ({
+ "execute": vi.fn()
+}));
+
+describe("ShapeClipRenderUseCase.js test", () => {
+
+ it("execute test case1 - basic clip without grid", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ const shapeCommandService = await import("../service/ShapeCommandService");
+
+ vi.mocked($context.reset).mockClear();
+ vi.mocked($context.setTransform).mockClear();
+ vi.mocked($context.useGrid).mockClear();
+ vi.mocked($context.clip).mockClear();
+ vi.mocked(shapeCommandService.execute).mockClear();
+
+ // matrix(6) + isGridEnabled(1) + length(1) + commands(3)
+ const renderQueue = new Float32Array([
+ 1, 0, 0, 1, 10, 20, // matrix
+ 0, // isGridEnabled = false
+ 3, // command length
+ 9, 0, 5 // commands (BEGIN_PATH, MOVE_TO partial)
+ ]);
+
+ const resultIndex = execute(renderQueue, 0);
+
+ expect($context.reset).toHaveBeenCalledTimes(1);
+ expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 10, 20);
+ expect($context.useGrid).toHaveBeenCalledWith(null);
+ expect(shapeCommandService.execute).toHaveBeenCalledTimes(1);
+ expect($context.clip).toHaveBeenCalledTimes(1);
+ expect(resultIndex).toBe(11);
+ });
+
+ it("execute test case2 - clip with grid enabled", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ const shapeCommandService = await import("../service/ShapeCommandService");
+
+ vi.mocked($context.reset).mockClear();
+ vi.mocked($context.setTransform).mockClear();
+ vi.mocked($context.useGrid).mockClear();
+ vi.mocked($context.clip).mockClear();
+ vi.mocked(shapeCommandService.execute).mockClear();
+
+ // matrix(6) + isGridEnabled(1) + gridData(24) + length(1) + commands(2)
+ const data = new Float32Array(6 + 1 + 24 + 1 + 2);
+ let idx = 0;
+ // matrix
+ data[idx++] = 2; data[idx++] = 0; data[idx++] = 0;
+ data[idx++] = 2; data[idx++] = 5; data[idx++] = 10;
+ // isGridEnabled = true
+ data[idx++] = 1;
+ // grid data (24 values)
+ for (let i = 0; i < 24; i++) {
+ data[idx++] = i;
+ }
+ // command length
+ data[idx++] = 2;
+ // commands
+ data[idx++] = 9; data[idx++] = 12;
+
+ const resultIndex = execute(data, 0);
+
+ expect($context.setTransform).toHaveBeenCalledWith(2, 0, 0, 2, 5, 10);
+ expect($context.useGrid).toHaveBeenCalledTimes(1);
+ const gridArg = vi.mocked($context.useGrid).mock.calls[0][0];
+ expect(gridArg).not.toBeNull();
+ expect(gridArg!.length).toBe(28);
+ expect($context.clip).toHaveBeenCalledTimes(1);
+ expect(resultIndex).toBe(idx);
+ });
+});
diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts
new file mode 100644
index 00000000..d9e8e232
--- /dev/null
+++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts
@@ -0,0 +1,127 @@
+import { execute } from "./ShapeRenderUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+const mockNode = { "x": 0, "y": 0, "w": 100, "h": 100 };
+
+vi.mock("@next2d/cache", () => ({
+ "$cacheStore": {
+ "get": vi.fn(() => mockNode),
+ "set": vi.fn(),
+ "has": vi.fn(() => false)
+ }
+}));
+
+vi.mock("../../RendererUtil", () => ({
+ "$context": {
+ "reset": vi.fn(),
+ "setTransform": vi.fn(),
+ "useGrid": vi.fn(),
+ "createNode": vi.fn(() => mockNode),
+ "beginNodeRendering": vi.fn(),
+ "endNodeRendering": vi.fn(),
+ "drawFill": vi.fn(),
+ "drawPixels": vi.fn(),
+ "drawDisplayObject": vi.fn(),
+ "drawArraysInstanced": vi.fn(),
+ "bind": vi.fn(),
+ "applyFilter": vi.fn(),
+ "currentAttachmentObject": null,
+ "atlasAttachmentObject": null,
+ "globalAlpha": 1,
+ "imageSmoothingEnabled": true,
+ "globalCompositeOperation": "normal"
+ }
+}));
+
+vi.mock("../service/ShapeCommandService", () => ({
+ "execute": vi.fn()
+}));
+
+vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({
+ "execute": vi.fn(() => "normal")
+}));
+
+describe("ShapeRenderUseCase.js test", () => {
+
+ it("execute test case1 - render with cache hit", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.drawDisplayObject).mockClear();
+ vi.mocked($context.setTransform).mockClear();
+
+ // Build render queue for cached shape
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 50, 60);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 100, 100);
+ // baseBounds xMin, yMin, xMax, yMax
+ data.push(0, 0, 100, 100);
+ // isGridEnabled, isDrawable, isBitmap
+ data.push(0, 1, 0);
+ // uniqueKey, cacheKey
+ data.push(1, 0);
+ // xScale, yScale
+ data.push(1, 1);
+ // filterKey
+ data.push(1);
+ // hasCache = 1 (cached)
+ data.push(1);
+ // blendMode
+ data.push(0);
+ // useFilter = 0
+ data.push(0);
+
+ const renderQueue = new Float32Array(data);
+ const resultIndex = execute(renderQueue, 0);
+
+ expect($context.drawDisplayObject).toHaveBeenCalledTimes(1);
+ expect(resultIndex).toBe(data.length);
+ });
+
+ it("execute test case2 - render with filter", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.applyFilter).mockClear();
+
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 50, 60);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 100, 100);
+ // baseBounds
+ data.push(0, 0, 100, 100);
+ // isGridEnabled, isDrawable, isBitmap
+ data.push(0, 1, 0);
+ // uniqueKey, cacheKey
+ data.push(2, 0);
+ // xScale, yScale
+ data.push(1, 1);
+ // filterKey
+ data.push(2);
+ // hasCache = 1
+ data.push(1);
+ // blendMode
+ data.push(0);
+ // useFilter = 1
+ data.push(1);
+ // updated
+ data.push(1);
+ // filterBounds (4)
+ data.push(-5, -5, 110, 110);
+ // filter params length
+ data.push(3);
+ // filter params
+ data.push(1, 2, 1);
+
+ const renderQueue = new Float32Array(data);
+ const resultIndex = execute(renderQueue, 0);
+
+ expect($context.applyFilter).toHaveBeenCalledTimes(1);
+ expect(resultIndex).toBe(data.length);
+ });
+});
diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts
index 923fd353..26a00d74 100644
--- a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts
+++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts
@@ -33,7 +33,9 @@ export const execute = (render_queue: Float32Array, index: number): number =>
const isGridEnabled = Boolean(render_queue[index++]);
const isDrawable = Boolean(render_queue[index++]);
- const isBitmap = Boolean(render_queue[index++]);
+ const renderMode = render_queue[index++]; // 0=vector, 1=bitmap, 2=cacheAsBitmap
+ const isBitmap = renderMode === 1;
+ const isCacheAsBitmap = renderMode === 2;
// cache uniqueKey
const uniqueKey = `${render_queue[index++]}`;
@@ -196,6 +198,24 @@ export const execute = (render_queue: Float32Array, index: number): number =>
matrix[4], matrix[5]
);
+ $context.drawDisplayObject(
+ node,
+ bounds[0], bounds[1], bounds[2], bounds[3],
+ colorTransform
+ );
+ } else if (isCacheAsBitmap) {
+
+ // cacheAsBitmap: Bitmapと同様の描画パスで、cacheScaleを補正
+ // baseBounds原点(xMin,yMin)のスクリーン座標をtranslationに反映
+ const screenX = matrix[0] * xMin + matrix[2] * yMin + matrix[4];
+ const screenY = matrix[1] * xMin + matrix[3] * yMin + matrix[5];
+
+ $context.setTransform(
+ matrix[0] / xScale, matrix[1] / xScale,
+ matrix[2] / yScale, matrix[3] / yScale,
+ screenX, screenY
+ );
+
$context.drawDisplayObject(
node,
bounds[0], bounds[1], bounds[2], bounds[3],
diff --git a/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts b/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts
new file mode 100644
index 00000000..44bdb89f
--- /dev/null
+++ b/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts
@@ -0,0 +1,205 @@
+import { execute } from "./TextFiledGetAlignOffsetService";
+import { describe, expect, it } from "vitest";
+
+describe("TextFiledGetAlignOffsetService.js test", () => {
+
+ it("execute test case1 - left align returns leftMargin", () =>
+ {
+ const textData = {
+ "widthTable": [100]
+ } as any;
+
+ const textObject = {
+ "line": 0,
+ "textFormat": {
+ "align": "left",
+ "leftMargin": 5,
+ "rightMargin": 0
+ }
+ } as any;
+
+ const textSetting = {
+ "wordWrap": true,
+ "rawWidth": 200,
+ "autoSize": "left"
+ } as any;
+
+ const result = execute(textData, textObject, textSetting);
+ expect(result).toBe(5);
+ });
+
+ it("execute test case2 - center align", () =>
+ {
+ const textData = {
+ "widthTable": [100]
+ } as any;
+
+ const textObject = {
+ "line": 0,
+ "textFormat": {
+ "align": "center",
+ "leftMargin": 0,
+ "rightMargin": 0
+ }
+ } as any;
+
+ const textSetting = {
+ "wordWrap": true,
+ "rawWidth": 200,
+ "autoSize": "none"
+ } as any;
+
+ const result = execute(textData, textObject, textSetting);
+ expect(result).toBe(Math.max(0, 200 / 2 - 0 - 0 - 100 / 2 - 2));
+ });
+
+ it("execute test case3 - right align", () =>
+ {
+ const textData = {
+ "widthTable": [100]
+ } as any;
+
+ const textObject = {
+ "line": 0,
+ "textFormat": {
+ "align": "right",
+ "leftMargin": 0,
+ "rightMargin": 0
+ }
+ } as any;
+
+ const textSetting = {
+ "wordWrap": true,
+ "rawWidth": 200,
+ "autoSize": "none"
+ } as any;
+
+ const result = execute(textData, textObject, textSetting);
+ expect(result).toBe(Math.max(0, 200 - 0 - 100 - 0 - 4));
+ });
+
+ it("execute test case4 - autoSize center", () =>
+ {
+ const textData = {
+ "widthTable": [80]
+ } as any;
+
+ const textObject = {
+ "line": 0,
+ "textFormat": {
+ "align": "left",
+ "leftMargin": 0,
+ "rightMargin": 0
+ }
+ } as any;
+
+ const textSetting = {
+ "wordWrap": true,
+ "rawWidth": 200,
+ "autoSize": "center"
+ } as any;
+
+ const result = execute(textData, textObject, textSetting);
+ expect(result).toBe(Math.max(0, 200 / 2 - 0 - 0 - 80 / 2 - 2));
+ });
+
+ it("execute test case5 - autoSize right", () =>
+ {
+ const textData = {
+ "widthTable": [80]
+ } as any;
+
+ const textObject = {
+ "line": 0,
+ "textFormat": {
+ "align": "left",
+ "leftMargin": 0,
+ "rightMargin": 0
+ }
+ } as any;
+
+ const textSetting = {
+ "wordWrap": true,
+ "rawWidth": 200,
+ "autoSize": "right"
+ } as any;
+
+ const result = execute(textData, textObject, textSetting);
+ expect(result).toBe(Math.max(0, 200 - 0 - 80 - 0 - 4));
+ });
+
+ it("execute test case6 - lineWidth exceeds rawWidth without wordWrap", () =>
+ {
+ const textData = {
+ "widthTable": [300]
+ } as any;
+
+ const textObject = {
+ "line": 0,
+ "textFormat": {
+ "align": "center",
+ "leftMargin": 10,
+ "rightMargin": 5
+ }
+ } as any;
+
+ const textSetting = {
+ "wordWrap": false,
+ "rawWidth": 200,
+ "autoSize": "none"
+ } as any;
+
+ const result = execute(textData, textObject, textSetting);
+ expect(result).toBe(10);
+ });
+
+ it("execute test case7 - missing line in widthTable defaults to 0", () =>
+ {
+ const textData = {
+ "widthTable": [100]
+ } as any;
+
+ const textObject = {
+ "line": 5,
+ "textFormat": {
+ "align": "left",
+ "leftMargin": 0,
+ "rightMargin": 0
+ }
+ } as any;
+
+ const textSetting = {
+ "wordWrap": true,
+ "rawWidth": 200,
+ "autoSize": "none"
+ } as any;
+
+ const result = execute(textData, textObject, textSetting);
+ expect(result).toBe(0);
+ });
+
+ it("execute test case8 - margins with center align", () =>
+ {
+ const textData = {
+ "widthTable": [100]
+ } as any;
+
+ const textObject = {
+ "line": 0,
+ "textFormat": {
+ "align": "center",
+ "leftMargin": 10,
+ "rightMargin": 10
+ }
+ } as any;
+
+ const textSetting = {
+ "wordWrap": true,
+ "rawWidth": 300,
+ "autoSize": "none"
+ } as any;
+
+ const result = execute(textData, textObject, textSetting);
+ expect(result).toBe(Math.max(0, 300 / 2 - 10 - 10 - 100 / 2 - 2));
+ });
+});
diff --git a/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts b/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts
new file mode 100644
index 00000000..7f6e7c04
--- /dev/null
+++ b/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts
@@ -0,0 +1,199 @@
+import { execute } from "./TextFieldDrawOffscreenCanvasUseCase";
+import { describe, expect, it, vi, beforeAll } from "vitest";
+
+vi.mock("../../RendererUtil", () => ({
+ "$intToRGBA": vi.fn((color: number) => ({
+ "R": (color >> 16) & 0xFF,
+ "G": (color >> 8) & 0xFF,
+ "B": color & 0xFF,
+ "A": 1
+ }))
+}));
+
+vi.mock("../service/TextFiledGetAlignOffsetService", () => ({
+ "execute": vi.fn(() => 0)
+}));
+
+vi.mock("../service/TextFieldGenerateFontStyleService", () => ({
+ "execute": vi.fn(() => "12px Arial")
+}));
+
+// OffscreenCanvas mock with full 2D context
+beforeAll(() => {
+ const originalOffscreenCanvas = globalThis.OffscreenCanvas;
+ (globalThis as any).OffscreenCanvas = class MockOffscreenCanvas {
+ width: number;
+ height: number;
+ constructor(w: number, h: number) {
+ this.width = w;
+ this.height = h;
+ }
+ getContext() {
+ return {
+ "fillRect": vi.fn(),
+ "beginPath": vi.fn(),
+ "moveTo": vi.fn(),
+ "lineTo": vi.fn(),
+ "quadraticCurveTo": vi.fn(),
+ "closePath": vi.fn(),
+ "isPointInPath": vi.fn(() => false),
+ "fill": vi.fn(),
+ "stroke": vi.fn(),
+ "save": vi.fn(),
+ "restore": vi.fn(),
+ "clip": vi.fn(),
+ "setTransform": vi.fn(),
+ "fillText": vi.fn(),
+ "strokeText": vi.fn(),
+ "rect": vi.fn(),
+ "font": "",
+ "fillStyle": "",
+ "strokeStyle": "",
+ "lineWidth": 1
+ };
+ }
+ };
+
+ return () => {
+ (globalThis as any).OffscreenCanvas = originalOffscreenCanvas;
+ };
+});
+
+describe("TextFieldDrawOffscreenCanvasUseCase.js test", () => {
+
+ it("execute test case1 - returns canvas with null text_data", () =>
+ {
+ const textSetting = {
+ "width": 200,
+ "height": 100,
+ "background": false,
+ "border": false,
+ "backgroundColor": 0xFFFFFF,
+ "borderColor": 0x000000,
+ "autoSize": "none",
+ "stopIndex": -1,
+ "scrollX": 0,
+ "scrollY": 0,
+ "textWidth": 200,
+ "textHeight": 100,
+ "rawWidth": 200,
+ "rawHeight": 100,
+ "focusIndex": -1,
+ "selectIndex": -1,
+ "focusVisible": false,
+ "thickness": 0,
+ "thicknessColor": 0,
+ "wordWrap": false,
+ "defaultColor": 0,
+ "defaultSize": 12
+ } as any;
+
+ const canvas = execute(null, textSetting, 1, 1);
+ expect(canvas).toBeInstanceOf(OffscreenCanvas);
+ expect(canvas.width).toBe(200);
+ expect(canvas.height).toBe(100);
+ });
+
+ it("execute test case2 - draws background and border", () =>
+ {
+ const textSetting = {
+ "width": 300,
+ "height": 150,
+ "background": true,
+ "border": true,
+ "backgroundColor": 0xFF0000,
+ "borderColor": 0x00FF00,
+ "autoSize": "none",
+ "stopIndex": -1,
+ "scrollX": 0,
+ "scrollY": 0,
+ "textWidth": 300,
+ "textHeight": 150,
+ "rawWidth": 300,
+ "rawHeight": 150,
+ "focusIndex": -1,
+ "selectIndex": -1,
+ "focusVisible": false,
+ "thickness": 0,
+ "thicknessColor": 0,
+ "wordWrap": false,
+ "defaultColor": 0,
+ "defaultSize": 12
+ } as any;
+
+ const canvas = execute(null, textSetting, 1, 1);
+ expect(canvas).toBeInstanceOf(OffscreenCanvas);
+ });
+
+ it("execute test case3 - renders text objects", () =>
+ {
+ const textData = {
+ "textTable": [
+ {
+ "mode": "wrap",
+ "line": 0,
+ "w": 0,
+ "h": 14,
+ "y": 0,
+ "text": "",
+ "textFormat": {
+ "align": "left",
+ "leftMargin": 0,
+ "rightMargin": 0,
+ "color": 0x000000,
+ "underline": false
+ }
+ },
+ {
+ "mode": "text",
+ "line": 0,
+ "w": 50,
+ "h": 14,
+ "y": 12,
+ "text": "Hello",
+ "textFormat": {
+ "align": "left",
+ "leftMargin": 0,
+ "rightMargin": 0,
+ "color": 0x000000,
+ "underline": false
+ }
+ }
+ ],
+ "lineTable": [],
+ "widthTable": [50],
+ "heightTable": [16],
+ "ascentTable": [12]
+ } as any;
+
+ const textSetting = {
+ "width": 200,
+ "height": 100,
+ "background": false,
+ "border": false,
+ "backgroundColor": 0xFFFFFF,
+ "borderColor": 0x000000,
+ "autoSize": "none",
+ "stopIndex": -1,
+ "scrollX": 0,
+ "scrollY": 0,
+ "textWidth": 200,
+ "textHeight": 100,
+ "rawWidth": 200,
+ "rawHeight": 100,
+ "focusIndex": -1,
+ "selectIndex": -1,
+ "focusVisible": false,
+ "thickness": 0,
+ "thicknessColor": 0,
+ "wordWrap": false,
+ "defaultColor": 0,
+ "defaultSize": 12
+ } as any;
+
+ const canvas = execute(textData, textSetting, 1, 1);
+ expect(canvas).toBeInstanceOf(OffscreenCanvas);
+ expect(canvas.width).toBe(200);
+ expect(canvas.height).toBe(100);
+ });
+});
diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts
new file mode 100644
index 00000000..4ff21fb6
--- /dev/null
+++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts
@@ -0,0 +1,154 @@
+import { execute } from "./TextFieldRenderUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+const mockNode = { "x": 0, "y": 0, "w": 100, "h": 50 };
+
+vi.mock("@next2d/cache", () => ({
+ "$cacheStore": {
+ "get": vi.fn(() => mockNode),
+ "set": vi.fn(),
+ "has": vi.fn(() => false)
+ }
+}));
+
+vi.mock("../../RendererUtil", () => ({
+ "$context": {
+ "reset": vi.fn(),
+ "setTransform": vi.fn(),
+ "createNode": vi.fn(() => mockNode),
+ "beginNodeRendering": vi.fn(),
+ "endNodeRendering": vi.fn(),
+ "drawElement": vi.fn(),
+ "drawDisplayObject": vi.fn(),
+ "bind": vi.fn(),
+ "applyFilter": vi.fn(),
+ "currentAttachmentObject": null,
+ "atlasAttachmentObject": null,
+ "globalAlpha": 1,
+ "imageSmoothingEnabled": true,
+ "globalCompositeOperation": "normal"
+ }
+}));
+
+vi.mock("./TextFieldDrawOffscreenCanvasUseCase", () => ({
+ "execute": vi.fn(() => new OffscreenCanvas(100, 50))
+}));
+
+vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({
+ "execute": vi.fn(() => "normal")
+}));
+
+describe("TextFieldRenderUseCase.js test", () => {
+
+ it("execute test case1 - render with cache hit", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.drawDisplayObject).mockClear();
+
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 10, 20);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 100, 50);
+ // baseBounds (4)
+ data.push(0, 0, 100, 50);
+ // uniqueKey, cacheKey
+ data.push(1, 0);
+ // changed
+ data.push(0);
+ // xScale, yScale
+ data.push(1, 1);
+ // filterKey
+ data.push(1);
+ // hasCache = 1
+ data.push(1);
+ // blendMode
+ data.push(0);
+ // useFilter = 0
+ data.push(0);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0);
+
+ expect($context.drawDisplayObject).toHaveBeenCalledTimes(1);
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case2 - render with filter", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.applyFilter).mockClear();
+
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 10, 20);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 100, 50);
+ // baseBounds (4)
+ data.push(0, 0, 100, 50);
+ // uniqueKey, cacheKey
+ data.push(5, 0);
+ // changed
+ data.push(1);
+ // xScale, yScale
+ data.push(1, 1);
+ // filterKey
+ data.push(5);
+ // hasCache = 1
+ data.push(1);
+ // blendMode
+ data.push(0);
+ // useFilter = 1
+ data.push(1);
+ // updated
+ data.push(1);
+ // filterBounds (4)
+ data.push(-5, -5, 110, 60);
+ // filter params length
+ data.push(3);
+ // filter params
+ data.push(1, 2, 1);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0);
+
+ expect($context.applyFilter).toHaveBeenCalledTimes(1);
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case3 - cache miss returns early when no node", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+ vi.mocked($cacheStore.get).mockReturnValueOnce(null as any);
+
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 10, 20);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 100, 50);
+ // baseBounds (4)
+ data.push(0, 0, 100, 50);
+ // uniqueKey, cacheKey
+ data.push(99, 0);
+ // changed
+ data.push(0);
+ // xScale, yScale
+ data.push(1, 1);
+ // filterKey
+ data.push(99);
+ // hasCache = 1
+ data.push(1);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0);
+
+ // Should return early at hasCache position + 1
+ expect(result).toBe(data.length);
+ });
+});
diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts
index 52cf396f..540be8f6 100644
--- a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts
+++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts
@@ -43,8 +43,10 @@ export const execute = (render_queue: Float32Array, index: number): number =>
const uniqueKey = `${render_queue[index++]}`;
const cacheKey = render_queue[index++];
- // text state
- const changed = Boolean(render_queue[index++]);
+ // text state (bit 0 = changed, bit 1 = cacheAsBitmap)
+ const changedFlag = render_queue[index++];
+ const changed = Boolean(changedFlag & 1);
+ const isCacheAsBitmap = Boolean(changedFlag & 2);
const xScale = render_queue[index++];
const yScale = render_queue[index++];
@@ -187,29 +189,46 @@ export const execute = (render_queue: Float32Array, index: number): number =>
$context.imageSmoothingEnabled = true;
$context.globalCompositeOperation = displayObjectGetBlendModeService(blendMode);
- const radianX = Math.atan2(matrix[1], matrix[0]);
- const radianY = Math.atan2(-matrix[2], matrix[3]);
- if (radianX || radianY) {
+ if (isCacheAsBitmap) {
- const tx = xMin * xScale;
- const ty = yMin * yScale;
-
- const cosX = Math.cos(radianX);
- const sinX = Math.sin(radianX);
- const cosY = Math.cos(radianY);
- const sinY = Math.sin(radianY);
+ // cacheAsBitmap: Bitmapと同様の描画パスで、cacheScaleを補正
+ // baseBounds原点(xMin,yMin)のスクリーン座標をtranslationに反映
+ const screenX = matrix[0] * xMin + matrix[2] * yMin + matrix[4];
+ const screenY = matrix[1] * xMin + matrix[3] * yMin + matrix[5];
$context.setTransform(
- cosX, sinX, -sinY, cosY,
- tx * cosX - ty * sinY + matrix[4],
- tx * sinX + ty * cosY + matrix[5]
+ matrix[0] / xScale, matrix[1] / xScale,
+ matrix[2] / yScale, matrix[3] / yScale,
+ screenX, screenY
);
} else {
- $context.setTransform(1, 0, 0, 1,
- bounds[0], bounds[1]
- );
+ const radianX = Math.atan2(matrix[1], matrix[0]);
+ const radianY = Math.atan2(-matrix[2], matrix[3]);
+ if (radianX || radianY) {
+
+ const tx = xMin * xScale;
+ const ty = yMin * yScale;
+
+ const cosX = Math.cos(radianX);
+ const sinX = Math.sin(radianX);
+ const cosY = Math.cos(radianY);
+ const sinY = Math.sin(radianY);
+
+ $context.setTransform(
+ cosX, sinX, -sinY, cosY,
+ tx * cosX - ty * sinY + matrix[4],
+ tx * sinX + ty * cosY + matrix[5]
+ );
+
+ } else {
+
+ $context.setTransform(1, 0, 0, 1,
+ bounds[0], bounds[1]
+ );
+
+ }
}
diff --git a/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts b/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts
new file mode 100644
index 00000000..c90b2ad8
--- /dev/null
+++ b/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts
@@ -0,0 +1,187 @@
+import { execute } from "./VideoRenderUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+const mockNode = { "x": 0, "y": 0, "w": 320, "h": 240 };
+
+vi.mock("@next2d/cache", () => ({
+ "$cacheStore": {
+ "get": vi.fn(() => mockNode),
+ "set": vi.fn(),
+ "has": vi.fn(() => false)
+ }
+}));
+
+vi.mock("../../RendererUtil", () => ({
+ "$context": {
+ "reset": vi.fn(),
+ "setTransform": vi.fn(),
+ "createNode": vi.fn(() => mockNode),
+ "beginNodeRendering": vi.fn(),
+ "endNodeRendering": vi.fn(),
+ "drawElement": vi.fn(),
+ "drawDisplayObject": vi.fn(),
+ "bind": vi.fn(),
+ "applyFilter": vi.fn(),
+ "currentAttachmentObject": null,
+ "atlasAttachmentObject": null,
+ "globalAlpha": 1,
+ "imageSmoothingEnabled": true,
+ "globalCompositeOperation": "normal"
+ }
+}));
+
+vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({
+ "execute": vi.fn(() => "normal")
+}));
+
+describe("VideoRenderUseCase.js test", () => {
+
+ it("execute test case1 - render with cache hit", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.drawDisplayObject).mockClear();
+ vi.mocked($context.setTransform).mockClear();
+
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 50, 60);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 320, 240);
+ // baseBounds (4)
+ data.push(0, 0, 320, 240);
+ // uniqueKey
+ data.push(1);
+ // changed
+ data.push(0);
+ // filterKey
+ data.push(1);
+ // hasCache = 1
+ data.push(1);
+ // blendMode
+ data.push(0);
+ // useFilter = 0
+ data.push(0);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, null);
+
+ expect($context.drawDisplayObject).toHaveBeenCalledTimes(1);
+ expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 50, 60);
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case2 - render with filter", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.applyFilter).mockClear();
+
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 50, 60);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 320, 240);
+ // baseBounds (4)
+ data.push(0, 0, 320, 240);
+ // uniqueKey
+ data.push(3);
+ // changed
+ data.push(1);
+ // filterKey
+ data.push(3);
+ // hasCache = 1
+ data.push(1);
+ // blendMode
+ data.push(0);
+ // useFilter = 1
+ data.push(1);
+ // updated
+ data.push(1);
+ // filterBounds (4)
+ data.push(-5, -5, 330, 250);
+ // filter params length
+ data.push(3);
+ // filter params
+ data.push(1, 2, 1);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, null);
+
+ expect($context.applyFilter).toHaveBeenCalledTimes(1);
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case3 - no cache, with image bitmap", async () =>
+ {
+ const { $context } = await import("../../RendererUtil");
+ vi.mocked($context.drawElement).mockClear();
+ vi.mocked($context.beginNodeRendering).mockClear();
+ vi.mocked($context.endNodeRendering).mockClear();
+
+ const mockBitmap = { "width": 320, "height": 240 } as unknown as ImageBitmap;
+
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 50, 60);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 320, 240);
+ // baseBounds (4)
+ data.push(0, 0, 320, 240);
+ // uniqueKey
+ data.push(10);
+ // changed
+ data.push(1);
+ // filterKey
+ data.push(10);
+ // hasCache = 0
+ data.push(0);
+ // hasNode = 0
+ data.push(0);
+ // blendMode
+ data.push(0);
+ // useFilter = 0
+ data.push(0);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, [mockBitmap]);
+
+ expect($context.beginNodeRendering).toHaveBeenCalledTimes(1);
+ expect($context.drawElement).toHaveBeenCalledWith(mockNode, mockBitmap, true);
+ expect($context.endNodeRendering).toHaveBeenCalledTimes(1);
+ expect(result).toBe(data.length);
+ });
+
+ it("execute test case4 - cache miss returns early when no node found", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+ vi.mocked($cacheStore.get).mockReturnValueOnce(null as any);
+
+ const data: number[] = [];
+ // matrix (6)
+ data.push(1, 0, 0, 1, 0, 0);
+ // colorTransform (8)
+ data.push(1, 1, 1, 1, 0, 0, 0, 0);
+ // bounds (4)
+ data.push(0, 0, 320, 240);
+ // baseBounds (4)
+ data.push(0, 0, 320, 240);
+ // uniqueKey
+ data.push(99);
+ // changed
+ data.push(0);
+ // filterKey
+ data.push(99);
+ // hasCache = 1
+ data.push(1);
+
+ const renderQueue = new Float32Array(data);
+ const result = execute(renderQueue, 0, null);
+
+ expect(result).toBe(data.length);
+ });
+});
diff --git a/packages/text/package.json b/packages/text/package.json
index 7a7e11fb..dee4c87c 100644
--- a/packages/text/package.json
+++ b/packages/text/package.json
@@ -23,9 +23,6 @@
"type": "git",
"url": "git+https://github.com/Next2D/Player.git"
},
- "dependencies": {
- "htmlparser2": "^10.0.0"
- },
"peerDependencies": {
"@next2d/display": "file:../display",
"@next2d/geom": "file:../geom",
diff --git a/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts b/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts
new file mode 100644
index 00000000..dc32b56a
--- /dev/null
+++ b/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts
@@ -0,0 +1,160 @@
+import type { IHtmlElementNode, IHtmlTextNode } from "../../interface/IHtmlNode";
+import { execute } from "./TextParserHtmlParserService";
+import { describe, expect, it } from "vitest";
+
+describe("TextParserHtmlParserService.ts test", () =>
+{
+ it("empty string returns empty array", () =>
+ {
+ const result = execute("");
+ expect(result.length).toBe(0);
+ });
+
+ it("plain text without tags", () =>
+ {
+ const result = execute("hello world");
+ expect(result.length).toBe(1);
+ expect(result[0].type).toBe("text");
+ expect((result[0] as IHtmlTextNode).value).toBe("hello world");
+ });
+
+ it("single tag with text", () =>
+ {
+ const result = execute("bold");
+ expect(result.length).toBe(1);
+
+ const el = result[0] as IHtmlElementNode;
+ expect(el.type).toBe("element");
+ expect(el.tagName).toBe("B");
+ expect(el.children.length).toBe(1);
+ expect((el.children[0] as IHtmlTextNode).value).toBe("bold");
+ });
+
+ it("nested tags", () =>
+ {
+ const result = execute("text
");
+ expect(result.length).toBe(1);
+
+ const p = result[0] as IHtmlElementNode;
+ expect(p.tagName).toBe("P");
+ expect(p.children.length).toBe(1);
+
+ const b = p.children[0] as IHtmlElementNode;
+ expect(b.tagName).toBe("B");
+ expect(b.children.length).toBe(1);
+ expect((b.children[0] as IHtmlTextNode).value).toBe("text");
+ });
+
+ it("tag with attributes (double quotes)", () =>
+ {
+ const result = execute('text');
+ expect(result.length).toBe(1);
+
+ const font = result[0] as IHtmlElementNode;
+ expect(font.tagName).toBe("FONT");
+ expect(font.attributes.length).toBe(3);
+ expect(font.attributes[0]).toEqual({ name: "face", value: "Arial" });
+ expect(font.attributes[1]).toEqual({ name: "size", value: "12" });
+ expect(font.attributes[2]).toEqual({ name: "color", value: "#FF0000" });
+ });
+
+ it("tag with attributes (single quotes)", () =>
+ {
+ const result = execute("text");
+ const font = result[0] as IHtmlElementNode;
+ expect(font.attributes[0]).toEqual({ name: "face", value: "Arial" });
+ });
+
+ it("style attribute", () =>
+ {
+ const result = execute('text');
+ const span = result[0] as IHtmlElementNode;
+ expect(span.tagName).toBe("SPAN");
+ expect(span.attributes.length).toBe(1);
+ expect(span.attributes[0].name).toBe("style");
+ expect(span.attributes[0].value).toBe("font-size: 14px; color: red;");
+ });
+
+ it("self-closing br tag", () =>
+ {
+ const result = execute("a
b");
+ expect(result.length).toBe(3);
+ expect((result[0] as IHtmlTextNode).value).toBe("a");
+ expect((result[1] as IHtmlElementNode).tagName).toBe("BR");
+ expect((result[1] as IHtmlElementNode).children.length).toBe(0);
+ expect((result[2] as IHtmlTextNode).value).toBe("b");
+ });
+
+ it("self-closing br/ tag", () =>
+ {
+ const result = execute("a
b");
+ expect(result.length).toBe(3);
+ expect((result[1] as IHtmlElementNode).tagName).toBe("BR");
+ expect((result[1] as IHtmlElementNode).children.length).toBe(0);
+ });
+
+ it("tag name is case-insensitive", () =>
+ {
+ const result = execute("text");
+ const el = result[0] as IHtmlElementNode;
+ expect(el.tagName).toBe("FONT");
+ });
+
+ it("complex HTML matching existing test input", () =>
+ {
+ const htmlText = `
+あ
+い
+う
+えお順
+
`;
+ const cleaned = htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, "");
+ const result = execute(cleaned);
+
+ expect(result.length).toBe(1);
+
+ const p = result[0] as IHtmlElementNode;
+ expect(p.tagName).toBe("P");
+
+ // children: あ い う
えお順
+ const children = p.children;
+
+ //
+ const u = children.find((n) => n.type === "element" && n.tagName === "U") as IHtmlElementNode;
+ expect(u).toBeDefined();
+ expect((u.children[0] as IHtmlTextNode).value).toBe("あ");
+
+ //
+ const b = children.find((n) => n.type === "element" && n.tagName === "B") as IHtmlElementNode;
+ expect(b).toBeDefined();
+ expect((b.children[0] as IHtmlTextNode).value).toBe("い");
+
+ //
+ const i = children.find((n) => n.type === "element" && n.tagName === "I") as IHtmlElementNode;
+ expect(i).toBeDefined();
+ expect((i.children[0] as IHtmlTextNode).value).toBe("う");
+
+ //
+ const br = children.find((n) => n.type === "element" && n.tagName === "BR") as IHtmlElementNode;
+ expect(br).toBeDefined();
+ expect(br.children.length).toBe(0);
+ });
+
+ it("div tag with align attribute", () =>
+ {
+ const result = execute('text
');
+ const div = result[0] as IHtmlElementNode;
+ expect(div.tagName).toBe("DIV");
+ expect(div.attributes[0]).toEqual({ name: "align", value: "center" });
+ expect((div.children[0] as IHtmlTextNode).value).toBe("text");
+ });
+
+ it("multiple sibling tags", () =>
+ {
+ const result = execute("abc");
+ expect(result.length).toBe(3);
+ expect((result[0] as IHtmlElementNode).tagName).toBe("U");
+ expect((result[1] as IHtmlElementNode).tagName).toBe("B");
+ expect((result[2] as IHtmlElementNode).tagName).toBe("I");
+ });
+});
diff --git a/packages/text/src/TextParser/service/TextParserHtmlParserService.ts b/packages/text/src/TextParser/service/TextParserHtmlParserService.ts
new file mode 100644
index 00000000..74334d96
--- /dev/null
+++ b/packages/text/src/TextParser/service/TextParserHtmlParserService.ts
@@ -0,0 +1,371 @@
+import type { IAttributeObject } from "../../interface/IAttributeObject";
+import type { IHtmlNode } from "../../interface/IHtmlNode";
+
+/**
+ * @description モジュールレベルのパーサ状態(クロージャ生成を回避)
+ * Module-level parser state (avoids closure allocation)
+ *
+ * @type {string}
+ * @private
+ */
+let _$html: string = "";
+
+/**
+ * @description 現在の解析位置
+ * Current parse position
+ *
+ * @type {number}
+ * @private
+ */
+let _$pos: number = 0;
+
+/**
+ * @description HTML文字列の長さ(キャッシュ)
+ * Cached length of the HTML string
+ *
+ * @type {number}
+ * @private
+ */
+let _$len: number = 0;
+
+/**
+ * @description void要素(BR等)の空children共有インスタンス
+ * 再利用によりアロケーションを回避する
+ * Shared empty children array for void elements (e.g. BR)
+ * Reused across calls to avoid allocation
+ *
+ * @type {IHtmlNode[]}
+ * @const
+ * @private
+ */
+const $EMPTY_CHILDREN: IHtmlNode[] = [];
+
+/**
+ * @description 軽量HTMLパーサ — TextField用の限定HTMLサブセットを1-passで解析
+ * 対応タグ: B, I, U, P, BR, DIV, FONT, SPAN
+ * 対応属性: align, face, size, color, style, letterSpacing,
+ * leading, leftMargin, rightMargin, underline, bold, italic
+ * Lightweight HTML parser — single-pass parse for TextField HTML subset
+ *
+ * @param {string} html - 解析対象のHTML文字列 / HTML string to parse
+ * @return {IHtmlNode[]} 解析結果のノード配列 / Array of parsed nodes
+ * @method
+ * @protected
+ */
+export const execute = (html: string): IHtmlNode[] =>
+{
+ _$html = html;
+ _$pos = 0;
+ _$len = html.length;
+
+ const result = $parseChildren("");
+
+ _$html = "";
+
+ return result;
+};
+
+/**
+ * @description 子ノードを再帰的に解析する
+ * parentTagに一致する閉じタグを検出すると、そのスコープの解析を終了して返却する
+ * Recursively parse child nodes.
+ * Returns when a closing tag matching parent_tag is found.
+ *
+ * @param {string} parent_tag - 親要素のタグ名(大文字)。ルートの場合は空文字列
+ * Parent element tag name (uppercase). Empty string for root.
+ * @return {IHtmlNode[]} 解析された子ノード配列 / Array of parsed child nodes
+ * @private
+ */
+const $parseChildren = (parent_tag: string): IHtmlNode[] =>
+{
+ const nodes: IHtmlNode[] = [];
+
+ while (_$pos < _$len) {
+
+ const ltIdx = _$html.indexOf("<", _$pos);
+
+ if (ltIdx === -1) {
+ if (_$pos < _$len) {
+ nodes[nodes.length] = {
+ "type": "text",
+ "value": _$html.substring(_$pos)
+ };
+ _$pos = _$len;
+ }
+ break;
+ }
+
+ if (ltIdx > _$pos) {
+ nodes[nodes.length] = {
+ "type": "text",
+ "value": _$html.substring(_$pos, ltIdx)
+ };
+ }
+
+ _$pos = ltIdx + 1;
+
+ if (_$pos >= _$len) {
+ break;
+ }
+
+ // closing tag: ''
+ if (_$html.charCodeAt(_$pos) === 0x2F) {
+ _$pos++;
+
+ const gtIdx = _$html.indexOf(">", _$pos);
+ if (gtIdx === -1) {
+ _$pos = _$len;
+ break;
+ }
+
+ if (parent_tag && $matchesUpperCase(_$pos, gtIdx, parent_tag)) {
+ _$pos = gtIdx + 1;
+ return nodes;
+ }
+
+ _$pos = gtIdx + 1;
+ continue;
+ }
+
+ const tagStart = _$pos;
+ while (_$pos < _$len) {
+ const c = _$html.charCodeAt(_$pos);
+ if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) {
+ break;
+ }
+ _$pos++;
+ }
+ const tagName = $toUpperCase(tagStart, _$pos);
+
+ const attributes = $parseAttributes();
+
+ // self-closing '/>'
+ let selfClosing = false;
+ if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x2F) {
+ selfClosing = true;
+ _$pos++;
+ }
+
+ if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3E) {
+ _$pos++;
+ }
+
+ if (selfClosing || tagName === "BR") {
+ nodes[nodes.length] = {
+ "type": "element",
+ "tagName": tagName,
+ "attributes": attributes,
+ "children": $EMPTY_CHILDREN
+ };
+ } else {
+ nodes[nodes.length] = {
+ "type": "element",
+ "tagName": tagName,
+ "attributes": attributes,
+ "children": $parseChildren(tagName)
+ };
+ }
+ }
+
+ return nodes;
+};
+
+/**
+ * @description 現在位置から開始タグの属性を解析して IAttributeObject[] として返す
+ * 属性形式: name="value", name='value', name=value, name (boolean)
+ * '>' または '/' に到達した時点で解析を終了する
+ * Parse attributes from current position and return as IAttributeObject[].
+ * Supports: name="value", name='value', name=value, name (boolean).
+ * Stops when '>' or '/' is encountered.
+ *
+ * @return {IAttributeObject[]} 解析された属性オブジェクトの配列 / Array of parsed attribute objects
+ * @private
+ */
+const $parseAttributes = (): IAttributeObject[] =>
+{
+ const attrs: IAttributeObject[] = [];
+
+ while (_$pos < _$len) {
+
+ while (_$pos < _$len) {
+ const c = _$html.charCodeAt(_$pos);
+ if (c !== 0x20 && c !== 0x09) {
+ break;
+ }
+ _$pos++;
+ }
+
+ if (_$pos >= _$len) {
+ break;
+ }
+
+ const ch = _$html.charCodeAt(_$pos);
+
+ // '>' or '/'
+ if (ch === 0x3E || ch === 0x2F) {
+ break;
+ }
+
+ const nameStart = _$pos;
+ while (_$pos < _$len) {
+ const c = _$html.charCodeAt(_$pos);
+ if (c === 0x3D || c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) {
+ break;
+ }
+ _$pos++;
+ }
+
+ if (_$pos === nameStart) {
+ break;
+ }
+
+ const name = _$html.substring(nameStart, _$pos);
+
+ while (_$pos < _$len) {
+ const c = _$html.charCodeAt(_$pos);
+ if (c !== 0x20 && c !== 0x09) {
+ break;
+ }
+ _$pos++;
+ }
+
+ if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3D) {
+ _$pos++;
+
+ while (_$pos < _$len) {
+ const c = _$html.charCodeAt(_$pos);
+ if (c !== 0x20 && c !== 0x09) {
+ break;
+ }
+ _$pos++;
+ }
+
+ let value: string;
+ const quote = _$html.charCodeAt(_$pos);
+
+ if (quote === 0x22 || quote === 0x27) {
+ _$pos++;
+ const closeIdx = _$html.indexOf(
+ quote === 0x22 ? "\"" : "'", _$pos
+ );
+ if (closeIdx === -1) {
+ value = _$html.substring(_$pos);
+ _$pos = _$len;
+ } else {
+ value = _$html.substring(_$pos, closeIdx);
+ _$pos = closeIdx + 1;
+ }
+ } else {
+ // unquoted value
+ const valStart = _$pos;
+ while (_$pos < _$len) {
+ const c = _$html.charCodeAt(_$pos);
+ if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) {
+ break;
+ }
+ _$pos++;
+ }
+ value = _$html.substring(valStart, _$pos);
+ }
+
+ attrs[attrs.length] = { "name": name, "value": value };
+ } else {
+ attrs[attrs.length] = { "name": name, "value": true };
+ }
+ }
+
+ return attrs;
+};
+
+/**
+ * @description 閉じタグ名を文字列生成せずにparentTag(大文字)と照合する
+ * _$html[start..end) の範囲を1文字ずつ大文字変換しながら比較する
+ * ゼロアロケーションで一致判定を行い、GC圧を回避する
+ * Compare closing tag name against parent_tag (uppercase) without string allocation.
+ * Converts each character to uppercase via charCode and compares in-place.
+ * Zero-allocation comparison to avoid GC pressure.
+ *
+ * @param {number} start - 照合開始位置('' の直後)/ Start index (right after '')
+ * @param {number} end - 照合終了位置('>' の位置)/ End index (position of '>')
+ * @param {string} tag - 比較対象の親タグ名(大文字)/ Parent tag name to match (uppercase)
+ * @return {boolean} 一致する場合true / True if the closing tag matches
+ * @private
+ */
+const $matchesUpperCase = (
+ start: number,
+ end: number,
+ tag: string
+): boolean =>
+{
+ while (start < end) {
+ const c = _$html.charCodeAt(start);
+ if (c !== 0x20 && c !== 0x09) {
+ break;
+ }
+ start++;
+ }
+
+ while (end > start) {
+ const c = _$html.charCodeAt(end - 1);
+ if (c !== 0x20 && c !== 0x09) {
+ break;
+ }
+ end--;
+ }
+
+ const tagLen = tag.length;
+ if (end - start !== tagLen) {
+ return false;
+ }
+
+ for (let i = 0; i < tagLen; i++) {
+ let c = _$html.charCodeAt(start + i);
+ if (c >= 0x61 && c <= 0x7A) {
+ c -= 0x20;
+ }
+ if (c !== tag.charCodeAt(i)) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+/**
+ * @description _$html[start..end) の部分文字列を大文字化して返す
+ * 1-2文字はString.fromCharCodeで直接生成(短いタグ名の最速パス)
+ * 3文字以上はsubstring().toUpperCase()にフォールバック
+ * Convert _$html[start..end) substring to uppercase.
+ * 1-2 char tags use String.fromCharCode (fastest path for short tag names).
+ * 3+ chars fall back to substring().toUpperCase().
+ *
+ * @param {number} start - 開始位置 / Start index
+ * @param {number} end - 終了位置 / End index (exclusive)
+ * @return {string} 大文字化されたタグ名 / Uppercased tag name
+ * @private
+ */
+const $toUpperCase = (start: number, end: number): string =>
+{
+ const length = end - start;
+
+ // 1文字タグ (B, I, U, P) — 最頻出パスを最速処理
+ if (length === 1) {
+ let c = _$html.charCodeAt(start);
+ if (c >= 0x61 && c <= 0x7A) {
+ c -= 0x20;
+ }
+ return String.fromCharCode(c);
+ }
+
+ // 2文字タグ (BR, DIV先頭判定高速化)
+ if (length === 2) {
+ let c0 = _$html.charCodeAt(start);
+ let c1 = _$html.charCodeAt(start + 1);
+ if (c0 >= 0x61 && c0 <= 0x7A) { c0 -= 0x20 }
+ if (c1 >= 0x61 && c1 <= 0x7A) { c1 -= 0x20 }
+ return String.fromCharCode(c0, c1);
+ }
+
+ // 3-4文字タグ (DIV, FONT, SPAN)
+ return _$html.substring(start, end).toUpperCase();
+};
diff --git a/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts b/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts
index cec8df3b..1906071a 100644
--- a/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts
+++ b/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts
@@ -1,7 +1,6 @@
import type { TextFormat } from "../../TextFormat";
import type { IOptions } from "../../interface/IOptions";
import { TextData } from "../../TextData";
-import { parseDocument } from "htmlparser2";
import { execute as textParserCreateNewLineUseCase } from "./TextParserCreateNewLineUseCase";
import { execute as textParserAdjustmentHeightService } from "../service/TextParserAdjustmentHeightService";
import { execute as textParserParseTagUseCase } from "./TextParserParseTagUseCase";
@@ -44,7 +43,7 @@ export const execute = (
textParserCreateNewLineUseCase(textData, textFormat);
textParserParseTagUseCase(
- parseDocument(htmlText), textFormat, textData, options
+ htmlText, textFormat, textData, options
);
textParserAdjustmentHeightService(textData);
diff --git a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts
index 760d50f2..b37afda6 100644
--- a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts
+++ b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts
@@ -1,7 +1,6 @@
import { execute } from "./TextParserParseTagUseCase";
import { TextData } from "../../TextData";
import { TextFormat } from "../../TextFormat";
-import { parseDocument } from "htmlparser2";
import { describe, expect, it } from "vitest";
describe("TextParserParseTagUseCase.js test", () =>
@@ -33,7 +32,7 @@ describe("TextParserParseTagUseCase.js test", () =>
えお順
`;
- execute(parseDocument(htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, "")), textFormat, textData, {
+ execute(htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, ""), textFormat, textData, {
"width": 200,
"multiline": true,
"wordWrap": true,
diff --git a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts
index 98f71c0a..81c863fe 100644
--- a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts
+++ b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts
@@ -1,16 +1,41 @@
import type { TextFormat } from "../../TextFormat";
import type { TextData } from "../../TextData";
import type { IOptions } from "../../interface/IOptions";
-import type { Document, Element, ChildNode } from "domhandler";
+import type { IAttributeObject } from "../../interface/IAttributeObject";
+import type { ITextFormatAlign } from "../../interface/ITextFormatAlign";
import { execute as textParserParseTextUseCase } from "./TextParserParseTextUseCase";
import { execute as textParserCreateNewLineUseCase } from "./TextParserCreateNewLineUseCase";
-import { execute as textParserSetAttributesUseCase } from "./TextParserSetAttributesUseCase";
+import { execute as textParserParseStyleService } from "../service/TextParserParseStyleService";
+import { $toColorInt } from "../../TextUtil";
/**
- * @description タグを解析してTextDataとTextFormatを設定
- * Analyze tags and set TextData and TextFormat
+ * @description 数値タグID — 文字列生成を完全排除
+ * Numeric tag IDs — eliminates all tag string allocation
+ */
+const $TAG_NONE: number = 0;
+const $TAG_B: number = 1;
+const $TAG_I: number = 2;
+const $TAG_U: number = 3;
+const $TAG_P: number = 4;
+const $TAG_BR: number = 5;
+const $TAG_DIV: number = 6;
+const $TAG_FONT: number = 7;
+const $TAG_SPAN: number = 8;
+
+/**
+ * @description モジュールレベルのパーサ状態
+ * Module-level parser state
+ */
+let _$html: string = "";
+let _$pos: number = 0;
+let _$len: number = 0;
+
+/**
+ * @description HTMLタグを直接解析してTextDataを構築
+ * 中間ツリーなし、タグ名文字列なし、clone()なし、属性オブジェクトなし
+ * Parse HTML directly — no tree, no tag strings, no clone, no attr objects
*
- * @param {Document} document
+ * @param {string} html
* @param {TextFormat} text_format
* @param {TextData} text_data
* @param {IOptions} options
@@ -19,68 +44,550 @@ import { execute as textParserSetAttributesUseCase } from "./TextParserSetAttrib
* @protected
*/
export const execute = (
- document: Document,
+ html: string,
+ text_format: TextFormat,
+ text_data: TextData,
+ options: IOptions
+): void => {
+
+ _$html = html;
+ _$pos = 0;
+ _$len = html.length;
+
+ $processChildren($TAG_NONE, text_format, text_data, options);
+
+ _$html = "";
+};
+
+/**
+ * @description 子要素をストリーミング解析・処理(再帰)
+ * save/restoreでTextFormatをスタック管理 — clone()ゼロ
+ * Stream-parse children with stack-based format — zero clone()
+ */
+const $processChildren = (
+ parent_tag_id: number,
text_format: TextFormat,
text_data: TextData,
options: IOptions
): void => {
- for (let idx = 0; idx < document.children.length; ++idx) {
+ while (_$pos < _$len) {
- const node = document.children[idx] as ChildNode;
+ const ltIdx: number = _$html.indexOf("<", _$pos);
+
+ if (ltIdx === -1) {
+ textParserParseTextUseCase(
+ _$html.substring(_$pos), text_format, text_data, options
+ );
+ _$pos = _$len;
+ break;
+ }
- if (node.nodeType === 3) {
+ if (ltIdx > _$pos) {
+ textParserParseTextUseCase(
+ _$html.substring(_$pos, ltIdx), text_format, text_data, options
+ );
+ }
- textParserParseTextUseCase(node.nodeValue || "", text_format, text_data, options);
+ _$pos = ltIdx + 1;
+ if (_$pos >= _$len) {
+ break;
+ }
+ // closing tag: ''
+ if (_$html.charCodeAt(_$pos) === 0x2F) {
+ _$pos++;
+ const gtIdx: number = _$html.indexOf(">", _$pos);
+ if (gtIdx === -1) {
+ _$pos = _$len;
+ break;
+ }
+ if (parent_tag_id !== $TAG_NONE && $identifyTag(_$pos, gtIdx) === parent_tag_id) {
+ _$pos = gtIdx + 1;
+ return;
+ }
+ _$pos = gtIdx + 1;
continue;
+ }
+ const tagStart: number = _$pos;
+ while (_$pos < _$len) {
+ const c: number = _$html.charCodeAt(_$pos);
+ if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) {
+ break;
+ }
+ _$pos++;
}
+ const tagId: number = $identifyTag(tagStart, _$pos);
- const textFormat = text_format.clone();
- switch ((node as Element).name.toUpperCase()) {
+ switch (tagId) {
- case "DIV": // div tag
- case "P": // p tag
- textParserSetAttributesUseCase((node as Element).attributes, textFormat, options);
+ case $TAG_B: {
+ const saved: boolean | null = text_format.bold;
+ text_format.bold = true;
+ $skipToClose();
+ $processChildren(tagId, text_format, text_data, options);
+ text_format.bold = saved;
+ continue;
+ }
- if (options.multiline) {
- textParserCreateNewLineUseCase(text_data, textFormat);
+ case $TAG_I: {
+ const saved: boolean | null = text_format.italic;
+ text_format.italic = true;
+ $skipToClose();
+ $processChildren(tagId, text_format, text_data, options);
+ text_format.italic = saved;
+ continue;
+ }
+
+ case $TAG_U: {
+ const saved: boolean | null = text_format.underline;
+ text_format.underline = true;
+ $skipToClose();
+ $processChildren(tagId, text_format, text_data, options);
+ text_format.underline = saved;
+ continue;
+ }
+
+ case $TAG_DIV:
+ case $TAG_P:
+ case $TAG_FONT:
+ case $TAG_SPAN: {
+ const sFont: string | null = text_format.font;
+ const sSize: number | null = text_format.size;
+ const sColor: number | null = text_format.color;
+ const sBold: boolean | null = text_format.bold;
+ const sItalic: boolean | null = text_format.italic;
+ const sUnderline: boolean | null = text_format.underline;
+ const sAlign: ITextFormatAlign | null = text_format.align;
+ const sLeftMargin: number | null = text_format.leftMargin;
+ const sRightMargin: number | null = text_format.rightMargin;
+ const sLeading: number | null = text_format.leading;
+ const sLetterSpacing: number | null = text_format.letterSpacing;
+
+ $applyAttributesInline(text_format, options);
+ $finishOpenTag();
+
+ if ((tagId === $TAG_P || tagId === $TAG_DIV) && options.multiline) {
+ textParserCreateNewLineUseCase(text_data, text_format);
}
- execute(node as Document, textFormat, text_data, options);
+ $processChildren(tagId, text_format, text_data, options);
+
+ text_format.font = sFont;
+ text_format.size = sSize;
+ text_format.color = sColor;
+ text_format.bold = sBold;
+ text_format.italic = sItalic;
+ text_format.underline = sUnderline;
+ text_format.align = sAlign;
+ text_format.leftMargin = sLeftMargin;
+ text_format.rightMargin = sRightMargin;
+ text_format.leading = sLeading;
+ text_format.letterSpacing = sLetterSpacing;
+ continue;
+ }
+
+ case $TAG_BR:
+ $skipToClose();
+ if (options.multiline) {
+ textParserCreateNewLineUseCase(text_data, text_format);
+ }
+ continue;
+ default:
+ $skipToClose();
continue;
- case "U": // underline
- textFormat.underline = true;
+ }
+ }
+};
+
+/**
+ * @description '>' までスキップ(V8 SIMD最適化のindexOf使用)
+ * Skip to '>' using SIMD-optimized indexOf
+ */
+const $skipToClose = (): void =>
+{
+ const idx: number = _$html.indexOf(">", _$pos);
+ _$pos = idx === -1 ? _$len : idx + 1;
+};
+
+/**
+ * @description 属性パース後のタグ末尾処理
+ * Handle end of opening tag after attribute parsing
+ */
+const $finishOpenTag = (): void =>
+{
+ if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x2F) {
+ _$pos++;
+ }
+ if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3E) {
+ _$pos++;
+ }
+};
+
+/**
+ * @description 属性をインラインで解析・直接適用
+ * IAttributeObject配列・オブジェクト・属性名文字列を一切生成しない
+ * Parse and apply attributes inline — zero arrays, objects, name strings
+ */
+const $applyAttributesInline = (
+ text_format: TextFormat,
+ options: IOptions
+): void => {
+
+ for (;;) {
+
+ while (_$pos < _$len) {
+ const c: number = _$html.charCodeAt(_$pos);
+ if (c !== 0x20 && c !== 0x09) {
break;
+ }
+ _$pos++;
+ }
+
+ if (_$pos >= _$len) {
+ break;
+ }
- case "B": // bold
- textFormat.bold = true;
+ const ch: number = _$html.charCodeAt(_$pos);
+ if (ch === 0x3E || ch === 0x2F) {
+ break;
+ }
+
+ const nameStart: number = _$pos;
+ while (_$pos < _$len) {
+ const c: number = _$html.charCodeAt(_$pos);
+ if (c === 0x3D || c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) {
break;
+ }
+ _$pos++;
+ }
+
+ if (_$pos === nameStart) {
+ break;
+ }
+
+ const name_len: number = _$pos - nameStart;
+ const name_c0: number = _$html.charCodeAt(nameStart);
- case "I": // italic
- textFormat.italic = true;
+ while (_$pos < _$len) {
+ const c: number = _$html.charCodeAt(_$pos);
+ if (c !== 0x20 && c !== 0x09) {
break;
+ }
+ _$pos++;
+ }
+
+ if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3D) {
+ _$pos++;
+
+ while (_$pos < _$len) {
+ const c: number = _$html.charCodeAt(_$pos);
+ if (c !== 0x20 && c !== 0x09) {
+ break;
+ }
+ _$pos++;
+ }
+
+ let value: string;
+ const quote: number = _$html.charCodeAt(_$pos);
+
+ if (quote === 0x22 || quote === 0x27) {
+ _$pos++;
+ const closeIdx: number = _$html.indexOf(
+ quote === 0x22 ? "\"" : "'", _$pos
+ );
+ if (closeIdx === -1) {
+ value = _$html.substring(_$pos);
+ _$pos = _$len;
+ } else {
+ value = _$html.substring(_$pos, closeIdx);
+ _$pos = closeIdx + 1;
+ }
+ } else {
+ const valStart: number = _$pos;
+ while (_$pos < _$len) {
+ const c: number = _$html.charCodeAt(_$pos);
+ if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) {
+ break;
+ }
+ _$pos++;
+ }
+ value = _$html.substring(valStart, _$pos);
+ }
+
+ $applyAttribute(name_len, name_c0, value, text_format, options);
+ } else {
+ $applyBooleanAttribute(name_len, name_c0, text_format);
+ }
+ }
+};
+
+/**
+ * @description 属性名をcharCodeで識別して値を直接適用
+ * Identify attribute name by charCode and apply value directly
+ *
+ * name lengths: face(4), size(4), bold(4), style(5), align(5), color(5),
+ * italic(6), leading(7), underline(9), leftMargin(10),
+ * rightMargin(11), letterSpacing(14)
+ */
+const $applyAttribute = (
+ name_len: number,
+ name_c0: number,
+ value: string,
+ text_format: TextFormat,
+ options: IOptions
+): void => {
+
+ switch (name_len) {
+
+ case 4:
+ // face(0x66/0x46) | size(0x73/0x53) | bold(0x62/0x42)
+ if (name_c0 === 0x66 || name_c0 === 0x46) {
+ text_format.font = value;
+ } else if (name_c0 === 0x73 || name_c0 === 0x53) {
+ text_format.size = +value;
+ if (options.subFontSize) {
+ text_format.size -= options.subFontSize;
+ if (1 > text_format.size) {
+ text_format.size = 1;
+ }
+ }
+ } else if (name_c0 === 0x62 || name_c0 === 0x42) {
+ text_format.bold = true;
+ }
+ break;
+
+ case 5:
+ // color(0x63/0x43) | style(0x73/0x53) | align(0x61/0x41)
+ if (name_c0 === 0x63 || name_c0 === 0x43) {
+ text_format.color = $toColorInt(value);
+ } else if (name_c0 === 0x73 || name_c0 === 0x53) {
+ $applyStyleAttributes(
+ textParserParseStyleService(value),
+ text_format, options
+ );
+ } else if (name_c0 === 0x61 || name_c0 === 0x41) {
+ text_format.align = value as ITextFormatAlign;
+ }
+ break;
+
+ case 6:
+ // italic(0x69/0x49)
+ if (name_c0 === 0x69 || name_c0 === 0x49) {
+ text_format.italic = true;
+ }
+ break;
+
+ case 7:
+ // leading(0x6C/0x4C)
+ if (name_c0 === 0x6C || name_c0 === 0x4C) {
+ text_format.leading = parseInt(value);
+ }
+ break;
+
+ case 9:
+ // underline(0x75/0x55)
+ if (name_c0 === 0x75 || name_c0 === 0x55) {
+ text_format.underline = true;
+ }
+ break;
- case "FONT": // FONT tag
- case "SPAN": // SPAN tag
- textParserSetAttributesUseCase((node as Element).attributes, textFormat, options);
+ case 10:
+ // leftMargin(0x6C/0x4C)
+ if (name_c0 === 0x6C || name_c0 === 0x4C) {
+ text_format.leftMargin = parseInt(value);
+ }
+ break;
+
+ case 11:
+ // rightMargin(0x72/0x52)
+ if (name_c0 === 0x72 || name_c0 === 0x52) {
+ text_format.rightMargin = parseInt(value);
+ }
+ break;
+
+ case 14:
+ // letterSpacing(0x6C/0x4C)
+ if (name_c0 === 0x6C || name_c0 === 0x4C) {
+ text_format.letterSpacing = parseInt(value);
+ }
+ break;
+
+ default:
+ break;
+
+ }
+};
+
+/**
+ * @description 値なし属性をcharCodeで識別して直接適用
+ * Apply boolean (no-value) attributes by charCode
+ */
+const $applyBooleanAttribute = (
+ name_len: number,
+ name_c0: number,
+ text_format: TextFormat
+): void => {
+ if (name_len === 4 && (name_c0 === 0x62 || name_c0 === 0x42)) {
+ text_format.bold = true;
+ } else if (name_len === 6 && (name_c0 === 0x69 || name_c0 === 0x49)) {
+ text_format.italic = true;
+ } else if (name_len === 9 && (name_c0 === 0x75 || name_c0 === 0x55)) {
+ text_format.underline = true;
+ }
+};
+
+/**
+ * @description style属性の解析結果をtext_formatに直接適用
+ * Apply parsed style attributes directly to text_format
+ */
+const $applyStyleAttributes = (
+ attributes: IAttributeObject[],
+ text_format: TextFormat,
+ options: IOptions
+): void => {
+ for (let idx: number = 0; idx < attributes.length; ++idx) {
+ const attr: IAttributeObject = attributes[idx];
+ switch (attr.name) {
+
+ case "face":
+ text_format.font = attr.value as string;
break;
- case "BR":
- if (!options.multiline) {
- continue;
+ case "size":
+ text_format.size = +attr.value;
+ if (options.subFontSize) {
+ text_format.size -= options.subFontSize;
+ if (1 > text_format.size) {
+ text_format.size = 1;
+ }
}
- textParserCreateNewLineUseCase(text_data, textFormat);
+ break;
+
+ case "color":
+ text_format.color = $toColorInt(attr.value);
+ break;
+
+ case "align":
+ text_format.align = attr.value as ITextFormatAlign;
+ break;
+
+ case "letterSpacing":
+ text_format.letterSpacing = parseInt(attr.value as string);
+ break;
+
+ case "leading":
+ text_format.leading = parseInt(attr.value as string);
+ break;
+
+ case "leftMargin":
+ text_format.leftMargin = parseInt(attr.value as string);
+ break;
+
+ case "rightMargin":
+ text_format.rightMargin = parseInt(attr.value as string);
+ break;
+
+ case "underline":
+ text_format.underline = true;
+ break;
+
+ case "bold":
+ text_format.bold = true;
+ break;
+
+ case "italic":
+ text_format.italic = true;
break;
default:
break;
}
+ }
+};
+
+/**
+ * @description 文字列未生成でタグを数値IDに変換
+ * Zero-allocation tag identification via character comparison
+ */
+const $identifyTag = (start: number, end: number): number =>
+{
+ while (start < end) {
+ const c: number = _$html.charCodeAt(start);
+ if (c !== 0x20 && c !== 0x09) {
+ break;
+ }
+ start++;
+ }
+ while (end > start) {
+ const c: number = _$html.charCodeAt(end - 1);
+ if (c !== 0x20 && c !== 0x09) {
+ break;
+ }
+ end--;
+ }
+
+ const length: number = end - start;
+
+ if (length === 1) {
+ let c: number = _$html.charCodeAt(start);
+ if (c >= 0x61) { c -= 0x20 }
+ if (c === 0x42) { return $TAG_B } // B
+ if (c === 0x49) { return $TAG_I } // I
+ if (c === 0x55) { return $TAG_U } // U
+ if (c === 0x50) { return $TAG_P } // P
+ return $TAG_NONE;
+ }
+
+ if (length === 2) {
+ let c0: number = _$html.charCodeAt(start);
+ let c1: number = _$html.charCodeAt(start + 1);
+ if (c0 >= 0x61) { c0 -= 0x20 }
+ if (c1 >= 0x61) { c1 -= 0x20 }
+ if (c0 === 0x42 && c1 === 0x52) { return $TAG_BR } // BR
+ return $TAG_NONE;
+ }
+
+ if (length === 3) {
+ let c0: number = _$html.charCodeAt(start);
+ if (c0 >= 0x61) { c0 -= 0x20 }
+ if (c0 === 0x44) { // D
+ let c1: number = _$html.charCodeAt(start + 1);
+ let c2: number = _$html.charCodeAt(start + 2);
+ if (c1 >= 0x61) { c1 -= 0x20 }
+ if (c2 >= 0x61) { c2 -= 0x20 }
+ if (c1 === 0x49 && c2 === 0x56) { return $TAG_DIV } // DIV
+ }
+ return $TAG_NONE;
+ }
- execute(node as Document, textFormat, text_data, options);
+ if (length === 4) {
+ let c0: number = _$html.charCodeAt(start);
+ if (c0 >= 0x61) { c0 -= 0x20 }
+ if (c0 === 0x46) { // F
+ let c1: number = _$html.charCodeAt(start + 1);
+ let c2: number = _$html.charCodeAt(start + 2);
+ let c3: number = _$html.charCodeAt(start + 3);
+ if (c1 >= 0x61) { c1 -= 0x20 }
+ if (c2 >= 0x61) { c2 -= 0x20 }
+ if (c3 >= 0x61) { c3 -= 0x20 }
+ if (c1 === 0x4F && c2 === 0x4E && c3 === 0x54) { return $TAG_FONT } // FONT
+ }
+ if (c0 === 0x53) { // S
+ let c1: number = _$html.charCodeAt(start + 1);
+ let c2: number = _$html.charCodeAt(start + 2);
+ let c3: number = _$html.charCodeAt(start + 3);
+ if (c1 >= 0x61) { c1 -= 0x20 }
+ if (c2 >= 0x61) { c2 -= 0x20 }
+ if (c3 >= 0x61) { c3 -= 0x20 }
+ if (c1 === 0x50 && c2 === 0x41 && c3 === 0x4E) { return $TAG_SPAN } // SPAN
+ }
+ return $TAG_NONE;
}
+
+ return $TAG_NONE;
};
\ No newline at end of file
diff --git a/packages/text/src/interface/IHtmlNode.ts b/packages/text/src/interface/IHtmlNode.ts
new file mode 100644
index 00000000..a136e1d6
--- /dev/null
+++ b/packages/text/src/interface/IHtmlNode.ts
@@ -0,0 +1,27 @@
+import type { IAttributeObject } from "./IAttributeObject";
+
+/**
+ * @description テキストノード
+ * Text node
+ */
+export interface IHtmlTextNode {
+ readonly type: "text";
+ readonly value: string;
+}
+
+/**
+ * @description 要素ノード
+ * Element node
+ */
+export interface IHtmlElementNode {
+ readonly type: "element";
+ readonly tagName: string;
+ readonly attributes: IAttributeObject[];
+ readonly children: IHtmlNode[];
+}
+
+/**
+ * @description HTMLノード共用型
+ * HTML node union type
+ */
+export type IHtmlNode = IHtmlTextNode | IHtmlElementNode;
diff --git a/packages/webgl/src/Context.ts b/packages/webgl/src/Context.ts
index 058892c7..f53b295d 100644
--- a/packages/webgl/src/Context.ts
+++ b/packages/webgl/src/Context.ts
@@ -47,6 +47,7 @@ import { execute as contextStrokeUseCase } from "./Context/usecase/ContextStroke
import { execute as contextApplyFilterUseCase } from "./Context/usecase/ContextApplyFilterUseCase";
import { execute as contextContainerBeginLayerUseCase } from "./Context/usecase/ContextContainerBeginLayerUseCase";
import { execute as contextContainerEndLayerUseCase } from "./Context/usecase/ContextContainerEndLayerUseCase";
+import { execute as contextContainerEndAtlasNodeUseCase } from "./Context/usecase/ContextContainerEndAtlasNodeUseCase";
import { execute as contextContainerDrawCachedFilterUseCase } from "./Context/usecase/ContextContainerDrawCachedFilterUseCase";
import { execute as contextUpdateTransferBoundsService } from "./Context/service/ContextUpdateTransferBoundsService";
import { execute as contextDrawFillUseCase } from "./Context/usecase/ContextDrawFillUseCase";
@@ -95,6 +96,7 @@ export class Context
public joints: number;
public miterLimit: number;
public newDrawState: boolean = false;
+ private readonly _pendingAtlasNodes: Node[] = [];
constructor (
gl: WebGL2RenderingContext,
@@ -509,6 +511,26 @@ export class Context
);
}
+ containerBeginAtlasNode (width: number, height: number): Node
+ {
+ this.drawArraysInstanced();
+
+ // アトラスノードを確保(後でコピー先として使用)
+ const node = atlasManagerCreateNodeService(width, height);
+ this._pendingAtlasNodes.push(node);
+
+ // temp FBOを作成して子要素の描画先として設定
+ contextContainerBeginLayerUseCase(width, height);
+
+ return node;
+ }
+
+ containerEndAtlasNode (): void
+ {
+ const node = this._pendingAtlasNodes.pop()!;
+ contextContainerEndAtlasNodeUseCase(node);
+ }
+
containerDrawCachedFilter (
blend_mode: IBlendMode,
matrix: Float32Array,
diff --git a/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts
new file mode 100644
index 00000000..92c2d069
--- /dev/null
+++ b/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts
@@ -0,0 +1,75 @@
+import { execute, $containerLayerStack } from "./ContextContainerBeginLayerUseCase";
+import { describe, expect, it, vi, beforeEach } from "vitest";
+
+vi.mock("../../WebGLUtil", () => ({
+ "$context": {
+ "drawArraysInstanced": vi.fn(),
+ "$mainAttachmentObject": { "width": 800, "height": 600, "label": "main" },
+ "bind": vi.fn()
+ }
+}));
+
+vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerGetAttachmentObjectUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600, "label": "layer" }))
+}));
+
+describe("ContextContainerBeginLayerUseCase.js test", () => {
+
+ beforeEach(() =>
+ {
+ $containerLayerStack.length = 0;
+ });
+
+ it("execute test case1 - pushes current main to stack", async () =>
+ {
+ const { $context } = await import("../../WebGLUtil");
+ const originalMain = $context.$mainAttachmentObject;
+
+ execute(800, 600);
+
+ expect($containerLayerStack.length).toBe(1);
+ expect($containerLayerStack[0]).toBe(originalMain);
+ });
+
+ it("execute test case2 - flushes instanced draw before switching", async () =>
+ {
+ const { $context } = await import("../../WebGLUtil");
+ vi.mocked($context.drawArraysInstanced).mockClear();
+
+ execute(800, 600);
+
+ expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1);
+ });
+
+ it("execute test case3 - sets new layer attachment as main", async () =>
+ {
+ const { $context } = await import("../../WebGLUtil");
+
+ execute(400, 300);
+
+ expect($context.$mainAttachmentObject).toEqual({ "width": 800, "height": 600, "label": "layer" });
+ });
+
+ it("execute test case4 - binds layer attachment", async () =>
+ {
+ const { $context } = await import("../../WebGLUtil");
+ vi.mocked($context.bind).mockClear();
+
+ execute(800, 600);
+
+ expect($context.bind).toHaveBeenCalledTimes(1);
+ });
+
+ it("execute test case5 - multiple layers stack correctly", async () =>
+ {
+ const { $context } = await import("../../WebGLUtil");
+
+ $context.$mainAttachmentObject = { "label": "main1" } as any;
+ execute(800, 600);
+
+ $context.$mainAttachmentObject = { "label": "main2" } as any;
+ execute(400, 300);
+
+ expect($containerLayerStack.length).toBe(2);
+ });
+});
diff --git a/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts
new file mode 100644
index 00000000..a709cfd7
--- /dev/null
+++ b/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts
@@ -0,0 +1,93 @@
+import { execute } from "./ContextContainerDrawCachedFilterUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("@next2d/cache", () => ({
+ "$cacheStore": {
+ "get": vi.fn()
+ }
+}));
+
+vi.mock("../../WebGLUtil", () => ({
+ "$context": {
+ "drawArraysInstanced": vi.fn(),
+ "reset": vi.fn(),
+ "globalCompositeOperation": "normal"
+ },
+ "$devicePixelRatio": 1
+}));
+
+vi.mock("../../Blend/usecase/BlendDrawFilterToMainUseCase", () => ({
+ "execute": vi.fn()
+}));
+
+describe("ContextContainerDrawCachedFilterUseCase.js test", () => {
+
+ it("execute test case1 - returns early if cached key does not match", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+ const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase");
+
+ vi.mocked($cacheStore.get).mockImplementation((key: string, prop: string) => {
+ if (prop === "fKey") { return "old_key"; }
+ return null;
+ });
+ vi.mocked(blendMod.execute).mockClear();
+
+ const matrix = new Float32Array([1, 0, 0, 1, 10, 20]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+ const filterBounds = new Float32Array([-5, -5, 110, 110]);
+
+ execute("normal", matrix, colorTransform, filterBounds, "1", "new_key");
+
+ expect(blendMod.execute).not.toHaveBeenCalled();
+ });
+
+ it("execute test case2 - returns early if no texture object", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+ const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase");
+
+ vi.mocked($cacheStore.get).mockImplementation((_key: string, prop: string) => {
+ if (prop === "fKey") { return "match_key"; }
+ if (prop === "fTexture") { return null; }
+ return null;
+ });
+ vi.mocked(blendMod.execute).mockClear();
+
+ const matrix = new Float32Array([1, 0, 0, 1, 10, 20]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+ const filterBounds = new Float32Array([-5, -5, 110, 110]);
+
+ execute("normal", matrix, colorTransform, filterBounds, "1", "match_key");
+
+ expect(blendMod.execute).not.toHaveBeenCalled();
+ });
+
+ it("execute test case3 - draws cached filter when key matches", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+ const { $context } = await import("../../WebGLUtil");
+ const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase");
+
+ const mockTexture = { "width": 100, "height": 100 };
+ vi.mocked($cacheStore.get).mockImplementation((_key: string, prop: string) => {
+ if (prop === "fKey") { return "valid_key"; }
+ if (prop === "fTexture") { return mockTexture; }
+ return null;
+ });
+ vi.mocked($context.drawArraysInstanced).mockClear();
+ vi.mocked($context.reset).mockClear();
+ vi.mocked(blendMod.execute).mockClear();
+
+ const matrix = new Float32Array([1, 0, 0, 1, 10, 20]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+ const filterBounds = new Float32Array([-5, -5, 110, 110]);
+
+ execute("add", matrix, colorTransform, filterBounds, "1", "valid_key");
+
+ expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1);
+ expect($context.reset).toHaveBeenCalledTimes(1);
+ expect($context.globalCompositeOperation).toBe("add");
+ expect(blendMod.execute).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/webgl/src/Context/usecase/ContextContainerEndAtlasNodeUseCase.ts b/packages/webgl/src/Context/usecase/ContextContainerEndAtlasNodeUseCase.ts
new file mode 100644
index 00000000..30ef2256
--- /dev/null
+++ b/packages/webgl/src/Context/usecase/ContextContainerEndAtlasNodeUseCase.ts
@@ -0,0 +1,79 @@
+import type { IAttachmentObject } from "../../interface/IAttachmentObject";
+import type { ITextureObject } from "../../interface/ITextureObject";
+import type { Node } from "@next2d/texture-packer";
+import { $containerLayerStack } from "./ContextContainerBeginLayerUseCase";
+import { execute as frameBufferManagerReleaseAttachmentObjectUseCase } from "../../FrameBufferManager/usecase/FrameBufferManagerReleaseAttachmentObjectUseCase";
+import { execute as variantsBlendMatrixTextureShaderService } from "../../Shader/Variants/Blend/service/VariantsBlendMatrixTextureShaderService";
+import { execute as shaderManagerSetMatrixTextureWithColorTransformUniformService } from "../../Shader/ShaderManager/service/ShaderManagerSetMatrixTextureWithColorTransformUniformService";
+import { execute as textureManagerBind0UseCase } from "../../TextureManager/usecase/TextureManagerBind0UseCase";
+import { execute as shaderManagerDrawTextureUseCase } from "../../Shader/ShaderManager/usecase/ShaderManagerDrawTextureUseCase";
+import { execute as textureManagerReleaseTextureObjectUseCase } from "../../TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase";
+import { execute as blendOperationUseCase } from "../../Blend/usecase/BlendOperationUseCase";
+import { $context } from "../../WebGLUtil";
+import { $getAtlasAttachmentObject } from "../../AtlasManager";
+
+/**
+ * @description コンテナ用のカラー変換(恒等変換)
+ * Identity color transform for container copy
+ *
+ * @type {Float32Array}
+ * @private
+ */
+const $identityColorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+/**
+ * @description コンテナのアトラスノードへの描画を終了し、
+ * temp FBOの内容をアトラスのノード領域にコピーします。
+ * End container atlas node rendering,
+ * copy temp FBO contents to atlas node region.
+ *
+ * @param {Node} node
+ * @return {void}
+ * @method
+ * @protected
+ */
+export const execute = (node: Node): void =>
+{
+ // temp FBOへの描画をフラッシュ
+ $context.drawArraysInstanced();
+
+ const tempAttachment = $context.$mainAttachmentObject as IAttachmentObject;
+ const textureObject = tempAttachment.texture as ITextureObject;
+
+ // mainを復元($containerLayerStackから)
+ $context.$mainAttachmentObject = $containerLayerStack.pop() as IAttachmentObject;
+
+ // temp FBOを解放(テクスチャは保持してアトラスコピーに使用)
+ frameBufferManagerReleaseAttachmentObjectUseCase(tempAttachment, false);
+
+ if (textureObject) {
+ // アトラスにバインド
+ const atlas = $getAtlasAttachmentObject() as IAttachmentObject;
+ $context.bind(atlas);
+
+ // ノード領域を設定(scissor + clear + transfer bounds登録)
+ $context.beginNodeRendering(node);
+
+ // ブレンドモード設定(premultiplied alpha用: ONE, ONE_MINUS_SRC_ALPHA)
+ blendOperationUseCase("normal");
+
+ // テクスチャをノード位置に描画(Y軸反転: WebGLはbottom-left原点)
+ const offsetY = atlas.height - node.y - node.h;
+ $context.setTransform(1, 0, 0, 1, node.x, offsetY);
+ const shaderManager = variantsBlendMatrixTextureShaderService(false);
+ shaderManagerSetMatrixTextureWithColorTransformUniformService(
+ shaderManager, $identityColorTransform, node.w, node.h
+ );
+ textureManagerBind0UseCase(textureObject);
+ shaderManagerDrawTextureUseCase(shaderManager);
+
+ // ノード描画終了
+ $context.endNodeRendering();
+
+ // テクスチャを解放
+ textureManagerReleaseTextureObjectUseCase(textureObject);
+ }
+
+ // mainをバインド
+ $context.bind($context.$mainAttachmentObject as IAttachmentObject);
+};
diff --git a/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts
new file mode 100644
index 00000000..5164173b
--- /dev/null
+++ b/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts
@@ -0,0 +1,122 @@
+import { execute } from "./ContextContainerEndLayerUseCase";
+import { describe, expect, it, vi, beforeEach } from "vitest";
+
+vi.mock("@next2d/cache", () => ({
+ "$cacheStore": {
+ "set": vi.fn(),
+ "get": vi.fn()
+ }
+}));
+
+vi.mock("../../WebGLUtil", () => ({
+ "$context": {
+ "drawArraysInstanced": vi.fn(),
+ "$mainAttachmentObject": { "width": 800, "height": 600, "label": "layer" },
+ "bind": vi.fn(),
+ "reset": vi.fn(),
+ "globalCompositeOperation": "normal"
+ },
+ "$devicePixelRatio": 1
+}));
+
+vi.mock("./ContextContainerBeginLayerUseCase", () => ({
+ "$containerLayerStack": [] as any[]
+}));
+
+vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerGetTextureFromBoundsUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600, "textureId": 1 }))
+}));
+
+vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerReleaseAttachmentObjectUseCase", () => ({
+ "execute": vi.fn()
+}));
+
+vi.mock("../../Blend/usecase/BlendDrawFilterToMainUseCase", () => ({
+ "execute": vi.fn()
+}));
+
+vi.mock("../../TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase", () => ({
+ "execute": vi.fn()
+}));
+
+vi.mock("../../Filter/BlurFilter/usecase/FilterApplyBlurFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600, "textureId": 2 }))
+}));
+vi.mock("../../Filter/BevelFilter/usecase/FilterApplyBevelFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600 }))
+}));
+vi.mock("../../Filter/ColorMatrixFilter/usecase/FilterApplyColorMatrixFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600 }))
+}));
+vi.mock("../../Filter/ConvolutionFilter/usecase/FilterApplyConvolutionFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600 }))
+}));
+vi.mock("../../Filter/DisplacementMapFilter/usecase/FilterApplyDisplacementMapFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600 }))
+}));
+vi.mock("../../Filter/DropShadowFilter/usecase/FilterApplyDropShadowFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600 }))
+}));
+vi.mock("../../Filter/GlowFilter/usecase/FilterApplyGlowFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600 }))
+}));
+vi.mock("../../Filter/GradientBevelFilter/usecase/FilterApplyGradientBevelFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600 }))
+}));
+vi.mock("../../Filter/GradientGlowFilter/usecase/FilterApplyGradientGlowFilterUseCase", () => ({
+ "execute": vi.fn(() => ({ "width": 800, "height": 600 }))
+}));
+vi.mock("../../Filter", () => ({
+ "$offset": { "x": 0, "y": 0 }
+}));
+
+describe("ContextContainerEndLayerUseCase.js test", () => {
+
+ beforeEach(async () =>
+ {
+ vi.clearAllMocks();
+ const { $containerLayerStack } = await import("./ContextContainerBeginLayerUseCase");
+ $containerLayerStack.length = 0;
+ $containerLayerStack.push({ "width": 800, "height": 600, "label": "main" } as any);
+
+ const { $context } = await import("../../WebGLUtil");
+ $context.$mainAttachmentObject = { "width": 800, "height": 600, "label": "layer" } as any;
+ });
+
+ it("execute test case1 - blend only (no filter)", async () =>
+ {
+ const { $context } = await import("../../WebGLUtil");
+ const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase");
+ const releaseMod = await import("../../TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase");
+
+ const matrix = new Float32Array([1, 0, 0, 1, 10, 20]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ execute("normal", matrix, colorTransform, false, null, null, "", "");
+
+ expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1);
+ expect($context.reset).toHaveBeenCalledTimes(1);
+ expect(blendMod.execute).toHaveBeenCalledTimes(1);
+ expect(releaseMod.execute).toHaveBeenCalledTimes(1);
+ expect($context.bind).toHaveBeenCalled();
+ });
+
+ it("execute test case2 - with filter (blur)", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+ const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase");
+ const blurMod = await import("../../Filter/BlurFilter/usecase/FilterApplyBlurFilterUseCase");
+
+ const matrix = new Float32Array([1, 0, 0, 1, 10, 20]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+ const filterBounds = new Float32Array([-5, -5, 110, 110]);
+ // BlurFilter: type=1, blurX=4, blurY=4, quality=1
+ const filterParams = new Float32Array([1, 4, 4, 1]);
+
+ execute("normal", matrix, colorTransform, true, filterBounds, filterParams, "uk1", "fk1");
+
+ expect(blurMod.execute).toHaveBeenCalledTimes(1);
+ expect($cacheStore.set).toHaveBeenCalledWith("uk1", "fKey", "fk1");
+ expect(blendMod.execute).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/webgpu/src/AtlasManager.test.ts b/packages/webgpu/src/AtlasManager.test.ts
index daa5c803..a8e48e2d 100644
--- a/packages/webgpu/src/AtlasManager.test.ts
+++ b/packages/webgpu/src/AtlasManager.test.ts
@@ -5,12 +5,8 @@ import {
$getAtlasAttachmentObjects,
$setAtlasAttachmentObject,
$getAtlasAttachmentObject,
- $hasAtlasAttachmentObject,
$rootNodes,
- $setAtlasTexture,
- $getAtlasTexture,
$getActiveTransferBounds,
- $getActiveAllTransferBounds,
$clearTransferBounds,
$setCurrentAtlasIndex,
$getCurrentAtlasIndex,
@@ -91,31 +87,6 @@ describe("AtlasManager", () =>
});
});
- describe("$hasAtlasAttachmentObject", () =>
- {
- it("should return false when no attachment exists", () =>
- {
- expect($hasAtlasAttachmentObject()).toBe(false);
- });
-
- it("should return true when attachment exists", () =>
- {
- const mockAttachment = createMockAttachment(1, 512, 512);
- $setAtlasAttachmentObject(mockAttachment);
-
- expect($hasAtlasAttachmentObject()).toBe(true);
- });
-
- it("should return false at non-existing index", () =>
- {
- const mockAttachment = createMockAttachment(1, 512, 512);
- $setAtlasAttachmentObject(mockAttachment);
-
- $setActiveAtlasIndex(5);
- expect($hasAtlasAttachmentObject()).toBe(false);
- });
- });
-
describe("$rootNodes", () =>
{
it("should be empty array after reset", () =>
@@ -133,44 +104,6 @@ describe("AtlasManager", () =>
});
});
- describe("$setAtlasTexture / $getAtlasTexture", () =>
- {
- it("should set and get atlas texture", () =>
- {
- const mockTexture = {
- "id": 1,
- "width": 4096,
- "height": 4096,
- "area": 4096 * 4096,
- "smooth": true,
- "resource": {},
- "view": {}
- } as any;
-
- $setAtlasTexture(mockTexture);
-
- expect($getAtlasTexture()).toBe(mockTexture);
- });
-
- it("should allow setting to null", () =>
- {
- const mockTexture = {
- "id": 1,
- "width": 4096,
- "height": 4096,
- "area": 4096 * 4096,
- "smooth": true,
- "resource": {},
- "view": {}
- } as any;
-
- $setAtlasTexture(mockTexture);
- $setAtlasTexture(null);
-
- expect($getAtlasTexture()).toBeNull();
- });
- });
-
describe("$getActiveTransferBounds", () =>
{
it("should create transfer bounds at index", () =>
@@ -209,28 +142,6 @@ describe("AtlasManager", () =>
});
});
- describe("$getActiveAllTransferBounds", () =>
- {
- it("should create all transfer bounds at index", () =>
- {
- const bounds = $getActiveAllTransferBounds(0);
-
- expect(bounds).toBeInstanceOf(Float32Array);
- expect(bounds.length).toBe(4);
- });
-
- it("should initialize with max/min values", () =>
- {
- const bounds = $getActiveAllTransferBounds(0);
-
- // Float32Array stores Number.MAX_VALUE as Infinity
- expect(bounds[0]).toBe(Infinity);
- expect(bounds[1]).toBe(Infinity);
- expect(bounds[2]).toBe(-Infinity);
- expect(bounds[3]).toBe(-Infinity);
- });
- });
-
describe("$clearTransferBounds", () =>
{
it("should reset transfer bounds to initial values", () =>
@@ -250,22 +161,19 @@ describe("AtlasManager", () =>
expect(bounds[3]).toBe(-Infinity);
});
- it("should reset all transfer bounds arrays", () =>
+ it("should reset multiple transfer bounds arrays", () =>
{
const bounds0 = $getActiveTransferBounds(0);
const bounds1 = $getActiveTransferBounds(1);
- const allBounds0 = $getActiveAllTransferBounds(0);
bounds0[0] = 50;
bounds1[0] = 60;
- allBounds0[0] = 70;
$clearTransferBounds();
// Float32Array stores Number.MAX_VALUE as Infinity
expect(bounds0[0]).toBe(Infinity);
expect(bounds1[0]).toBe(Infinity);
- expect(allBounds0[0]).toBe(Infinity);
});
});
@@ -348,7 +256,7 @@ describe("AtlasManager", () =>
$resetAtlas();
// Attachment should be removed
- expect($hasAtlasAttachmentObject()).toBe(false);
+ expect($getAtlasAttachmentObjects().length).toBe(0);
});
it("should clear transfer bounds", () =>
diff --git a/packages/webgpu/src/AtlasManager.ts b/packages/webgpu/src/AtlasManager.ts
index 9bfd2cb1..f53c9050 100644
--- a/packages/webgpu/src/AtlasManager.ts
+++ b/packages/webgpu/src/AtlasManager.ts
@@ -1,43 +1,105 @@
import type { IAttachmentObject } from "./interface/IAttachmentObject";
-import type { ITextureObject } from "./interface/ITextureObject";
import type { TexturePacker } from "@next2d/texture-packer";
+/**
+ * @description テクスチャアトラス境界の初期最大値
+ * Initial maximum value for texture atlas bounds
+ * @type {number}
+ */
const $MAX_VALUE: number = Number.MAX_VALUE;
+
+/**
+ * @description テクスチャアトラス境界の初期最小値
+ * Initial minimum value for texture atlas bounds
+ * @type {number}
+ */
const $MIN_VALUE: number = -Number.MAX_VALUE;
+/**
+ * @description 現在アクティブなアトラスのインデックス
+ * Index of the currently active atlas
+ * @type {number}
+ */
let $activeAtlasIndex: number = 0;
+/**
+ * @description アクティブなアトラスインデックスを設定する
+ * Set the active atlas index
+ * @param {number} index - アトラスインデックス / atlas index
+ * @return {void}
+ */
export const $setActiveAtlasIndex = (index: number): void =>
{
$activeAtlasIndex = index;
};
+/**
+ * @description アクティブなアトラスインデックスを取得する
+ * Get the active atlas index
+ * @return {number}
+ */
export const $getActiveAtlasIndex = (): number =>
{
return $activeAtlasIndex;
};
+/**
+ * @description アトラスのアタッチメントオブジェクト配列
+ * Array of atlas attachment objects
+ * @type {IAttachmentObject[]}
+ */
const $atlasAttachmentObjects: IAttachmentObject[] = [];
+/**
+ * @description アトラスのアタッチメントオブジェクト配列を取得する
+ * Get the array of atlas attachment objects
+ * @return {IAttachmentObject[]}
+ */
export const $getAtlasAttachmentObjects = (): IAttachmentObject[] =>
{
return $atlasAttachmentObjects;
};
+/**
+ * @description アクティブなインデックスにアタッチメントオブジェクトを設定する
+ * Set an attachment object at the active atlas index
+ * @param {IAttachmentObject} attachment_object - アタッチメントオブジェクト / attachment object
+ * @return {void}
+ */
export const $setAtlasAttachmentObject = (attachment_object: IAttachmentObject): void =>
{
$atlasAttachmentObjects[$activeAtlasIndex] = attachment_object;
};
+/**
+ * @description アトラス生成関数の型定義
+ * Type definition for atlas creator function
+ */
type AtlasCreator = (index: number) => IAttachmentObject;
+/**
+ * @description アトラス生成関数の参照
+ * Atlas creator function reference
+ * @type {AtlasCreator | null}
+ */
let $atlasCreator: AtlasCreator | null = null;
+/**
+ * @description アトラス生成関数を設定する
+ * Set the atlas creator function
+ * @param {AtlasCreator} creator - アトラス生成関数 / atlas creator function
+ * @return {void}
+ */
export const $setAtlasCreator = (creator: AtlasCreator): void =>
{
$atlasCreator = creator;
};
+/**
+ * @description アクティブなインデックスのアタッチメントオブジェクトを取得する(未作成の場合はcreatorで生成)
+ * Get the attachment object at the active index (creates via creator if not yet created)
+ * @return {IAttachmentObject | null}
+ */
export const $getAtlasAttachmentObject = (): IAttachmentObject | null =>
{
if (!($activeAtlasIndex in $atlasAttachmentObjects)) {
@@ -51,6 +113,12 @@ export const $getAtlasAttachmentObject = (): IAttachmentObject | null =>
return $atlasAttachmentObjects[$activeAtlasIndex];
};
+/**
+ * @description 指定インデックスのアタッチメントオブジェクトを取得する
+ * Get the attachment object at a specified index
+ * @param {number} index - アトラスインデックス / atlas index
+ * @return {IAttachmentObject | null}
+ */
export const $getAtlasAttachmentObjectByIndex = (index: number): IAttachmentObject | null =>
{
if (!(index in $atlasAttachmentObjects)) {
@@ -59,27 +127,26 @@ export const $getAtlasAttachmentObjectByIndex = (index: number): IAttachmentObje
return $atlasAttachmentObjects[index];
};
-export const $hasAtlasAttachmentObject = (): boolean =>
-{
- return $activeAtlasIndex in $atlasAttachmentObjects;
-};
-
+/**
+ * @description テクスチャパッカーのルートノード配列
+ * Array of root nodes for texture packing
+ * @type {TexturePacker[]}
+ */
export const $rootNodes: TexturePacker[] = [];
-export let $atlasTexture: ITextureObject | null = null;
-
-export const $setAtlasTexture = (texture_object: ITextureObject | null): void =>
-{
- $atlasTexture = texture_object;
-};
-
-export const $getAtlasTexture = (): ITextureObject | null =>
-{
- return $atlasTexture;
-};
-
+/**
+ * @description アトラスごとの転送領域配列
+ * Array of transfer bounds per atlas
+ * @type {Float32Array[]}
+ */
const $transferBounds: Float32Array[] = [];
+/**
+ * @description 指定インデックスのアクティブな転送領域を取得する(未作成の場合は初期化)
+ * Get the active transfer bounds at the specified index (initializes if not yet created)
+ * @param {number} index - アトラスインデックス / atlas index
+ * @return {Float32Array}
+ */
export const $getActiveTransferBounds = (index: number): Float32Array =>
{
if (!(index in $transferBounds)) {
@@ -93,21 +160,11 @@ export const $getActiveTransferBounds = (index: number): Float32Array =>
return $transferBounds[index];
};
-const $allTransferBounds: Float32Array[] = [];
-
-export const $getActiveAllTransferBounds = (index: number): Float32Array =>
-{
- if (!(index in $allTransferBounds)) {
- $allTransferBounds[index] = new Float32Array([
- $MAX_VALUE,
- $MAX_VALUE,
- $MIN_VALUE,
- $MIN_VALUE
- ]);
- }
- return $allTransferBounds[index];
-};
-
+/**
+ * @description 全ての転送領域を初期値にリセットする
+ * Reset all transfer bounds to their initial values
+ * @return {void}
+ */
export const $clearTransferBounds = (): void =>
{
for (let idx = 0; idx < $transferBounds.length; ++idx) {
@@ -119,30 +176,41 @@ export const $clearTransferBounds = (): void =>
bounds[0] = bounds[1] = $MAX_VALUE;
bounds[2] = bounds[3] = $MIN_VALUE;
}
-
- for (let idx = 0; idx < $allTransferBounds.length; ++idx) {
- const bounds = $allTransferBounds[idx];
- if (!bounds) {
- continue;
- }
-
- bounds[0] = bounds[1] = $MAX_VALUE;
- bounds[2] = bounds[3] = $MIN_VALUE;
- }
};
+/**
+ * @description 現在処理中のアトラスインデックス
+ * Index of the currently processed atlas
+ * @type {number}
+ */
let $currentAtlasIndex: number = 0;
+/**
+ * @description 現在のアトラスインデックスを設定する
+ * Set the current atlas index
+ * @param {number} index - アトラスインデックス / atlas index
+ * @return {void}
+ */
export const $setCurrentAtlasIndex = (index: number): void =>
{
$currentAtlasIndex = index;
};
+/**
+ * @description 現在のアトラスインデックスを取得する
+ * Get the current atlas index
+ * @return {number}
+ */
export const $getCurrentAtlasIndex = (): number =>
{
return $currentAtlasIndex;
};
+/**
+ * @description アトラスの全状態をリセットする(テクスチャリソースの破棄を含む)
+ * Reset all atlas state including destroying texture resources
+ * @return {void}
+ */
export const $resetAtlas = (): void =>
{
$rootNodes.length = 0;
diff --git a/packages/webgpu/src/AttachmentManager.test.ts b/packages/webgpu/src/AttachmentManager.test.ts
index 2db615c6..d47a1f18 100644
--- a/packages/webgpu/src/AttachmentManager.test.ts
+++ b/packages/webgpu/src/AttachmentManager.test.ts
@@ -27,16 +27,6 @@ vi.mock("./AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase",
"execute": vi.fn()
}));
-vi.mock("./AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService", () => ({
- "execute": vi.fn((attachment, r, g, b, a, loadOp) => ({
- "colorAttachments": [{
- "view": attachment.texture?.view,
- "clearValue": { r, g, b, a },
- "loadOp": loadOp,
- "storeOp": "store"
- }]
- }))
-}));
describe("AttachmentManager", () =>
{
@@ -65,13 +55,6 @@ describe("AttachmentManager", () =>
expect(manager).toBeDefined();
});
- it("should initialize with null current attachment", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
-
- expect(manager.getCurrentAttachment()).toBeNull();
- });
});
describe("getAttachmentObject", () =>
@@ -119,71 +102,6 @@ describe("AttachmentManager", () =>
});
});
- describe("bindAttachment", () =>
- {
- it("should set current attachment", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
- const attachment = manager.getAttachmentObject(100, 100);
-
- manager.bindAttachment(attachment);
-
- expect(manager.getCurrentAttachment()).toBe(attachment);
- });
- });
-
- describe("getCurrentAttachment", () =>
- {
- it("should return null before binding", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
-
- expect(manager.getCurrentAttachment()).toBeNull();
- });
-
- it("should return bound attachment", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
- const attachment = manager.getAttachmentObject(100, 100);
-
- manager.bindAttachment(attachment);
-
- expect(manager.getCurrentAttachment()).toBe(attachment);
- });
- });
-
- describe("currentAttachmentObject", () =>
- {
- it("should return same as getCurrentAttachment", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
- const attachment = manager.getAttachmentObject(100, 100);
-
- manager.bindAttachment(attachment);
-
- expect(manager.currentAttachmentObject).toBe(manager.getCurrentAttachment());
- });
- });
-
- describe("unbindAttachment", () =>
- {
- it("should set current attachment to null", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
- const attachment = manager.getAttachmentObject(100, 100);
-
- manager.bindAttachment(attachment);
- manager.unbindAttachment();
-
- expect(manager.getCurrentAttachment()).toBeNull();
- });
- });
-
describe("releaseAttachment", () =>
{
it("should release attachment back to pool", () =>
@@ -196,51 +114,6 @@ describe("AttachmentManager", () =>
});
});
- describe("createRenderPassDescriptor", () =>
- {
- it("should create descriptor with clear color", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
- const attachment = manager.getAttachmentObject(100, 100);
-
- const descriptor = manager.createRenderPassDescriptor(
- attachment, 0.5, 0.5, 0.5, 1.0, "clear"
- );
-
- expect(descriptor.colorAttachments).toBeDefined();
- expect((descriptor.colorAttachments as any)[0].clearValue).toEqual({
- "r": 0.5, "g": 0.5, "b": 0.5, "a": 1.0
- });
- });
-
- it("should use clear as default loadOp", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
- const attachment = manager.getAttachmentObject(100, 100);
-
- const descriptor = manager.createRenderPassDescriptor(
- attachment, 0, 0, 0, 0
- );
-
- expect((descriptor.colorAttachments as any)[0].loadOp).toBe("clear");
- });
-
- it("should accept load as loadOp", () =>
- {
- const device = createMockDevice();
- const manager = new AttachmentManager(device);
- const attachment = manager.getAttachmentObject(100, 100);
-
- const descriptor = manager.createRenderPassDescriptor(
- attachment, 0, 0, 0, 0, "load"
- );
-
- expect((descriptor.colorAttachments as any)[0].loadOp).toBe("load");
- });
- });
-
describe("dispose", () =>
{
it("should not throw when disposing empty manager", () =>
diff --git a/packages/webgpu/src/AttachmentManager.ts b/packages/webgpu/src/AttachmentManager.ts
index 107ab1ba..95d2c1fd 100644
--- a/packages/webgpu/src/AttachmentManager.ts
+++ b/packages/webgpu/src/AttachmentManager.ts
@@ -4,8 +4,11 @@ import type { IColorBufferObject } from "./interface/IColorBufferObject";
import type { IStencilBufferObject } from "./interface/IStencilBufferObject";
import { execute as attachmentManagerGetAttachmentObjectUseCase } from "./AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase";
import { execute as attachmentManagerReleaseAttachmentUseCase } from "./AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase";
-import { execute as attachmentManagerCreateRenderPassDescriptorService } from "./AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService";
+/**
+ * @description アタッチメントリソースのプール管理クラス
+ * Pool manager class for attachment resources
+ */
export class AttachmentManager
{
private device: GPUDevice;
@@ -14,8 +17,12 @@ export class AttachmentManager
private colorBufferPool: IColorBufferObject[];
private stencilBufferPool: IStencilBufferObject[];
private idCounter: { attachmentId: number; textureId: number; stencilId: number };
- private currentAttachment: IAttachmentObject | null;
+ /**
+ * @description AttachmentManagerのコンストラクタ
+ * Constructor for AttachmentManager
+ * @param {GPUDevice} device - WebGPUデバイス / WebGPU device
+ */
constructor(device: GPUDevice)
{
this.device = device;
@@ -24,9 +31,16 @@ export class AttachmentManager
this.colorBufferPool = [];
this.stencilBufferPool = [];
this.idCounter = { "attachmentId": 0, "textureId": 0, "stencilId": 0 };
- this.currentAttachment = null;
}
+ /**
+ * @description プールからアタッチメントオブジェクトを取得する
+ * Get an attachment object from the pool
+ * @param {number} width - テクスチャの幅 / Texture width
+ * @param {number} height - テクスチャの高さ / Texture height
+ * @param {boolean} [msaa=false] - MSAAを有効にするか / Whether to enable MSAA
+ * @return {IAttachmentObject}
+ */
getAttachmentObject(
width: number,
height: number,
@@ -46,26 +60,12 @@ export class AttachmentManager
);
}
- bindAttachment(attachment: IAttachmentObject): void
- {
- this.currentAttachment = attachment;
- }
-
- getCurrentAttachment(): IAttachmentObject | null
- {
- return this.currentAttachment;
- }
-
- get currentAttachmentObject(): IAttachmentObject | null
- {
- return this.currentAttachment;
- }
-
- unbindAttachment(): void
- {
- this.currentAttachment = null;
- }
-
+ /**
+ * @description アタッチメントをプールに返却する
+ * Release an attachment back to the pool
+ * @param {IAttachmentObject} attachment - 返却するアタッチメント / Attachment to release
+ * @return {void}
+ */
releaseAttachment(attachment: IAttachmentObject): void
{
attachmentManagerReleaseAttachmentUseCase(
@@ -77,25 +77,11 @@ export class AttachmentManager
);
}
- createRenderPassDescriptor(
- attachment: IAttachmentObject,
- r: number,
- g: number,
- b: number,
- a: number,
- loadOp: GPULoadOp = "clear"
- ): GPURenderPassDescriptor
- {
- return attachmentManagerCreateRenderPassDescriptorService(
- attachment,
- r,
- g,
- b,
- a,
- loadOp
- );
- }
-
+ /**
+ * @description 全リソースを破棄する
+ * Dispose all resources
+ * @return {void}
+ */
dispose(): void
{
for (const pool of this.texturePool.values()) {
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts
index 5ca9deca..48b06f0a 100644
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts
+++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts
@@ -4,16 +4,16 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject";
* @description 新しいアタッチメントオブジェクトを作成
* Create a new attachment object
*
- * @param {{ attachmentId: number }} idCounter
+ * @param {{ attachmentId: number }} id_counter - ID管理カウンタ
* @return {IAttachmentObject}
* @method
* @protected
*/
export const execute = (
- idCounter: { attachmentId: number }
+ id_counter: { attachmentId: number }
): IAttachmentObject => {
return {
- "id": idCounter.attachmentId++,
+ "id": id_counter.attachmentId++,
"width": 0,
"height": 0,
"clipLevel": 0,
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts
index 38cc6932..8a21184f 100644
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts
+++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts
@@ -5,10 +5,10 @@ import type { IStencilBufferObject } from "../../interface/IStencilBufferObject"
* @description カラーバッファを新規作成
* Create a new color buffer
*
- * @param {GPUDevice} device
- * @param {number} width
- * @param {number} height
- * @param {IStencilBufferObject} stencil
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {number} width - バッファ幅
+ * @param {number} height - バッファ高さ
+ * @param {IStencilBufferObject} stencil - 関連するステンシルバッファ
* @return {IColorBufferObject}
* @method
* @protected
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts
deleted file mode 100644
index 7cc3038c..00000000
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { describe, it, expect } from "vitest";
-import type { IAttachmentObject } from "../../interface/IAttachmentObject";
-import { execute } from "./AttachmentManagerCreateRenderPassDescriptorService";
-
-describe("AttachmentManagerCreateRenderPassDescriptorService", () =>
-{
- const createMockAttachment = (
- hasColorView: boolean = true,
- hasTextureView: boolean = false,
- hasStencil: boolean = false
- ): IAttachmentObject => ({
- "id": 1,
- "width": 256,
- "height": 256,
- "color": hasColorView ? { "view": { "label": "colorView" } } : null,
- "texture": hasTextureView ? { "view": { "label": "textureView" } } : null,
- "stencil": hasStencil ? { "view": { "label": "stencilView" } } : null
- } as unknown as IAttachmentObject);
-
- describe("color attachments", () =>
- {
- it("should use color.view when available", () =>
- {
- const attachment = createMockAttachment(true, false, false);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.colorAttachments[0].view).toEqual({ "label": "colorView" });
- });
-
- it("should fallback to texture.view when color.view is not available", () =>
- {
- const attachment = createMockAttachment(false, true, false);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.colorAttachments[0].view).toEqual({ "label": "textureView" });
- });
-
- it("should throw error when no color view available", () =>
- {
- const attachment = createMockAttachment(false, false, false);
-
- expect(() => execute(attachment, 0, 0, 0, 1, "clear")).toThrow(
- "No color view available for render pass"
- );
- });
-
- it("should set clearValue with provided RGBA values", () =>
- {
- const attachment = createMockAttachment(true);
-
- const result = execute(attachment, 0.5, 0.6, 0.7, 0.8, "clear");
-
- expect(result.colorAttachments[0].clearValue).toEqual({
- "r": 0.5,
- "g": 0.6,
- "b": 0.7,
- "a": 0.8
- });
- });
-
- it("should set loadOp to provided value", () =>
- {
- const attachment = createMockAttachment(true);
-
- const result = execute(attachment, 0, 0, 0, 1, "load");
-
- expect(result.colorAttachments[0].loadOp).toBe("load");
- });
-
- it("should default loadOp to clear", () =>
- {
- const attachment = createMockAttachment(true);
-
- const result = execute(attachment, 0, 0, 0, 1);
-
- expect(result.colorAttachments[0].loadOp).toBe("clear");
- });
-
- it("should set storeOp to store", () =>
- {
- const attachment = createMockAttachment(true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.colorAttachments[0].storeOp).toBe("store");
- });
- });
-
- describe("depth stencil attachment", () =>
- {
- it("should include depthStencilAttachment when stencil is available", () =>
- {
- const attachment = createMockAttachment(true, false, true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment).toBeDefined();
- });
-
- it("should not include depthStencilAttachment when stencil is not available", () =>
- {
- const attachment = createMockAttachment(true, false, false);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment).toBeUndefined();
- });
-
- it("should set stencil view correctly", () =>
- {
- const attachment = createMockAttachment(true, false, true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment?.view).toEqual({ "label": "stencilView" });
- });
-
- it("should set depth clear value to 1.0", () =>
- {
- const attachment = createMockAttachment(true, false, true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment?.depthClearValue).toBe(1.0);
- });
-
- it("should set stencil clear value to 0", () =>
- {
- const attachment = createMockAttachment(true, false, true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment?.stencilClearValue).toBe(0);
- });
-
- it("should set depthLoadOp to clear", () =>
- {
- const attachment = createMockAttachment(true, false, true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment?.depthLoadOp).toBe("clear");
- });
-
- it("should set depthStoreOp to store", () =>
- {
- const attachment = createMockAttachment(true, false, true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment?.depthStoreOp).toBe("store");
- });
-
- it("should set stencilLoadOp to clear", () =>
- {
- const attachment = createMockAttachment(true, false, true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment?.stencilLoadOp).toBe("clear");
- });
-
- it("should set stencilStoreOp to store", () =>
- {
- const attachment = createMockAttachment(true, false, true);
-
- const result = execute(attachment, 0, 0, 0, 1, "clear");
-
- expect(result.depthStencilAttachment?.stencilStoreOp).toBe("store");
- });
- });
-});
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts
deleted file mode 100644
index 77628233..00000000
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { IAttachmentObject } from "../../interface/IAttachmentObject";
-
-const $clearValue: GPUColorDict = { "r": 0, "g": 0, "b": 0, "a": 0 };
-const $colorAttachment: GPURenderPassColorAttachment = {
- "view": null as unknown as GPUTextureView,
- "loadOp": "clear",
- "storeOp": "store",
- "clearValue": $clearValue
-};
-const $depthStencilAttachment: GPURenderPassDepthStencilAttachment = {
- "view": null as unknown as GPUTextureView,
- "depthLoadOp": "clear",
- "depthStoreOp": "store",
- "depthClearValue": 1.0,
- "stencilLoadOp": "clear",
- "stencilStoreOp": "store",
- "stencilClearValue": 0
-};
-const $descriptor: GPURenderPassDescriptor = {
- "colorAttachments": [$colorAttachment]
-};
-
-/**
- * @description レンダーパスディスクリプタを作成(プリアロケート再利用)
- */
-export const execute = (
- attachment: IAttachmentObject,
- r: number,
- g: number,
- b: number,
- a: number,
- loadOp: GPULoadOp = "clear"
-): GPURenderPassDescriptor => {
- const colorView = attachment.color?.view ?? attachment.texture?.view;
- if (!colorView) {
- throw new Error("No color view available for render pass");
- }
- $colorAttachment.view = colorView;
- $colorAttachment.loadOp = loadOp;
- $clearValue.r = r;
- $clearValue.g = g;
- $clearValue.b = b;
- $clearValue.a = a;
- if (attachment.stencil?.view) {
- $depthStencilAttachment.view = attachment.stencil.view;
- $descriptor.depthStencilAttachment = $depthStencilAttachment;
- } else {
- $descriptor.depthStencilAttachment = undefined;
- }
- return $descriptor;
-};
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts
index 367d1639..3cf886ff 100644
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts
+++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts
@@ -4,10 +4,10 @@ import type { IStencilBufferObject } from "../../interface/IStencilBufferObject"
* @description ステンシルバッファを新規作成
* Create a new stencil buffer
*
- * @param {GPUDevice} device
- * @param {number} width
- * @param {number} height
- * @param {{ stencilId: number }} idCounter
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {number} width - バッファ幅
+ * @param {number} height - バッファ高さ
+ * @param {{ stencilId: number }} id_counter - ID管理カウンタ
* @return {IStencilBufferObject}
* @method
* @protected
@@ -16,7 +16,7 @@ export const execute = (
device: GPUDevice,
width: number,
height: number,
- idCounter: { stencilId: number }
+ id_counter: { stencilId: number }
): IStencilBufferObject => {
const texture = device.createTexture({
"size": { width, height },
@@ -25,7 +25,7 @@ export const execute = (
});
return {
- "id": idCounter.stencilId++,
+ "id": id_counter.stencilId++,
"resource": texture,
"view": texture.createView(),
width,
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts
index 5b03d7d7..115ca073 100644
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts
+++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts
@@ -4,11 +4,11 @@ import type { ITextureObject } from "../../interface/ITextureObject";
* @description テクスチャオブジェクトを新規作成
* Create a new texture object
*
- * @param {GPUDevice} device
- * @param {number} width
- * @param {number} height
- * @param {boolean} smooth
- * @param {{ textureId: number }} idCounter
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {number} width - テクスチャ幅
+ * @param {number} height - テクスチャ高さ
+ * @param {boolean} smooth - スムーズフィルタリングの有効フラグ
+ * @param {{ textureId: number }} id_counter - ID管理カウンタ
* @return {ITextureObject}
* @method
* @protected
@@ -18,7 +18,7 @@ export const execute = (
width: number,
height: number,
smooth: boolean,
- idCounter: { textureId: number }
+ id_counter: { textureId: number }
): ITextureObject => {
const texture = device.createTexture({
"size": { width, height },
@@ -32,7 +32,7 @@ export const execute = (
const view = texture.createView();
return {
- "id": idCounter.textureId++,
+ "id": id_counter.textureId++,
"resource": texture,
view,
width,
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts
index d43ed5c0..07e571e6 100644
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts
+++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts
@@ -6,27 +6,27 @@ import { execute as attachmentManagerCreateColorBufferService } from "./Attachme
* @description カラーバッファを取得(プールから再利用または新規作成)
* Get color buffer from pool or create new one
*
- * @param {GPUDevice} device
- * @param {IColorBufferObject[]} colorBufferPool
- * @param {number} width
- * @param {number} height
- * @param {IStencilBufferObject} stencil
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール
+ * @param {number} width - バッファ幅
+ * @param {number} height - バッファ高さ
+ * @param {IStencilBufferObject} stencil - 関連するステンシルバッファ
* @return {IColorBufferObject}
* @method
* @protected
*/
export const execute = (
device: GPUDevice,
- colorBufferPool: IColorBufferObject[],
+ color_buffer_pool: IColorBufferObject[],
width: number,
height: number,
stencil: IStencilBufferObject
): IColorBufferObject => {
// プールから適切なサイズのものを検索
- for (let i = 0; i < colorBufferPool.length; i++) {
- const buffer = colorBufferPool[i];
+ for (let i = 0; i < color_buffer_pool.length; i++) {
+ const buffer = color_buffer_pool[i];
if (buffer.width >= width && buffer.height >= height) {
- colorBufferPool.splice(i, 1);
+ color_buffer_pool.splice(i, 1);
buffer.stencil = stencil;
buffer.dirty = false;
return buffer;
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts
index b49cc1c4..70b5fdfb 100644
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts
+++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts
@@ -5,32 +5,32 @@ import { execute as attachmentManagerCreateStencilBufferService } from "./Attach
* @description ステンシルバッファを取得(プールから再利用または新規作成)
* Get stencil buffer from pool or create new one
*
- * @param {GPUDevice} device
- * @param {IStencilBufferObject[]} stencilBufferPool
- * @param {number} width
- * @param {number} height
- * @param {{ stencilId: number }} idCounter
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール
+ * @param {number} width - バッファ幅
+ * @param {number} height - バッファ高さ
+ * @param {{ stencilId: number }} id_counter - ID管理カウンタ
* @return {IStencilBufferObject}
* @method
* @protected
*/
export const execute = (
device: GPUDevice,
- stencilBufferPool: IStencilBufferObject[],
+ stencil_buffer_pool: IStencilBufferObject[],
width: number,
height: number,
- idCounter: { stencilId: number }
+ id_counter: { stencilId: number }
): IStencilBufferObject => {
// プールから適切なサイズのものを検索
- for (let i = 0; i < stencilBufferPool.length; i++) {
- const buffer = stencilBufferPool[i];
+ for (let i = 0; i < stencil_buffer_pool.length; i++) {
+ const buffer = stencil_buffer_pool[i];
if (buffer.width >= width && buffer.height >= height) {
- stencilBufferPool.splice(i, 1);
+ stencil_buffer_pool.splice(i, 1);
buffer.dirty = false;
return buffer;
}
}
// 新規作成
- return attachmentManagerCreateStencilBufferService(device, width, height, idCounter);
+ return attachmentManagerCreateStencilBufferService(device, width, height, id_counter);
};
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts
index 300464bf..3c9f5608 100644
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts
+++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts
@@ -5,34 +5,34 @@ import { execute as attachmentManagerCreateTextureObjectService } from "./Attach
* @description テクスチャオブジェクトを取得(プールから再利用または新規作成)
* Get texture object from pool or create new one
*
- * @param {GPUDevice} device
- * @param {Map} texturePool
- * @param {number} width
- * @param {number} height
- * @param {boolean} smooth
- * @param {{ textureId: number }} idCounter
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {Map} texture_pool - テクスチャプール
+ * @param {number} width - テクスチャ幅
+ * @param {number} height - テクスチャ高さ
+ * @param {boolean} smooth - スムーズフィルタリングの有効フラグ
+ * @param {{ textureId: number }} id_counter - ID管理カウンタ
* @return {ITextureObject}
* @method
* @protected
*/
export const execute = (
device: GPUDevice,
- texturePool: Map,
+ texture_pool: Map,
width: number,
height: number,
smooth: boolean,
- idCounter: { textureId: number }
+ id_counter: { textureId: number }
): ITextureObject => {
const key = `${width}x${height}_${smooth ? "smooth" : "nearest"}`;
// プールから再利用
- if (texturePool.has(key)) {
- const pool = texturePool.get(key)!;
+ if (texture_pool.has(key)) {
+ const pool = texture_pool.get(key)!;
if (pool.length > 0) {
return pool.pop()!;
}
}
// 新規作成
- return attachmentManagerCreateTextureObjectService(device, width, height, smooth, idCounter);
+ return attachmentManagerCreateTextureObjectService(device, width, height, smooth, id_counter);
};
diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts
index f202edda..0627bdc4 100644
--- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts
+++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts
@@ -4,21 +4,21 @@ import type { ITextureObject } from "../../interface/ITextureObject";
* @description テクスチャをプールに返却
* Release texture back to pool
*
- * @param {Map} texturePool
- * @param {ITextureObject} textureObject
+ * @param {Map} texture_pool - テクスチャプール
+ * @param {ITextureObject} texture_object - 返却するテクスチャオブジェクト
* @return {void}
* @method
* @protected
*/
export const execute = (
- texturePool: Map,
- textureObject: ITextureObject
+ texture_pool: Map,
+ texture_object: ITextureObject
): void => {
- const key = `${textureObject.width}x${textureObject.height}_${textureObject.smooth ? "smooth" : "nearest"}`;
+ const key = `${texture_object.width}x${texture_object.height}_${texture_object.smooth ? "smooth" : "nearest"}`;
- if (!texturePool.has(key)) {
- texturePool.set(key, []);
+ if (!texture_pool.has(key)) {
+ texture_pool.set(key, []);
}
- texturePool.get(key)!.push(textureObject);
+ texture_pool.get(key)!.push(texture_object);
};
diff --git a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts
index a6fb60ae..85a614ec 100644
--- a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts
+++ b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts
@@ -11,34 +11,34 @@ import { execute as attachmentManagerGetTextureService } from "../service/Attach
* @description アタッチメントオブジェクトを取得
* Get attachment object
*
- * @param {GPUDevice} device
- * @param {IAttachmentObject[]} attachmentPool
- * @param {Map} texturePool
- * @param {IColorBufferObject[]} colorBufferPool
- * @param {IStencilBufferObject[]} stencilBufferPool
- * @param {number} width
- * @param {number} height
- * @param {boolean} msaa
- * @param {{ attachmentId: number, textureId: number, stencilId: number }} idCounter
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {IAttachmentObject[]} attachment_pool - アタッチメントプール
+ * @param {Map} texture_pool - テクスチャプール
+ * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール
+ * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール
+ * @param {number} width - バッファ幅
+ * @param {number} height - バッファ高さ
+ * @param {boolean} msaa - MSAA有効フラグ
+ * @param {{ attachmentId: number, textureId: number, stencilId: number }} id_counter - ID管理カウンタ
* @return {IAttachmentObject}
* @method
* @protected
*/
export const execute = (
device: GPUDevice,
- attachmentPool: IAttachmentObject[],
- texturePool: Map,
- colorBufferPool: IColorBufferObject[],
- stencilBufferPool: IStencilBufferObject[],
+ attachment_pool: IAttachmentObject[],
+ texture_pool: Map,
+ color_buffer_pool: IColorBufferObject[],
+ stencil_buffer_pool: IStencilBufferObject[],
width: number,
height: number,
msaa: boolean,
- idCounter: { attachmentId: number; textureId: number; stencilId: number }
+ id_counter: { attachmentId: number; textureId: number; stencilId: number }
): IAttachmentObject => {
// プールから再利用
- const attachment = attachmentPool.length > 0
- ? attachmentPool.pop()!
- : attachmentManagerCreateAttachmentObjectService(idCounter);
+ const attachment = attachment_pool.length > 0
+ ? attachment_pool.pop()!
+ : attachmentManagerCreateAttachmentObjectService(id_counter);
// サイズとフラグを更新
attachment.width = width;
@@ -50,16 +50,16 @@ export const execute = (
// ステンシルバッファを取得または作成
const stencil = attachmentManagerGetStencilBufferService(
device,
- stencilBufferPool,
+ stencil_buffer_pool,
width,
height,
- idCounter
+ id_counter
);
// カラーバッファを取得または作成(ステンシルを参照)
const color = attachmentManagerGetColorBufferService(
device,
- colorBufferPool,
+ color_buffer_pool,
width,
height,
stencil
@@ -70,11 +70,11 @@ export const execute = (
// テクスチャを取得
const texture = attachmentManagerGetTextureService(
device,
- texturePool,
+ texture_pool,
width,
height,
true,
- idCounter
+ id_counter
);
attachment.texture = texture;
diff --git a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts
index 6987a867..551edda5 100644
--- a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts
+++ b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts
@@ -8,40 +8,40 @@ import { execute as attachmentManagerReleaseTextureService } from "../service/At
* @description アタッチメントを解放してプールに返却
* Release attachment and return to pool
*
- * @param {IAttachmentObject[]} attachmentPool
- * @param {Map} texturePool
- * @param {IColorBufferObject[]} colorBufferPool
- * @param {IStencilBufferObject[]} stencilBufferPool
- * @param {IAttachmentObject} attachment
+ * @param {IAttachmentObject[]} attachment_pool - アタッチメントプール
+ * @param {Map} texture_pool - テクスチャプール
+ * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール
+ * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール
+ * @param {IAttachmentObject} attachment - 解放するアタッチメント
* @return {void}
* @method
* @protected
*/
export const execute = (
- attachmentPool: IAttachmentObject[],
- texturePool: Map,
- colorBufferPool: IColorBufferObject[],
- stencilBufferPool: IStencilBufferObject[],
+ attachment_pool: IAttachmentObject[],
+ texture_pool: Map,
+ color_buffer_pool: IColorBufferObject[],
+ stencil_buffer_pool: IStencilBufferObject[],
attachment: IAttachmentObject
): void => {
// テクスチャをプールに返却
if (attachment.texture) {
- attachmentManagerReleaseTextureService(texturePool, attachment.texture);
+ attachmentManagerReleaseTextureService(texture_pool, attachment.texture);
attachment.texture = null;
}
// カラーバッファをプールに返却
if (attachment.color) {
- colorBufferPool.push(attachment.color);
+ color_buffer_pool.push(attachment.color);
attachment.color = null;
}
// ステンシルバッファをプールに返却
if (attachment.stencil) {
- stencilBufferPool.push(attachment.stencil);
+ stencil_buffer_pool.push(attachment.stencil);
attachment.stencil = null;
}
// アタッチメントをプールに返却
- attachmentPool.push(attachment);
+ attachment_pool.push(attachment);
};
diff --git a/packages/webgpu/src/BezierConverter/BezierConverter.ts b/packages/webgpu/src/BezierConverter/BezierConverter.ts
index 80c670a4..942221e4 100644
--- a/packages/webgpu/src/BezierConverter/BezierConverter.ts
+++ b/packages/webgpu/src/BezierConverter/BezierConverter.ts
@@ -10,11 +10,16 @@
*
* これにより品質を維持しながら不要な計算を削減。
*/
+/**
+ * @description 三次ベジェ曲線を適応的に二次ベジェ曲線群に変換する関数
+ * Function to adaptively convert cubic bezier to quadratic bezier segments
+ */
export {
- execute as adaptiveCubicToQuad,
- calculateAdaptiveThreshold
+ execute as adaptiveCubicToQuad
} from "./usecase/BezierConverterAdaptiveCubicToQuadUseCase";
-export type { IQuadraticSegment } from "../interface/IQuadraticSegment";
-export { execute as calculateFlatness } from "./service/BezierConverterCalculateFlatnessService";
-export { execute as splitCubic } from "./service/BezierConverterSplitCubicService";
+/**
+ * @description 二次ベジェ曲線セグメントのインターフェース
+ * Interface for quadratic bezier curve segment
+ */
+export type { IQuadraticSegment } from "../interface/IQuadraticSegment";
diff --git a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts
index c31e4d84..42a04cce 100644
--- a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts
+++ b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
-import { execute, calculateAdaptiveThreshold } from "./BezierConverterAdaptiveCubicToQuadUseCase";
+import { execute } from "./BezierConverterAdaptiveCubicToQuadUseCase";
describe("BezierConverterAdaptiveCubicToQuadUseCase", () =>
{
@@ -83,38 +83,3 @@ describe("BezierConverterAdaptiveCubicToQuadUseCase", () =>
}
});
});
-
-describe("calculateAdaptiveThreshold", () =>
-{
- it("should return smaller threshold for larger scale", () =>
- {
- const threshold1 = calculateAdaptiveThreshold(1.0);
- const threshold2 = calculateAdaptiveThreshold(2.0);
-
- expect(threshold2).toBeLessThan(threshold1);
- });
-
- it("should return larger threshold for smaller scale", () =>
- {
- const threshold1 = calculateAdaptiveThreshold(1.0);
- const threshold2 = calculateAdaptiveThreshold(0.5);
-
- expect(threshold2).toBeGreaterThan(threshold1);
- });
-
- it("should clamp to minimum threshold", () =>
- {
- // 非常に大きなスケールでも最小値を下回らない
- // 最小値は0.0625(0.25px squared)
- const threshold = calculateAdaptiveThreshold(100.0);
- expect(threshold).toBeGreaterThanOrEqual(0.0625);
- });
-
- it("should clamp to maximum threshold", () =>
- {
- // 非常に小さなスケールでも最大値を超えない
- // 最大値は4.0(2px squared)
- const threshold = calculateAdaptiveThreshold(0.01);
- expect(threshold).toBeLessThanOrEqual(4.0);
- });
-});
diff --git a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts
index 0072c066..aecbeb74 100644
--- a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts
+++ b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts
@@ -32,7 +32,7 @@ const MAX_RECURSION_DEPTH = 8;
* @param {IPoint} p1 - 制御点1
* @param {IPoint} p2 - 制御点2
* @param {IPoint} p3 - 終点
- * @param {number} flatnessThreshold - フラットネス閾値(オプション)
+ * @param {number} flatness_threshold - フラットネス閾値(オプション)
* @return {IQuadraticSegment[]} 二次ベジェ曲線のセグメント配列
*/
export const execute = (
@@ -40,12 +40,21 @@ export const execute = (
p1: IPoint,
p2: IPoint,
p3: IPoint,
- flatnessThreshold: number = DEFAULT_FLATNESS_THRESHOLD
+ flatness_threshold: number = DEFAULT_FLATNESS_THRESHOLD
): IQuadraticSegment[] => {
const result: IQuadraticSegment[] = [];
- // 再帰的に分割を行う内部関数
+ /**
+ * @description 再帰的に三次ベジェ曲線を分割し、二次ベジェ曲線に近似する内部関数
+ * Internal recursive function that subdivides cubic bezier and approximates with quadratic bezier
+ * @param {IPoint} start - 始点
+ * @param {IPoint} ctrl1 - 制御点1
+ * @param {IPoint} ctrl2 - 制御点2
+ * @param {IPoint} end - 終点
+ * @param {number} depth - 現在の再帰深度
+ * @return {void}
+ */
const subdivide = (
start: IPoint,
ctrl1: IPoint,
@@ -58,7 +67,7 @@ export const execute = (
const flatness = calculateFlatness(start, ctrl1, ctrl2, end);
// フラットネスが閾値以下、または最大深度に達した場合は近似
- if (flatness <= flatnessThreshold || depth >= MAX_RECURSION_DEPTH) {
+ if (flatness <= flatness_threshold || depth >= MAX_RECURSION_DEPTH) {
// 三次ベジェを二次ベジェに近似
// WebGL版と同じ: 分割後は単純に2つの制御点の中点を使用
const ctrl: IPoint = {
@@ -99,27 +108,3 @@ export const execute = (
return result;
};
-
-/**
- * @description スケールに応じたフラットネス閾値を計算
- * Calculate flatness threshold based on scale
- *
- * ズームレベルが高い場合は高品質な近似が必要。
- * スケール = sqrt(matrix[0]^2 + matrix[1]^2) などで計算可能。
- *
- * @param {number} scale - 現在のスケール
- * @return {number} 調整されたフラットネス閾値
- */
-export const calculateAdaptiveThreshold = (scale: number): number => {
- // スケールが大きい場合は閾値を小さくして高品質に
- // スケールが小さい場合は閾値を大きくしてパフォーマンス優先
- const baseThreshold = DEFAULT_FLATNESS_THRESHOLD;
-
- // スケールの逆数に比例した閾値
- // 最小値と最大値を設定して極端な値を防ぐ
- const adjustedThreshold = baseThreshold / (scale * scale);
-
- // 閾値の範囲を制限(0.0625〜4.0)
- // 0.0625 = 0.25px squared, 4.0 = 2px squared
- return Math.max(0.0625, Math.min(4.0, adjustedThreshold));
-};
diff --git a/packages/webgpu/src/Blend.test.ts b/packages/webgpu/src/Blend.test.ts
index 53359038..3d80fa6a 100644
--- a/packages/webgpu/src/Blend.test.ts
+++ b/packages/webgpu/src/Blend.test.ts
@@ -2,8 +2,6 @@ import { describe, it, expect, beforeEach } from "vitest";
import {
$setCurrentBlendMode,
$currentBlendMode,
- $setFuncCode,
- $funcCode,
$getBlendState
} from "./Blend";
@@ -12,7 +10,6 @@ describe("Blend", () =>
beforeEach(() =>
{
$setCurrentBlendMode("normal");
- $setFuncCode(0);
});
describe("blend mode", () =>
@@ -42,24 +39,6 @@ describe("Blend", () =>
});
});
- describe("func code", () =>
- {
- it("should default to 0", () =>
- {
- $setFuncCode(0);
- expect($funcCode).toBe(0);
- });
-
- it("should set and get func code", () =>
- {
- $setFuncCode(123);
- expect($funcCode).toBe(123);
-
- $setFuncCode(456);
- expect($funcCode).toBe(456);
- });
- });
-
describe("$getBlendState", () =>
{
it("should return normal blend state", () =>
diff --git a/packages/webgpu/src/Blend.ts b/packages/webgpu/src/Blend.ts
index 2dfe18c8..e0ae0925 100644
--- a/packages/webgpu/src/Blend.ts
+++ b/packages/webgpu/src/Blend.ts
@@ -3,20 +3,33 @@ import type { IBlendState } from "./interface/IBlendState";
export type { IBlendState };
+/**
+ * @description 現在のブレンドモード
+ * The current blend mode used for rendering
+ *
+ * @type {IBlendMode}
+ */
export let $currentBlendMode: IBlendMode = "normal";
-export let $funcCode: number = 0;
-
+/**
+ * @description 現在のブレンドモードを設定する
+ * Set the current blend mode
+ *
+ * @param {IBlendMode} blend_mode - ブレンドモード / blend mode to apply
+ * @return {void}
+ */
export const $setCurrentBlendMode = (blend_mode: IBlendMode): void =>
{
$currentBlendMode = blend_mode;
};
-export const $setFuncCode = (code: number): void =>
-{
- $funcCode = code;
-};
-
+/**
+ * @description 指定されたブレンドモードに対応するWebGPUブレンドステートを返す
+ * Returns the WebGPU blend state configuration for the given blend mode
+ *
+ * @param {IBlendMode} mode - ブレンドモード / blend mode
+ * @return {IBlendState}
+ */
export const $getBlendState = (mode: IBlendMode): IBlendState =>
{
switch (mode) {
diff --git a/packages/webgpu/src/Blend/BlendInstancedManager.ts b/packages/webgpu/src/Blend/BlendInstancedManager.ts
index 8d9438d7..6aad5917 100644
--- a/packages/webgpu/src/Blend/BlendInstancedManager.ts
+++ b/packages/webgpu/src/Blend/BlendInstancedManager.ts
@@ -8,28 +8,37 @@ import { $context } from "../WebGPUUtil";
/**
* @description シンプルなブレンドモード(インスタンス描画可能)
+ * Simple blend modes that support instanced rendering
+ * @type {ReadonlySet}
*/
-const SIMPLE_BLEND_MODES: ReadonlySet = new Set([
+const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([
"normal", "layer", "add", "screen", "alpha", "erase", "copy"
]);
/**
* @description 複雑なブレンドモード描画キュー
+ * Queue for complex blend mode draw items
+ * @type {IComplexBlendItem[]}
*/
const $complexBlendQueue: IComplexBlendItem[] = [];
/**
* @description Float32Array(8) プール(color_transform 用)
+ * Object pool for Float32Array(8) used by color transforms
+ * @type {Float32Array[]}
*/
const $ct8Pool: Float32Array[] = [];
/**
* @description Float32Array(9) プール(matrix 用)
+ * Object pool for Float32Array(9) used by matrices
+ * @type {Float32Array[]}
*/
const $m9Pool: Float32Array[] = [];
/**
* @description 複雑なブレンドモードの描画キューを取得
+ * Returns the queue of complex blend mode draw items
* @return {IComplexBlendItem[]}
*/
export const getComplexBlendQueue = (): IComplexBlendItem[] =>
@@ -38,7 +47,8 @@ export const getComplexBlendQueue = (): IComplexBlendItem[] =>
};
/**
- * @description 複雑なブレンドモードの描画キューをクリア
+ * @description 複雑なブレンドモードの描画キューをクリアし、プールへ返却する
+ * Clears the complex blend queue and returns arrays to their pools
* @return {void}
*/
export const clearComplexBlendQueue = (): void =>
@@ -54,37 +64,40 @@ export const clearComplexBlendQueue = (): void =>
/**
* @description インスタンスシェーダーマネージャーのキャッシュ
- * @private
+ * Cache map for instanced shader managers
+ * @type {Map}
*/
-const shaderManagers = new Map();
+const $shaderManagers = new Map();
/**
- * @description インスタンスシェーダーマネージャーを取得
+ * @description インスタンスシェーダーマネージャーを取得(なければ生成)
+ * Gets or creates the instanced shader manager
* @return {ShaderInstancedManager}
*/
export const getInstancedShaderManager = (): ShaderInstancedManager =>
{
const key = "blend_instanced";
- if (!shaderManagers.has(key)) {
- shaderManagers.set(key, new ShaderInstancedManager());
+ if (!$shaderManagers.has(key)) {
+ $shaderManagers.set(key, new ShaderInstancedManager());
}
- return shaderManagers.get(key)!;
+ return $shaderManagers.get(key)!;
};
/**
- * @description DisplayObject単体の描画をインスタンス配列に追加
- * @param {Node} node
- * @param {number} x_min
- * @param {number} y_min
- * @param {number} x_max
- * @param {number} y_max
- * @param {Float32Array} color_transform
- * @param {Float32Array} matrix
- * @param {string} blend_mode
- * @param {number} viewport_width
- * @param {number} viewport_height
- * @param {number} render_max_size
- * @param {number} global_alpha
+ * @description DisplayObject単体の描画をインスタンス配列に追加する
+ * Adds a single DisplayObject's draw data to the instanced array
+ * @param {Node} node - テクスチャアトラスノード / Texture atlas node
+ * @param {number} x_min - バウンディングボックス左端 / Bounding box left edge
+ * @param {number} y_min - バウンディングボックス上端 / Bounding box top edge
+ * @param {number} x_max - バウンディングボックス右端 / Bounding box right edge
+ * @param {number} y_max - バウンディングボックス下端 / Bounding box bottom edge
+ * @param {Float32Array} color_transform - カラートランスフォーム配列 / Color transform array
+ * @param {Float32Array} matrix - 変換行列配列 / Transformation matrix array
+ * @param {string} blend_mode - ブレンドモード名 / Blend mode name
+ * @param {number} viewport_width - ビューポート幅 / Viewport width
+ * @param {number} viewport_height - ビューポート高さ / Viewport height
+ * @param {number} render_max_size - レンダーテクスチャ最大サイズ / Render texture max size
+ * @param {number} global_alpha - グローバルアルファ値 / Global alpha value
* @return {void}
*/
export const addDisplayObjectToInstanceArray = (
@@ -112,7 +125,7 @@ export const addDisplayObjectToInstanceArray = (
const ct6 = color_transform[6] / 255;
const ct7 = 0;
- if (SIMPLE_BLEND_MODES.has(blend_mode)) {
+ if ($SIMPLE_BLEND_MODES.has(blend_mode)) {
// ブレンドモードまたはアトラスインデックスが変わった場合
if ($currentBlendMode !== blend_mode || $getCurrentAtlasIndex() !== node.index) {
// 異なるブレンドモード/アトラスになるので、切り替え前にバッチを描画
diff --git a/packages/webgpu/src/Blend/service/BlendAddService.test.ts b/packages/webgpu/src/Blend/service/BlendAddService.test.ts
deleted file mode 100644
index 02261765..00000000
--- a/packages/webgpu/src/Blend/service/BlendAddService.test.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { describe, it, expect, beforeEach } from "vitest";
-import { execute } from "./BlendAddService";
-import { $setFuncCode, $funcCode } from "../../Blend";
-
-describe("BlendAddService", () =>
-{
- beforeEach(() =>
- {
- // Reset to non-add state
- $setFuncCode(0);
- });
-
- it("should return true when func code is different", () =>
- {
- $setFuncCode(0);
-
- const result = execute();
-
- expect(result).toBe(true);
- });
-
- it("should set func code to 101 (add)", () =>
- {
- $setFuncCode(0);
-
- execute();
-
- expect($funcCode).toBe(101);
- });
-
- it("should return false when already set to add (101)", () =>
- {
- $setFuncCode(101);
-
- const result = execute();
-
- expect(result).toBe(false);
- });
-
- it("should not change func code when already 101", () =>
- {
- $setFuncCode(101);
-
- execute();
-
- expect($funcCode).toBe(101);
- });
-
- it("should return true when changing from another mode", () =>
- {
- $setFuncCode(301); // screen mode
-
- const result = execute();
-
- expect(result).toBe(true);
- expect($funcCode).toBe(101);
- });
-
- it("should return true when changing from normal mode", () =>
- {
- $setFuncCode(613); // normal mode
-
- const result = execute();
-
- expect(result).toBe(true);
- });
-});
diff --git a/packages/webgpu/src/Blend/service/BlendAddService.ts b/packages/webgpu/src/Blend/service/BlendAddService.ts
deleted file mode 100644
index 07affca6..00000000
--- a/packages/webgpu/src/Blend/service/BlendAddService.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
- $setFuncCode,
- $funcCode
-} from "../../Blend";
-
-export const execute = (): boolean =>
-{
- if ($funcCode !== 101) {
- $setFuncCode(101);
- return true;
- }
- return false;
-};
diff --git a/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts b/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts
deleted file mode 100644
index 56edb947..00000000
--- a/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { describe, it, expect, beforeEach } from "vitest";
-import { execute } from "./BlendAlphaService";
-import { $setFuncCode, $funcCode } from "../../Blend";
-
-describe("BlendAlphaService", () =>
-{
- beforeEach(() =>
- {
- $setFuncCode(0);
- });
-
- it("should return true when func code is different", () =>
- {
- $setFuncCode(0);
-
- const result = execute();
-
- expect(result).toBe(true);
- });
-
- it("should set func code to 401 (alpha)", () =>
- {
- $setFuncCode(0);
-
- execute();
-
- expect($funcCode).toBe(401);
- });
-
- it("should return false when already set to alpha (401)", () =>
- {
- $setFuncCode(401);
-
- const result = execute();
-
- expect(result).toBe(false);
- });
-
- it("should not change func code when already 401", () =>
- {
- $setFuncCode(401);
-
- execute();
-
- expect($funcCode).toBe(401);
- });
-
- it("should return true when changing from normal mode", () =>
- {
- $setFuncCode(613); // normal mode
-
- const result = execute();
-
- expect(result).toBe(true);
- expect($funcCode).toBe(401);
- });
-});
diff --git a/packages/webgpu/src/Blend/service/BlendAlphaService.ts b/packages/webgpu/src/Blend/service/BlendAlphaService.ts
deleted file mode 100644
index 7f5617aa..00000000
--- a/packages/webgpu/src/Blend/service/BlendAlphaService.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
- $setFuncCode,
- $funcCode
-} from "../../Blend";
-
-export const execute = (): boolean =>
-{
- if ($funcCode !== 401) {
- $setFuncCode(401);
- return true;
- }
- return false;
-};
diff --git a/packages/webgpu/src/Blend/service/BlendEraseService.test.ts b/packages/webgpu/src/Blend/service/BlendEraseService.test.ts
deleted file mode 100644
index 196dd414..00000000
--- a/packages/webgpu/src/Blend/service/BlendEraseService.test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { describe, it, expect, beforeEach } from "vitest";
-import { execute } from "./BlendEraseService";
-import { $setFuncCode, $funcCode } from "../../Blend";
-
-describe("BlendEraseService", () =>
-{
- beforeEach(() =>
- {
- $setFuncCode(0);
- });
-
- it("should return true when func code is different", () =>
- {
- $setFuncCode(0);
-
- const result = execute();
-
- expect(result).toBe(true);
- });
-
- it("should set func code to 501 (erase)", () =>
- {
- $setFuncCode(0);
-
- execute();
-
- expect($funcCode).toBe(501);
- });
-
- it("should return false when already set to erase (501)", () =>
- {
- $setFuncCode(501);
-
- const result = execute();
-
- expect(result).toBe(false);
- });
-
- it("should not change func code when already 501", () =>
- {
- $setFuncCode(501);
-
- execute();
-
- expect($funcCode).toBe(501);
- });
-
- it("should return true when changing from screen mode", () =>
- {
- $setFuncCode(301); // screen mode
-
- const result = execute();
-
- expect(result).toBe(true);
- expect($funcCode).toBe(501);
- });
-});
diff --git a/packages/webgpu/src/Blend/service/BlendEraseService.ts b/packages/webgpu/src/Blend/service/BlendEraseService.ts
deleted file mode 100644
index 86ee87fd..00000000
--- a/packages/webgpu/src/Blend/service/BlendEraseService.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
- $setFuncCode,
- $funcCode
-} from "../../Blend";
-
-export const execute = (): boolean =>
-{
- if ($funcCode !== 501) {
- $setFuncCode(501);
- return true;
- }
- return false;
-};
diff --git a/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts b/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts
deleted file mode 100644
index dde24480..00000000
--- a/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { describe, it, expect } from "vitest";
-import { execute } from "./BlendGetStateService";
-
-describe("BlendGetStateService", () =>
-{
- describe("normal blend mode", () =>
- {
- it("should return correct blend state for normal mode", () =>
- {
- const result = execute("normal");
-
- expect(result.color.srcFactor).toBe("one");
- expect(result.color.dstFactor).toBe("one-minus-src-alpha");
- expect(result.color.operation).toBe("add");
- expect(result.alpha.srcFactor).toBe("one");
- expect(result.alpha.dstFactor).toBe("one-minus-src-alpha");
- expect(result.alpha.operation).toBe("add");
- });
- });
-
- describe("add blend mode", () =>
- {
- it("should return correct blend state for add mode", () =>
- {
- const result = execute("add");
-
- expect(result.color.srcFactor).toBe("one");
- expect(result.color.dstFactor).toBe("one");
- expect(result.color.operation).toBe("add");
- expect(result.alpha.srcFactor).toBe("one");
- expect(result.alpha.dstFactor).toBe("one-minus-src-alpha");
- });
- });
-
- describe("screen blend mode", () =>
- {
- it("should return correct blend state for screen mode", () =>
- {
- const result = execute("screen");
-
- expect(result.color.srcFactor).toBe("one-minus-dst");
- expect(result.color.dstFactor).toBe("one");
- expect(result.color.operation).toBe("add");
- });
- });
-
- describe("alpha blend mode", () =>
- {
- it("should return correct blend state for alpha mode", () =>
- {
- const result = execute("alpha");
-
- expect(result.color.srcFactor).toBe("zero");
- expect(result.color.dstFactor).toBe("src-alpha");
- expect(result.color.operation).toBe("add");
- expect(result.alpha.srcFactor).toBe("zero");
- expect(result.alpha.dstFactor).toBe("src-alpha");
- });
- });
-
- describe("erase blend mode", () =>
- {
- it("should return correct blend state for erase mode", () =>
- {
- const result = execute("erase");
-
- expect(result.color.srcFactor).toBe("zero");
- expect(result.color.dstFactor).toBe("one-minus-src-alpha");
- expect(result.color.operation).toBe("add");
- expect(result.alpha.srcFactor).toBe("zero");
- expect(result.alpha.dstFactor).toBe("one-minus-src-alpha");
- });
- });
-
- describe("copy blend mode", () =>
- {
- it("should return correct blend state for copy mode", () =>
- {
- const result = execute("copy");
-
- expect(result.color.srcFactor).toBe("one");
- expect(result.color.dstFactor).toBe("zero");
- expect(result.color.operation).toBe("add");
- expect(result.alpha.srcFactor).toBe("one");
- expect(result.alpha.dstFactor).toBe("zero");
- });
- });
-
- describe("default behavior", () =>
- {
- it("should return normal state for unknown mode", () =>
- {
- const result = execute("unknown" as any);
-
- expect(result.color.srcFactor).toBe("one");
- expect(result.color.dstFactor).toBe("one-minus-src-alpha");
- });
- });
-});
diff --git a/packages/webgpu/src/Blend/service/BlendGetStateService.ts b/packages/webgpu/src/Blend/service/BlendGetStateService.ts
deleted file mode 100644
index 5f8535eb..00000000
--- a/packages/webgpu/src/Blend/service/BlendGetStateService.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { IBlendMode } from "../../interface/IBlendMode";
-import type { IBlendState } from "../../Blend";
-import { $getBlendState } from "../../Blend";
-
-/**
- * @description ブレンドモードからWebGPUブレンドステートを取得するサービス
- * Service to get WebGPU blend state from blend mode
- *
- * @param {IBlendMode} mode
- * @return {IBlendState}
- * @method
- * @protected
- */
-export const execute = (mode: IBlendMode): IBlendState =>
-{
- return $getBlendState(mode);
-};
diff --git a/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts b/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts
deleted file mode 100644
index 4b4083fe..00000000
--- a/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { describe, it, expect, beforeEach } from "vitest";
-import { execute } from "./BlendOneZeroService";
-import { $setFuncCode, $funcCode } from "../../Blend";
-
-describe("BlendOneZeroService", () =>
-{
- beforeEach(() =>
- {
- $setFuncCode(0);
- });
-
- it("should return true when func code is different", () =>
- {
- $setFuncCode(100);
-
- const result = execute();
-
- expect(result).toBe(true);
- });
-
- it("should set func code to 10 (copy/one-zero)", () =>
- {
- $setFuncCode(100);
-
- execute();
-
- expect($funcCode).toBe(10);
- });
-
- it("should return false when already set to one-zero (10)", () =>
- {
- $setFuncCode(10);
-
- const result = execute();
-
- expect(result).toBe(false);
- });
-
- it("should not change func code when already 10", () =>
- {
- $setFuncCode(10);
-
- execute();
-
- expect($funcCode).toBe(10);
- });
-
- it("should return true when changing from normal mode", () =>
- {
- $setFuncCode(613); // normal mode
-
- const result = execute();
-
- expect(result).toBe(true);
- expect($funcCode).toBe(10);
- });
-});
diff --git a/packages/webgpu/src/Blend/service/BlendOneZeroService.ts b/packages/webgpu/src/Blend/service/BlendOneZeroService.ts
deleted file mode 100644
index 108cac13..00000000
--- a/packages/webgpu/src/Blend/service/BlendOneZeroService.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
- $setFuncCode,
- $funcCode
-} from "../../Blend";
-
-export const execute = (): boolean =>
-{
- if ($funcCode !== 10) {
- $setFuncCode(10);
- return true;
- }
- return false;
-};
diff --git a/packages/webgpu/src/Blend/service/BlendResetService.test.ts b/packages/webgpu/src/Blend/service/BlendResetService.test.ts
deleted file mode 100644
index c9ccf537..00000000
--- a/packages/webgpu/src/Blend/service/BlendResetService.test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { describe, it, expect, beforeEach } from "vitest";
-import { execute } from "./BlendResetService";
-import { $setFuncCode, $funcCode } from "../../Blend";
-
-describe("BlendResetService", () =>
-{
- beforeEach(() =>
- {
- $setFuncCode(0);
- });
-
- it("should return true when func code is not 613 (normal)", () =>
- {
- $setFuncCode(101); // add mode
-
- const result = execute();
-
- expect(result).toBe(true);
- });
-
- it("should set func code to 613 (normal)", () =>
- {
- $setFuncCode(101); // add mode
-
- execute();
-
- expect($funcCode).toBe(613);
- });
-
- it("should return false when already set to normal (613)", () =>
- {
- $setFuncCode(613);
-
- const result = execute();
-
- expect(result).toBe(false);
- });
-
- it("should not change func code when already 613", () =>
- {
- $setFuncCode(613);
-
- execute();
-
- expect($funcCode).toBe(613);
- });
-
- it("should return true when resetting from screen mode", () =>
- {
- $setFuncCode(301); // screen mode
-
- const result = execute();
-
- expect(result).toBe(true);
- expect($funcCode).toBe(613);
- });
-
- it("should return true when resetting from erase mode", () =>
- {
- $setFuncCode(501); // erase mode
-
- const result = execute();
-
- expect(result).toBe(true);
- expect($funcCode).toBe(613);
- });
-
- it("should return true when resetting from one-zero mode", () =>
- {
- $setFuncCode(10); // one-zero (copy) mode
-
- const result = execute();
-
- expect(result).toBe(true);
- expect($funcCode).toBe(613);
- });
-});
diff --git a/packages/webgpu/src/Blend/service/BlendResetService.ts b/packages/webgpu/src/Blend/service/BlendResetService.ts
deleted file mode 100644
index aff3f081..00000000
--- a/packages/webgpu/src/Blend/service/BlendResetService.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
- $setFuncCode,
- $funcCode
-} from "../../Blend";
-
-export const execute = (): boolean =>
-{
- if ($funcCode !== 613) {
- $setFuncCode(613);
- return true;
- }
- return false;
-};
diff --git a/packages/webgpu/src/Blend/service/BlendScreenService.test.ts b/packages/webgpu/src/Blend/service/BlendScreenService.test.ts
deleted file mode 100644
index ad266f30..00000000
--- a/packages/webgpu/src/Blend/service/BlendScreenService.test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { describe, it, expect, beforeEach } from "vitest";
-import { execute } from "./BlendScreenService";
-import { $setFuncCode, $funcCode } from "../../Blend";
-
-describe("BlendScreenService", () =>
-{
- beforeEach(() =>
- {
- $setFuncCode(0);
- });
-
- it("should return true when func code is different", () =>
- {
- $setFuncCode(0);
-
- const result = execute();
-
- expect(result).toBe(true);
- });
-
- it("should set func code to 301 (screen)", () =>
- {
- $setFuncCode(0);
-
- execute();
-
- expect($funcCode).toBe(301);
- });
-
- it("should return false when already set to screen (301)", () =>
- {
- $setFuncCode(301);
-
- const result = execute();
-
- expect(result).toBe(false);
- });
-
- it("should not change func code when already 301", () =>
- {
- $setFuncCode(301);
-
- execute();
-
- expect($funcCode).toBe(301);
- });
-
- it("should return true when changing from add mode", () =>
- {
- $setFuncCode(101); // add mode
-
- const result = execute();
-
- expect(result).toBe(true);
- expect($funcCode).toBe(301);
- });
-});
diff --git a/packages/webgpu/src/Blend/service/BlendScreenService.ts b/packages/webgpu/src/Blend/service/BlendScreenService.ts
deleted file mode 100644
index 53acf82c..00000000
--- a/packages/webgpu/src/Blend/service/BlendScreenService.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {
- $setFuncCode,
- $funcCode
-} from "../../Blend";
-
-export const execute = (): boolean =>
-{
- if ($funcCode !== 301) {
- $setFuncCode(301);
- return true;
- }
- return false;
-};
diff --git a/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts b/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts
deleted file mode 100644
index b8b0d3f5..00000000
--- a/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { describe, it, expect, beforeEach } from "vitest";
-import { execute } from "./BlendSetModeService";
-import { $currentBlendMode, $setCurrentBlendMode } from "../../Blend";
-
-describe("BlendSetModeService", () =>
-{
- beforeEach(() =>
- {
- $setCurrentBlendMode("normal");
- });
-
- it("should set blend mode to normal", () =>
- {
- execute("normal");
-
- expect($currentBlendMode).toBe("normal");
- });
-
- it("should set blend mode to add", () =>
- {
- execute("add");
-
- expect($currentBlendMode).toBe("add");
- });
-
- it("should set blend mode to screen", () =>
- {
- execute("screen");
-
- expect($currentBlendMode).toBe("screen");
- });
-
- it("should set blend mode to alpha", () =>
- {
- execute("alpha");
-
- expect($currentBlendMode).toBe("alpha");
- });
-
- it("should set blend mode to erase", () =>
- {
- execute("erase");
-
- expect($currentBlendMode).toBe("erase");
- });
-
- it("should set blend mode to copy", () =>
- {
- execute("copy");
-
- expect($currentBlendMode).toBe("copy");
- });
-
- it("should change mode from one to another", () =>
- {
- execute("add");
- expect($currentBlendMode).toBe("add");
-
- execute("screen");
- expect($currentBlendMode).toBe("screen");
-
- execute("normal");
- expect($currentBlendMode).toBe("normal");
- });
-});
diff --git a/packages/webgpu/src/Blend/service/BlendSetModeService.ts b/packages/webgpu/src/Blend/service/BlendSetModeService.ts
deleted file mode 100644
index 3162d9f5..00000000
--- a/packages/webgpu/src/Blend/service/BlendSetModeService.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { IBlendMode } from "../../interface/IBlendMode";
-import { $setCurrentBlendMode } from "../../Blend";
-
-export const execute = (mode: IBlendMode): void =>
-{
- $setCurrentBlendMode(mode);
-};
diff --git a/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts b/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts
index 8c7e5c5f..4f4c4ecc 100644
--- a/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts
+++ b/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts
@@ -4,11 +4,15 @@ import { ShaderSource } from "../../Shader/ShaderSource";
/**
* @description プリアロケートされた uniform データ (12 floats = 48 bytes)
+ * Pre-allocated uniform data array (12 floats = 48 bytes)
+ * @type {Float32Array}
*/
const $uniform12 = new Float32Array(12);
/**
* @description プリアロケートされた BindGroupEntry 配列 (4 bindings)
+ * Pre-allocated BindGroupEntry array (4 bindings)
+ * @type {GPUBindGroupEntry[]}
*/
const $entries4: GPUBindGroupEntry[] = [
{ "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
@@ -18,21 +22,28 @@ const $entries4: GPUBindGroupEntry[] = [
];
/**
- * @description 複雑なブレンドモードを適用
+ * @description 複雑なブレンドモードを適用し、ブレンド結果のアタッチメントを返す
+ * Applies a complex blend mode and returns the resulting attachment
+ * @param {IAttachmentObject} src_attachment - ソースアタッチメント / Source attachment
+ * @param {IAttachmentObject} dst_attachment - デスティネーションアタッチメント / Destination attachment
+ * @param {string} blend_mode - ブレンドモード名 / Blend mode name
+ * @param {Float32Array} color_transform - カラートランスフォーム配列 / Color transform array
+ * @param {IFilterConfig} config - フィルター設定 / Filter configuration
+ * @return {IAttachmentObject}
*/
export const execute = (
- srcAttachment: IAttachmentObject,
- dstAttachment: IAttachmentObject,
- blendMode: string,
- colorTransform: Float32Array,
+ src_attachment: IAttachmentObject,
+ dst_attachment: IAttachmentObject,
+ blend_mode: string,
+ color_transform: Float32Array,
config: IFilterConfig
): IAttachmentObject => {
const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config;
// 出力サイズは両方の大きい方を使用
- const width = Math.max(srcAttachment.width, dstAttachment.width);
- const height = Math.max(srcAttachment.height, dstAttachment.height);
+ const width = Math.max(src_attachment.width, dst_attachment.width);
+ const height = Math.max(src_attachment.height, dst_attachment.height);
// 出力アタッチメントを作成
const destAttachment = frameBufferManager.createTemporaryAttachment(width, height);
@@ -42,9 +53,9 @@ export const execute = (
const bindGroupLayout = pipelineManager.getBindGroupLayout("complex_blend");
if (!pipeline || !bindGroupLayout) {
- console.error(`[WebGPU ComplexBlend] Pipeline not found for blend mode: ${blendMode}`);
+ console.error(`[WebGPU ComplexBlend] Pipeline not found for blend mode: ${blend_mode}`);
// フォールバック: srcをそのまま返す
- return srcAttachment;
+ return src_attachment;
}
// サンプラーを作成
@@ -55,15 +66,15 @@ export const execute = (
// addColor: vec4 (16 bytes)
// blendMode: f32 + padding: vec3 (16 bytes)
// Total: 48 bytes
- const blendModeIndex = ShaderSource.getBlendModeIndex(blendMode);
- $uniform12[0] = colorTransform[0];
- $uniform12[1] = colorTransform[1];
- $uniform12[2] = colorTransform[2];
- $uniform12[3] = colorTransform[3];
- $uniform12[4] = colorTransform[4];
- $uniform12[5] = colorTransform[5];
- $uniform12[6] = colorTransform[6];
- $uniform12[7] = colorTransform[7];
+ const blendModeIndex = ShaderSource.getBlendModeIndex(blend_mode);
+ $uniform12[0] = color_transform[0];
+ $uniform12[1] = color_transform[1];
+ $uniform12[2] = color_transform[2];
+ $uniform12[3] = color_transform[3];
+ $uniform12[4] = color_transform[4];
+ $uniform12[5] = color_transform[5];
+ $uniform12[6] = color_transform[6];
+ $uniform12[7] = color_transform[7];
$uniform12[8] = blendModeIndex;
$uniform12[9] = 0;
$uniform12[10] = 0;
@@ -82,8 +93,8 @@ export const execute = (
// バインドグループを作成
($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries4[1].resource = sampler;
- $entries4[2].resource = dstAttachment.texture!.view;
- $entries4[3].resource = srcAttachment.texture!.view;
+ $entries4[2].resource = dst_attachment.texture!.view;
+ $entries4[3].resource = src_attachment.texture!.view;
const bindGroup = device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries4
diff --git a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts b/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts
deleted file mode 100644
index 3d74804c..00000000
--- a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { execute } from "./BlendOperationUseCase";
-import { describe, expect, it, beforeEach } from "vitest";
-import { $setFuncCode } from "../../Blend";
-
-describe("BlendOperationUseCase.ts method test", () =>
-{
- beforeEach(() =>
- {
- // Reset func code before each test
- $setFuncCode(0);
- });
-
- it("test case - add blend mode", () =>
- {
- const changed = execute("add");
- expect(changed).toBe(true);
-
- // Second call should return false (no change)
- const changed2 = execute("add");
- expect(changed2).toBe(false);
- });
-
- it("test case - screen blend mode", () =>
- {
- const changed = execute("screen");
- expect(changed).toBe(true);
-
- const changed2 = execute("screen");
- expect(changed2).toBe(false);
- });
-
- it("test case - alpha blend mode", () =>
- {
- const changed = execute("alpha");
- expect(changed).toBe(true);
-
- const changed2 = execute("alpha");
- expect(changed2).toBe(false);
- });
-
- it("test case - erase blend mode", () =>
- {
- const changed = execute("erase");
- expect(changed).toBe(true);
-
- const changed2 = execute("erase");
- expect(changed2).toBe(false);
- });
-
- it("test case - copy blend mode", () =>
- {
- const changed = execute("copy");
- expect(changed).toBe(true);
-
- const changed2 = execute("copy");
- expect(changed2).toBe(false);
- });
-
- it("test case - normal blend mode (default)", () =>
- {
- const changed = execute("normal");
- expect(changed).toBe(true);
-
- const changed2 = execute("normal");
- expect(changed2).toBe(false);
- });
-
- it("test case - switching between modes", () =>
- {
- execute("add");
- const changed = execute("screen");
- expect(changed).toBe(true);
-
- const changed2 = execute("normal");
- expect(changed2).toBe(true);
- });
-});
diff --git a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts b/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts
deleted file mode 100644
index d7101eb0..00000000
--- a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { IBlendMode } from "../../interface/IBlendMode";
-import { execute as blendAddService } from "../service/BlendAddService";
-import { execute as blendResetService } from "../service/BlendResetService";
-import { execute as blendScreenService } from "../service/BlendScreenService";
-import { execute as blendAlphaService } from "../service/BlendAlphaService";
-import { execute as blendEraseService } from "../service/BlendEraseService";
-import { execute as blendOneZeroService } from "../service/BlendOneZeroService";
-
-/**
- * @description 設定されたブレンドモードへ切り替える
- * Switch to the set blend mode
- *
- * @param {IBlendMode} operation
- * @return {boolean} ブレンドモードが変更されたかどうか
- * @method
- * @protected
- */
-export const execute = (operation: IBlendMode): boolean =>
-{
- switch (operation) {
-
- case "add":
- return blendAddService();
-
- case "screen":
- return blendScreenService();
-
- case "alpha":
- return blendAlphaService();
-
- case "erase":
- return blendEraseService();
-
- case "copy":
- return blendOneZeroService();
-
- default:
- return blendResetService();
-
- }
-};
diff --git a/packages/webgpu/src/BufferManager.test.ts b/packages/webgpu/src/BufferManager.test.ts
index a557244a..cef33579 100644
--- a/packages/webgpu/src/BufferManager.test.ts
+++ b/packages/webgpu/src/BufferManager.test.ts
@@ -59,13 +59,6 @@ vi.mock("./BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase", () =
})
}));
-vi.mock("./BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase", () => ({
- "execute": vi.fn((pool, buffer) => {
- const entry = pool.find((e: any) => e.buffer === buffer);
- if (entry) entry.inUse = false;
- })
-}));
-
vi.mock("./BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase", () => ({
"execute": vi.fn()
}));
@@ -110,121 +103,6 @@ describe("BufferManager", () =>
expect(manager).toBeDefined();
});
- it("should initialize with zero frame number", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- expect(manager.getFrameNumber()).toBe(0);
- });
-
- it("should initialize with empty pool stats", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- const stats = manager.getPoolStats();
- expect(stats.vertexPoolSize).toBe(0);
- expect(stats.uniformPoolSize).toBe(0);
- });
- });
-
- describe("createVertexBuffer", () =>
- {
- it("should create vertex buffer with data", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
- const data = new Float32Array([1, 2, 3, 4]);
-
- const buffer = manager.createVertexBuffer("test", data);
-
- expect(buffer).toBeDefined();
- expect(device.createBuffer).toHaveBeenCalled();
- });
-
- it("should store buffer by name", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
- const data = new Float32Array([1, 2, 3]);
-
- manager.createVertexBuffer("myBuffer", data);
- const retrieved = manager.getVertexBuffer("myBuffer");
-
- expect(retrieved).toBeDefined();
- });
- });
-
- describe("createUniformBuffer", () =>
- {
- it("should create uniform buffer with specified size", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- const buffer = manager.createUniformBuffer("uniforms", 64);
-
- expect(buffer).toBeDefined();
- expect(device.createBuffer).toHaveBeenCalled();
- });
-
- it("should store buffer by name", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.createUniformBuffer("myUniforms", 128);
- const retrieved = manager.getUniformBuffer("myUniforms");
-
- expect(retrieved).toBeDefined();
- });
- });
-
- describe("updateUniformBuffer", () =>
- {
- it("should write data to existing buffer", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
- const data = new Float32Array([1, 2, 3, 4]);
-
- manager.createUniformBuffer("uniforms", 64);
- manager.updateUniformBuffer("uniforms", data);
-
- expect(device.queue.writeBuffer).toHaveBeenCalled();
- });
-
- it("should not throw when buffer does not exist", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
- const data = new Float32Array([1, 2, 3, 4]);
-
- expect(() => manager.updateUniformBuffer("nonexistent", data)).not.toThrow();
- });
- });
-
- describe("getVertexBuffer", () =>
- {
- it("should return undefined for non-existent buffer", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- expect(manager.getVertexBuffer("nonexistent")).toBeUndefined();
- });
- });
-
- describe("getUniformBuffer", () =>
- {
- it("should return undefined for non-existent buffer", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- expect(manager.getUniformBuffer("nonexistent")).toBeUndefined();
- });
});
describe("createRectVertices", () =>
@@ -253,28 +131,6 @@ describe("BufferManager", () =>
expect(buffer).toBeDefined();
});
- it("should update pool stats", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.acquireVertexBuffer(256);
-
- const stats = manager.getPoolStats();
- expect(stats.vertexPoolSize).toBe(1);
- });
- });
-
- describe("releaseVertexBuffer", () =>
- {
- it("should release buffer back to pool", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
- const buffer = manager.acquireVertexBuffer(256);
-
- expect(() => manager.releaseVertexBuffer(buffer)).not.toThrow();
- });
});
describe("acquireUniformBuffer", () =>
@@ -289,85 +145,18 @@ describe("BufferManager", () =>
expect(buffer).toBeDefined();
});
- it("should update pool stats", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.acquireUniformBuffer(64);
-
- const stats = manager.getPoolStats();
- expect(stats.uniformPoolSize).toBe(1);
- });
- });
-
- describe("releaseUniformBuffer", () =>
- {
- it("should release buffer back to pool", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
- const buffer = manager.acquireUniformBuffer(64);
-
- expect(() => manager.releaseUniformBuffer(buffer)).not.toThrow();
- });
- });
-
- describe("destroyBuffer", () =>
- {
- it("should destroy vertex buffer by name", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.createVertexBuffer("test", new Float32Array([1, 2, 3]));
- manager.destroyBuffer("test");
-
- expect(manager.getVertexBuffer("test")).toBeUndefined();
- });
-
- it("should destroy uniform buffer by name", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.createUniformBuffer("test", 64);
- manager.destroyBuffer("test");
-
- expect(manager.getUniformBuffer("test")).toBeUndefined();
- });
});
describe("dispose", () =>
{
- it("should clear all buffers", () =>
+ it("should not throw when disposing", () =>
{
const device = createMockDevice();
const manager = new BufferManager(device);
- manager.createVertexBuffer("v1", new Float32Array([1, 2, 3]));
- manager.createUniformBuffer("u1", 64);
-
- manager.dispose();
-
- expect(manager.getVertexBuffer("v1")).toBeUndefined();
- expect(manager.getUniformBuffer("u1")).toBeUndefined();
+ expect(() => manager.dispose()).not.toThrow();
});
- it("should reset pool stats", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.acquireVertexBuffer(256);
- manager.acquireUniformBuffer(64);
-
- manager.dispose();
-
- const stats = manager.getPoolStats();
- expect(stats.vertexPoolSize).toBe(0);
- expect(stats.uniformPoolSize).toBe(0);
- });
});
describe("acquireStorageBuffer", () =>
@@ -382,29 +171,6 @@ describe("BufferManager", () =>
expect(buffer).toBeDefined();
});
- it("should update storage pool stats", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.acquireStorageBuffer(1024);
-
- const stats = manager.getStoragePoolStats();
- expect(stats.storagePoolSize).toBe(1);
- expect(stats.storagePoolInUse).toBe(1);
- });
- });
-
- describe("releaseStorageBuffer", () =>
- {
- it("should release storage buffer", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
- const buffer = manager.acquireStorageBuffer(1024);
-
- expect(() => manager.releaseStorageBuffer(buffer)).not.toThrow();
- });
});
describe("writeStorageBuffer", () =>
@@ -432,35 +198,18 @@ describe("BufferManager", () =>
manager.acquireStorageBuffer(256);
manager.acquireStorageBuffer(512);
- manager.releaseAllStorageBuffers();
-
- const stats = manager.getStoragePoolStats();
- expect(stats.storagePoolInUse).toBe(0);
+ expect(() => manager.releaseAllStorageBuffers()).not.toThrow();
});
});
describe("clearFrameBuffers", () =>
{
- it("should clear named buffers", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.createVertexBuffer("frame", new Float32Array([1, 2, 3]));
- manager.clearFrameBuffers();
-
- expect(manager.getVertexBuffer("frame")).toBeUndefined();
- });
-
- it("should increment frame number", () =>
+ it("should not throw when clearing", () =>
{
const device = createMockDevice();
const manager = new BufferManager(device);
- const beforeFrame = manager.getFrameNumber();
- manager.clearFrameBuffers();
-
- expect(manager.getFrameNumber()).toBe(beforeFrame + 1);
+ expect(() => manager.clearFrameBuffers()).not.toThrow();
});
it("should release all storage buffers", () =>
@@ -469,34 +218,7 @@ describe("BufferManager", () =>
const manager = new BufferManager(device);
manager.acquireStorageBuffer(256);
- manager.clearFrameBuffers();
-
- const stats = manager.getStoragePoolStats();
- expect(stats.storagePoolInUse).toBe(0);
- });
- });
-
- describe("getOrCreateIndirectBuffer", () =>
- {
- it("should create indirect buffer on first call", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- const buffer = manager.getOrCreateIndirectBuffer(6, 10);
-
- expect(buffer).toBeDefined();
- });
-
- it("should return same buffer on subsequent calls", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- const buffer1 = manager.getOrCreateIndirectBuffer(6, 10);
- const buffer2 = manager.getOrCreateIndirectBuffer(6, 20);
-
- expect(buffer1).toBe(buffer2);
+ expect(() => manager.clearFrameBuffers()).not.toThrow();
});
});
@@ -515,18 +237,4 @@ describe("BufferManager", () =>
});
});
- describe("getFrameNumber", () =>
- {
- it("should track frame count", () =>
- {
- const device = createMockDevice();
- const manager = new BufferManager(device);
-
- manager.clearFrameBuffers();
- manager.clearFrameBuffers();
- manager.clearFrameBuffers();
-
- expect(manager.getFrameNumber()).toBe(3);
- });
- });
});
diff --git a/packages/webgpu/src/BufferManager.ts b/packages/webgpu/src/BufferManager.ts
index 4b78913d..e037d27d 100644
--- a/packages/webgpu/src/BufferManager.ts
+++ b/packages/webgpu/src/BufferManager.ts
@@ -5,15 +5,13 @@ import { execute as bufferManagerAcquireUniformBufferUseCase } from "./BufferMan
import { execute as bufferManagerReleaseVertexBufferService } from "./BufferManager/service/BufferManagerReleaseVertexBufferService";
import { execute as bufferManagerReleaseUniformBufferService } from "./BufferManager/service/BufferManagerReleaseUniformBufferService";
import { execute as bufferManagerAcquireStorageBufferUseCase } from "./BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase";
-import { execute as releaseStorageBufferUseCase } from "./BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase";
import { execute as cleanupStorageBuffersUseCase } from "./BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase";
import { execute as bufferManagerCreateIndirectBufferService } from "./BufferManager/service/BufferManagerCreateIndirectBufferService";
import { execute as updateIndirectBuffer } from "./BufferManager/service/BufferManagerUpdateIndirectBufferService";
/**
- * @description Dynamic Uniform Buffer Allocator
- * 1フレーム内の全fill uniform データを1本の大バッファにサブアロケートし、
- * BindGroup作成を1回に削減する。
+ * @description 動的Uniformバッファアロケータ。1フレーム内の全uniformデータを1本の大バッファにサブアロケートし、BindGroup作成を1回に削減
+ * Dynamic Uniform Buffer Allocator. Sub-allocates all uniform data within a frame into a single large buffer, reducing BindGroup creation to once
*/
export class DynamicUniformAllocator
{
@@ -27,6 +25,12 @@ export class DynamicUniformAllocator
private stagingFloat32: Float32Array;
private dirtyEnd: number = 0;
+ /**
+ * @description コンストラクタ。GPUデバイスとバッファ容量を設定
+ * Constructor. Sets up GPU device and buffer capacity
+ * @param {GPUDevice} device - WebGPUデバイス
+ * @param {number} capacity - 初期バッファ容量(バイト単位、デフォルト: 65536)
+ */
constructor (device: GPUDevice, capacity: number = 65536)
{
this.device = device;
@@ -36,8 +40,9 @@ export class DynamicUniformAllocator
}
/**
- * @description フレーム開始時にオフセットをリセット
- * 前フレームの旧バッファを安全に破棄(submit済みのため)
+ * @description フレーム開始時にオフセットをリセットし、前フレームの旧バッファを安全に破棄
+ * Reset offset at frame start and safely destroy old buffers from previous frame
+ * @return {void}
*/
resetFrame (): void
{
@@ -52,6 +57,8 @@ export class DynamicUniformAllocator
/**
* @description バッファを取得(遅延生成)
+ * Get buffer with lazy initialization
+ * @return {GPUBuffer} GPUバッファ
*/
getBuffer (): GPUBuffer
{
@@ -65,10 +72,10 @@ export class DynamicUniformAllocator
}
/**
- * @description uniform データをCPUステージングバッファにコピーし、アライメント済みオフセットを返す
- * 実際のGPU書き込みはflush()で一括実行される
- * @param data - 書き込むデータ
- * @return アライメント済みオフセット(バイト単位)
+ * @description uniformデータをCPUステージングバッファにコピーし、アライメント済みオフセットを返す。実際のGPU書き込みはflush()で一括実行
+ * Copy uniform data to CPU staging buffer and return aligned offset. Actual GPU write is batched in flush()
+ * @param {Float32Array} data - 書き込むデータ
+ * @return {number} アライメント済みオフセット(バイト単位)
*/
allocate (data: Float32Array): number
{
@@ -118,8 +125,9 @@ export class DynamicUniformAllocator
}
/**
- * @description ステージングバッファの内容をGPUバッファに一括書き込み
- * submit前に1回だけ呼び出す
+ * @description ステージングバッファの内容をGPUバッファに一括書き込み。submit前に1回だけ呼び出す
+ * Flush staging buffer content to GPU buffer in bulk. Call once before submit
+ * @return {void}
*/
flush (): void
{
@@ -129,6 +137,11 @@ export class DynamicUniformAllocator
}
}
+ /**
+ * @description バッファを破棄してリソースを解放
+ * Dispose buffers and release resources
+ * @return {void}
+ */
dispose (): void
{
if (this.buffer) {
@@ -143,15 +156,16 @@ export class DynamicUniformAllocator
}
}
+/**
+ * @description GPUバッファの管理クラス。頂点・ユニフォーム・ストレージ・インダイレクトバッファのプール管理と再利用を提供
+ * GPU buffer management class. Provides pooling and reuse for vertex, uniform, storage, and indirect buffers
+ */
export class BufferManager
{
private device: GPUDevice;
- private vertexBuffers: Map;
- private uniformBuffers: Map;
private vertexBufferBuckets: Map;
private uniformBufferBuckets: Map;
private storageBufferPool: IPooledStorageBuffer[];
- private indirectBuffer: GPUBuffer | null;
private indirectBufferPool: GPUBuffer[];
private frameIndirectBuffers: GPUBuffer[];
private frameNumber: number;
@@ -160,15 +174,17 @@ export class BufferManager
private frameUniformPoolBuffers: GPUBuffer[];
readonly dynamicUniform: DynamicUniformAllocator;
+ /**
+ * @description コンストラクタ。GPUデバイスを設定し、各種バッファプールを初期化
+ * Constructor. Sets up GPU device and initializes buffer pools
+ * @param {GPUDevice} device - WebGPUデバイス
+ */
constructor (device: GPUDevice)
{
this.device = device;
- this.vertexBuffers = new Map();
- this.uniformBuffers = new Map();
this.vertexBufferBuckets = new Map();
this.uniformBufferBuckets = new Map();
this.storageBufferPool = [];
- this.indirectBuffer = null;
this.indirectBufferPool = [];
this.frameIndirectBuffers = [];
this.frameNumber = 0;
@@ -178,78 +194,51 @@ export class BufferManager
this.dynamicUniform = new DynamicUniformAllocator(device);
}
- createVertexBuffer (name: string, data: Float32Array): GPUBuffer
- {
- const buffer = this.device.createBuffer({
- "size": data.byteLength,
- "usage": GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
- "mappedAtCreation": true
- });
-
- new Float32Array(buffer.getMappedRange()).set(data);
- buffer.unmap();
-
- this.vertexBuffers.set(name, buffer);
- return buffer;
- }
-
- createUniformBuffer (name: string, size: number): GPUBuffer
- {
- const buffer = this.device.createBuffer({
- "size": size,
- "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
- });
-
- this.uniformBuffers.set(name, buffer);
- return buffer;
- }
-
- updateUniformBuffer (name: string, data: Float32Array): void
- {
- const buffer = this.uniformBuffers.get(name);
- if (buffer) {
- this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength);
- }
- }
-
- getVertexBuffer (name: string): GPUBuffer | undefined
- {
- return this.vertexBuffers.get(name);
- }
-
- getUniformBuffer (name: string): GPUBuffer | undefined
- {
- return this.uniformBuffers.get(name);
- }
-
+ /**
+ * @description 矩形の頂点データを作成
+ * Create rect vertices data
+ * @param {number} x - X座標
+ * @param {number} y - Y座標
+ * @param {number} width - 幅
+ * @param {number} height - 高さ
+ * @return {Float32Array} 矩形の頂点データ
+ */
createRectVertices (x: number, y: number, width: number, height: number): Float32Array
{
return bufferManagerCreateRectVerticesService(x, y, width, height);
}
- acquireVertexBuffer (requiredSize: number, data?: Float32Array): GPUBuffer
+ /**
+ * @description プールから頂点バッファを取得(または新規作成)
+ * Acquire vertex buffer from pool or create new one
+ * @param {number} required_size - 必要なバイトサイズ
+ * @param {Float32Array} [data] - 初期データ
+ * @return {GPUBuffer} 取得された頂点バッファ
+ */
+ acquireVertexBuffer (required_size: number, data?: Float32Array): GPUBuffer
{
const buffer = bufferManagerAcquireVertexBufferUseCase(
this.device,
this.vertexBufferBuckets,
- requiredSize,
+ required_size,
data
);
this.frameVertexPoolBuffers.push(buffer);
return buffer;
}
- releaseVertexBuffer (buffer: GPUBuffer): void
- {
- bufferManagerReleaseVertexBufferService(this.vertexBufferBuckets, buffer);
- }
-
- acquireUniformBuffer (requiredSize: number): GPUBuffer
+ /**
+ * @description プールからユニフォームバッファを取得(または新規作成)
+ * Acquire uniform buffer from pool or create new one
+ * @param {number} required_size - 必要なバイトサイズ
+ * @return {GPUBuffer} 取得されたユニフォームバッファ
+ */
+ acquireUniformBuffer (required_size: number): GPUBuffer
{
const buffer = bufferManagerAcquireUniformBufferUseCase(
this.device,
this.uniformBufferBuckets,
- requiredSize
+ required_size
);
this.frameUniformPoolBuffers.push(buffer);
return buffer;
@@ -257,51 +246,26 @@ export class BufferManager
/**
* @description Uniform Bufferの取得と書き込みを一括で行うヘルパー
- * acquireUniformBuffer + writeBuffer の2ステップを1呼び出しに統合
- * @param data - 書き込むデータ
- * @param byteLength - 書き込みバイト数(省略時はdata.byteLength)
- * @return GPUBuffer
+ * Helper to acquire and write uniform buffer in one call, combining acquireUniformBuffer + writeBuffer
+ * @param {Float32Array} data - 書き込むデータ
+ * @param {number} [byte_length] - 書き込みバイト数(省略時はdata.byteLength)
+ * @return {GPUBuffer} 取得されたユニフォームバッファ
*/
- acquireAndWriteUniformBuffer (data: Float32Array, byteLength?: number): GPUBuffer
+ acquireAndWriteUniformBuffer (data: Float32Array, byte_length?: number): GPUBuffer
{
- const writeBytes = byteLength ?? data.byteLength;
+ const writeBytes = byte_length ?? data.byteLength;
const buffer = this.acquireUniformBuffer(writeBytes);
this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, writeBytes);
return buffer;
}
- releaseUniformBuffer (buffer: GPUBuffer): void
- {
- bufferManagerReleaseUniformBufferService(this.uniformBufferBuckets, buffer);
- }
-
- destroyBuffer (name: string): void
- {
- const vertexBuffer = this.vertexBuffers.get(name);
- if (vertexBuffer) {
- vertexBuffer.destroy();
- this.vertexBuffers.delete(name);
- }
-
- const uniformBuffer = this.uniformBuffers.get(name);
- if (uniformBuffer) {
- uniformBuffer.destroy();
- this.uniformBuffers.delete(name);
- }
- }
-
+ /**
+ * @description 全バッファを破棄してリソースを解放
+ * Dispose all buffers and release resources
+ * @return {void}
+ */
dispose (): void
{
- for (const buffer of this.vertexBuffers.values()) {
- buffer.destroy();
- }
- this.vertexBuffers.clear();
-
- for (const buffer of this.uniformBuffers.values()) {
- buffer.destroy();
- }
- this.uniformBuffers.clear();
-
for (const bucket of this.vertexBufferBuckets.values()) {
for (const buffer of bucket) {
buffer.destroy();
@@ -321,11 +285,6 @@ export class BufferManager
}
this.storageBufferPool = [];
- if (this.indirectBuffer) {
- this.indirectBuffer.destroy();
- this.indirectBuffer = null;
- }
-
for (const buffer of this.indirectBufferPool) {
buffer.destroy();
}
@@ -347,34 +306,13 @@ export class BufferManager
this.dynamicUniform.dispose();
}
- getPoolStats (): { vertexPoolSize: number; uniformPoolSize: number }
- {
- let vertexCount = 0;
- for (const bucket of this.vertexBufferBuckets.values()) {
- vertexCount += bucket.length;
- }
- let uniformCount = 0;
- for (const bucket of this.uniformBufferBuckets.values()) {
- uniformCount += bucket.length;
- }
- return {
- "vertexPoolSize": vertexCount,
- "uniformPoolSize": uniformCount
- };
- }
-
+ /**
+ * @description フレーム内で使用したバッファをクリアし、プールに返却
+ * Clear frame buffers and return them to pool
+ * @return {void}
+ */
clearFrameBuffers (): void
{
- for (const buffer of this.vertexBuffers.values()) {
- buffer.destroy();
- }
- this.vertexBuffers.clear();
-
- for (const buffer of this.uniformBuffers.values()) {
- buffer.destroy();
- }
- this.uniformBuffers.clear();
-
// フレーム内で取得したプールバッファをプールに返却
for (const buffer of this.frameVertexPoolBuffers) {
bufferManagerReleaseVertexBufferService(this.vertexBufferBuckets, buffer);
@@ -403,6 +341,11 @@ export class BufferManager
}
}
+ /**
+ * @description 全てのStorage Bufferを未使用状態に戻す
+ * Mark all storage buffers as not in use
+ * @return {void}
+ */
releaseAllStorageBuffers (): void
{
for (const entry of this.storageBufferPool) {
@@ -410,82 +353,77 @@ export class BufferManager
}
}
- acquireStorageBuffer (requiredSize: number): GPUBuffer
+ /**
+ * @description プールからStorage Bufferを取得(または新規作成)
+ * Acquire storage buffer from pool or create new one
+ * @param {number} required_size - 必要なバイトサイズ
+ * @return {GPUBuffer} 取得されたStorage Buffer
+ */
+ acquireStorageBuffer (required_size: number): GPUBuffer
{
return bufferManagerAcquireStorageBufferUseCase(
this.device,
this.storageBufferPool,
- requiredSize,
+ required_size,
this.frameNumber
);
}
- releaseStorageBuffer (buffer: GPUBuffer): void
- {
- releaseStorageBufferUseCase(this.storageBufferPool, buffer);
- }
-
+ /**
+ * @description Storage Bufferにデータを書き込む
+ * Write data to storage buffer
+ * @param {GPUBuffer} buffer - 書き込み先バッファ
+ * @param {Float32Array | Uint32Array} data - 書き込むデータ
+ * @return {void}
+ */
writeStorageBuffer (buffer: GPUBuffer, data: Float32Array | Uint32Array): void
{
this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength);
}
- getOrCreateIndirectBuffer (
- vertexCount: number,
- instanceCount: number,
- firstVertex: number = 0,
- firstInstance: number = 0
- ): GPUBuffer {
- if (!this.indirectBuffer) {
- this.indirectBuffer = bufferManagerCreateIndirectBufferService(
- this.device,
- vertexCount,
- instanceCount,
- firstVertex,
- firstInstance
- );
- } else {
- updateIndirectBuffer(
- this.device,
- this.indirectBuffer,
- vertexCount,
- instanceCount,
- firstVertex,
- firstInstance
- );
- }
- return this.indirectBuffer;
- }
-
+ /**
+ * @description 新しいIndirect Bufferを作成(プールから再利用または新規作成)
+ * Create new indirect buffer (reuse from pool or create new)
+ * @param {number} vertex_count - 頂点数
+ * @param {number} instance_count - インスタンス数
+ * @param {number} first_vertex - 開始頂点インデックス
+ * @param {number} first_instance - 開始インスタンスインデックス
+ * @return {GPUBuffer} 作成されたIndirect Buffer
+ */
createIndirectBuffer (
- vertexCount: number,
- instanceCount: number,
- firstVertex: number = 0,
- firstInstance: number = 0
+ vertex_count: number,
+ instance_count: number,
+ first_vertex: number = 0,
+ first_instance: number = 0
): GPUBuffer {
let buffer = this.indirectBufferPool.pop();
if (buffer) {
updateIndirectBuffer(
this.device,
buffer,
- vertexCount,
- instanceCount,
- firstVertex,
- firstInstance
+ vertex_count,
+ instance_count,
+ first_vertex,
+ first_instance
);
} else {
buffer = bufferManagerCreateIndirectBufferService(
this.device,
- vertexCount,
- instanceCount,
- firstVertex,
- firstInstance
+ vertex_count,
+ instance_count,
+ first_vertex,
+ first_instance
);
}
this.frameIndirectBuffers.push(buffer);
return buffer;
}
+ /**
+ * @description 単位矩形(0,0,1,1)の頂点バッファを取得(遅延生成)
+ * Get unit rect (0,0,1,1) vertex buffer with lazy creation
+ * @return {GPUBuffer} 単位矩形の頂点バッファ
+ */
getUnitRectBuffer (): GPUBuffer
{
if (!this.unitRectBuffer) {
@@ -501,17 +439,4 @@ export class BufferManager
return this.unitRectBuffer;
}
- getFrameNumber (): number
- {
- return this.frameNumber;
- }
-
- getStoragePoolStats (): { storagePoolSize: number; storagePoolInUse: number }
- {
- const inUse = this.storageBufferPool.filter((e) => e.inUse).length;
- return {
- "storagePoolSize": this.storageBufferPool.length,
- "storagePoolInUse": inUse
- };
- }
}
diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts
index ea30eb88..c04b8bad 100644
--- a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts
+++ b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts
@@ -4,7 +4,7 @@
* @type {number}
* @const
*/
-const MAX_BUCKET_SIZE: number = 32;
+const $MAX_BUCKET_SIZE: number = 32;
/**
* @description ユニフォームバッファをプールに返却
@@ -29,7 +29,7 @@ export const execute = (
buckets.set(size, bucket);
}
- if (bucket.length >= MAX_BUCKET_SIZE) {
+ if (bucket.length >= $MAX_BUCKET_SIZE) {
// バケットが満杯の場合、このバッファを破棄
buffer.destroy();
return;
diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts
index 359e5922..a546046b 100644
--- a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts
+++ b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts
@@ -4,7 +4,7 @@
* @type {number}
* @const
*/
-const MAX_BUCKET_SIZE: number = 32;
+const $MAX_BUCKET_SIZE: number = 32;
/**
* @description 頂点バッファをプールに返却
@@ -29,7 +29,7 @@ export const execute = (
buckets.set(size, bucket);
}
- if (bucket.length >= MAX_BUCKET_SIZE) {
+ if (bucket.length >= $MAX_BUCKET_SIZE) {
// バケットが満杯の場合、このバッファを破棄
buffer.destroy();
return;
diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts
new file mode 100644
index 00000000..17443da7
--- /dev/null
+++ b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts
@@ -0,0 +1,74 @@
+import { execute } from "./BufferManagerUpdateIndirectBufferService";
+import { describe, expect, it, vi } from "vitest";
+
+describe("BufferManagerUpdateIndirectBufferService.js test", () => {
+
+ it("execute test case1", () =>
+ {
+ let writtenData: Uint32Array | null = null;
+ let writtenOffset: number = -1;
+
+ const mockBuffer = {} as unknown as GPUBuffer;
+ const mockDevice = {
+ "queue": {
+ "writeBuffer": vi.fn((buffer: GPUBuffer, offset: number, data: Uint32Array) => {
+ expect(buffer).toBe(mockBuffer);
+ writtenOffset = offset;
+ writtenData = new Uint32Array(data);
+ })
+ }
+ } as unknown as GPUDevice;
+
+ execute(mockDevice, mockBuffer, 12, 3, 0, 0);
+
+ expect(mockDevice.queue.writeBuffer).toHaveBeenCalledTimes(1);
+ expect(writtenOffset).toBe(0);
+ expect(writtenData).not.toBeNull();
+ expect(writtenData![0]).toBe(12);
+ expect(writtenData![1]).toBe(3);
+ expect(writtenData![2]).toBe(0);
+ expect(writtenData![3]).toBe(0);
+ });
+
+ it("execute test case2 - custom first_vertex and first_instance", () =>
+ {
+ let writtenData: Uint32Array | null = null;
+
+ const mockBuffer = {} as unknown as GPUBuffer;
+ const mockDevice = {
+ "queue": {
+ "writeBuffer": vi.fn((_buffer: GPUBuffer, _offset: number, data: Uint32Array) => {
+ writtenData = new Uint32Array(data);
+ })
+ }
+ } as unknown as GPUDevice;
+
+ execute(mockDevice, mockBuffer, 6, 1, 5, 10);
+
+ expect(writtenData![0]).toBe(6);
+ expect(writtenData![1]).toBe(1);
+ expect(writtenData![2]).toBe(5);
+ expect(writtenData![3]).toBe(10);
+ });
+
+ it("execute test case3 - default parameters", () =>
+ {
+ let writtenData: Uint32Array | null = null;
+
+ const mockBuffer = {} as unknown as GPUBuffer;
+ const mockDevice = {
+ "queue": {
+ "writeBuffer": vi.fn((_buffer: GPUBuffer, _offset: number, data: Uint32Array) => {
+ writtenData = new Uint32Array(data);
+ })
+ }
+ } as unknown as GPUDevice;
+
+ execute(mockDevice, mockBuffer, 100, 50);
+
+ expect(writtenData![0]).toBe(100);
+ expect(writtenData![1]).toBe(50);
+ expect(writtenData![2]).toBe(0);
+ expect(writtenData![3]).toBe(0);
+ });
+});
diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts
index 1ddec2ad..43fcf90f 100644
--- a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts
+++ b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts
@@ -8,6 +8,7 @@
* @param {number} instance_count - インスタンス数
* @param {number} first_vertex - 開始頂点インデックス
* @param {number} first_instance - 開始インスタンスインデックス
+ * @return {void}
*/
export const execute = (
device: GPUDevice,
diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts
index 2801ffc0..881f9766 100644
--- a/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts
+++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts
@@ -24,9 +24,21 @@ vi.mock("../service/BufferManagerCreateStorageBufferService", () => ({
}));
import { execute } from "./BufferManagerAcquireStorageBufferUseCase";
-import { execute as releaseStorageBuffer } from "./BufferManagerReleaseStorageBufferUseCase";
import { execute as cleanupStorageBuffers } from "./BufferManagerCleanupStorageBuffersUseCase";
+/**
+ * @description テスト用ヘルパー: Storage Bufferをプールに返却
+ * Test helper: Release storage buffer back to pool
+ */
+const releaseStorageBuffer = (pool: IPooledStorageBuffer[], buffer: GPUBuffer): void => {
+ for (const entry of pool) {
+ if (entry.buffer === buffer) {
+ entry.inUse = false;
+ return;
+ }
+ }
+};
+
describe("BufferManagerAcquireStorageBufferUseCase", () =>
{
let mockDevice: GPUDevice;
@@ -95,26 +107,6 @@ describe("BufferManagerAcquireStorageBufferUseCase", () =>
});
});
- describe("releaseStorageBuffer", () =>
- {
- it("should mark buffer as not in use", () =>
- {
- const buffer = execute(mockDevice, pool, 1024, 0);
- expect(pool[0].inUse).toBe(true);
-
- releaseStorageBuffer(pool, buffer);
- expect(pool[0].inUse).toBe(false);
- });
-
- it("should handle buffer not in pool", () =>
- {
- const fakeBuffer = {} as GPUBuffer;
-
- // Should not throw
- expect(() => releaseStorageBuffer(pool, fakeBuffer)).not.toThrow();
- });
- });
-
describe("cleanupStorageBuffers", () =>
{
it("should remove old unused buffers", () =>
diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts
new file mode 100644
index 00000000..4e572c54
--- /dev/null
+++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts
@@ -0,0 +1,63 @@
+import { execute } from "./BufferManagerCleanupStorageBuffersUseCase";
+import { describe, expect, it, vi } from "vitest";
+
+describe("BufferManagerCleanupStorageBuffersUseCase.js test", () => {
+
+ it("execute test case1 - should remove old unused buffers", () =>
+ {
+ const destroyFn = vi.fn();
+ const pool = [
+ { "buffer": { "destroy": destroyFn }, "inUse": false, "lastUsedFrame": 0, "size": 256 },
+ { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 0, "size": 256 },
+ { "buffer": { "destroy": vi.fn() }, "inUse": false, "lastUsedFrame": 50, "size": 256 }
+ ] as any;
+
+ execute(pool, 100, 60);
+
+ expect(pool.length).toBe(2);
+ expect(destroyFn).toHaveBeenCalledTimes(1);
+ });
+
+ it("execute test case2 - should not remove in-use buffers", () =>
+ {
+ const pool = [
+ { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 0, "size": 256 },
+ { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 10, "size": 256 }
+ ] as any;
+
+ execute(pool, 100, 60);
+
+ expect(pool.length).toBe(2);
+ });
+
+ it("execute test case3 - should not remove recently used buffers", () =>
+ {
+ const pool = [
+ { "buffer": { "destroy": vi.fn() }, "inUse": false, "lastUsedFrame": 95, "size": 256 }
+ ] as any;
+
+ execute(pool, 100, 60);
+
+ expect(pool.length).toBe(1);
+ });
+
+ it("execute test case4 - empty pool", () =>
+ {
+ const pool: any[] = [];
+ execute(pool, 100, 60);
+ expect(pool.length).toBe(0);
+ });
+
+ it("execute test case5 - default max_age", () =>
+ {
+ const destroyFn = vi.fn();
+ const pool = [
+ { "buffer": { "destroy": destroyFn }, "inUse": false, "lastUsedFrame": 0, "size": 256 }
+ ] as any;
+
+ execute(pool, 61);
+
+ expect(pool.length).toBe(0);
+ expect(destroyFn).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts
index 6304374c..6cb69ac7 100644
--- a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts
+++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts
@@ -9,6 +9,7 @@ import type { IPooledStorageBuffer } from "../../interface/IStorageBufferConfig"
* @param {IPooledStorageBuffer[]} pool - Storage Bufferプール
* @param {number} current_frame - 現在のフレーム番号
* @param {number} max_age - 最大保持フレーム数
+ * @return {void}
*/
export const execute = (
pool: IPooledStorageBuffer[],
diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts
deleted file mode 100644
index da94118e..00000000
--- a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { IPooledStorageBuffer } from "../../interface/IStorageBufferConfig";
-
-/**
- * @description Storage Bufferをプールに返却
- * Release Storage Buffer back to pool
- *
- * @param {IPooledStorageBuffer[]} pool - Storage Bufferプール
- * @param {GPUBuffer} buffer - 返却するバッファ
- */
-export const execute = (
- pool: IPooledStorageBuffer[],
- buffer: GPUBuffer
-): void => {
-
- for (const entry of pool) {
- if (entry.buffer === buffer) {
- entry.inUse = false;
- return;
- }
- }
-};
diff --git a/packages/webgpu/src/Compute/ComputePipelineManager.test.ts b/packages/webgpu/src/Compute/ComputePipelineManager.test.ts
deleted file mode 100644
index 62ba9cda..00000000
--- a/packages/webgpu/src/Compute/ComputePipelineManager.test.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-
-// Mock GPUShaderStage
-const GPUShaderStage = {
- COMPUTE: 0x04
-};
-(globalThis as any).GPUShaderStage = GPUShaderStage;
-
-describe("ComputePipelineManager", () =>
-{
- // Create a mock implementation for testing without the actual class
- class MockComputePipelineManager
- {
- private pipelines: Map;
- private bindGroupLayouts: Map;
-
- constructor (_device: GPUDevice)
- {
- this.pipelines = new Map();
- this.bindGroupLayouts = new Map();
-
- // Initialize mock pipelines
- this.pipelines.set("blur_compute_horizontal", { "label": "blur_compute_horizontal" });
- this.pipelines.set("blur_compute_vertical", { "label": "blur_compute_vertical" });
-
- // Initialize mock bind group layouts
- this.bindGroupLayouts.set("blur_compute", { "label": "blur_compute_bind_group_layout" });
- }
-
- getPipeline (name: string): any
- {
- return this.pipelines.get(name);
- }
-
- getBindGroupLayout (name: string): any
- {
- return this.bindGroupLayouts.get(name);
- }
-
- destroy (): void
- {
- this.pipelines.clear();
- this.bindGroupLayouts.clear();
- }
- }
-
- const createMockDevice = (): GPUDevice =>
- {
- return {
- "createShaderModule": vi.fn(() => ({ "label": "mockShaderModule" })),
- "createBindGroupLayout": vi.fn(() => ({ "label": "mockBindGroupLayout" })),
- "createPipelineLayout": vi.fn(() => ({ "label": "mockPipelineLayout" })),
- "createComputePipeline": vi.fn(() => ({ "label": "mockComputePipeline" }))
- } as unknown as GPUDevice;
- };
-
- beforeEach(() =>
- {
- vi.clearAllMocks();
- });
-
- describe("constructor", () =>
- {
- it("should create instance with device", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- expect(manager).toBeDefined();
- });
-
- it("should initialize blur pipelines", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- expect(manager.getPipeline("blur_compute_horizontal")).toBeDefined();
- expect(manager.getPipeline("blur_compute_vertical")).toBeDefined();
- });
- });
-
- describe("getPipeline", () =>
- {
- it("should return horizontal blur pipeline", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- const pipeline = manager.getPipeline("blur_compute_horizontal");
-
- expect(pipeline).toBeDefined();
- expect(pipeline.label).toBe("blur_compute_horizontal");
- });
-
- it("should return vertical blur pipeline", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- const pipeline = manager.getPipeline("blur_compute_vertical");
-
- expect(pipeline).toBeDefined();
- expect(pipeline.label).toBe("blur_compute_vertical");
- });
-
- it("should return undefined for non-existent pipeline", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- expect(manager.getPipeline("nonexistent")).toBeUndefined();
- });
- });
-
- describe("getBindGroupLayout", () =>
- {
- it("should return blur compute bind group layout", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- const layout = manager.getBindGroupLayout("blur_compute");
-
- expect(layout).toBeDefined();
- });
-
- it("should return undefined for non-existent layout", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- expect(manager.getBindGroupLayout("nonexistent")).toBeUndefined();
- });
- });
-
- describe("destroy", () =>
- {
- it("should clear pipelines", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- manager.destroy();
-
- expect(manager.getPipeline("blur_compute_horizontal")).toBeUndefined();
- expect(manager.getPipeline("blur_compute_vertical")).toBeUndefined();
- });
-
- it("should clear bind group layouts", () =>
- {
- const device = createMockDevice();
- const manager = new MockComputePipelineManager(device);
-
- manager.destroy();
-
- expect(manager.getBindGroupLayout("blur_compute")).toBeUndefined();
- });
- });
-});
diff --git a/packages/webgpu/src/Compute/ComputePipelineManager.ts b/packages/webgpu/src/Compute/ComputePipelineManager.ts
deleted file mode 100644
index 46a873d9..00000000
--- a/packages/webgpu/src/Compute/ComputePipelineManager.ts
+++ /dev/null
@@ -1,322 +0,0 @@
-/**
- * @description Compute Pipeline Manager
- * Compute Shaderパイプラインの管理
- *
- * Compute Shaderは並列処理に最適で、フィルター処理を高速化。
- * 特に大きなブラー半径(64+)の場合、20-35%の高速化が期待できる。
- *
- * @class
- */
-export class ComputePipelineManager
-{
- private device: GPUDevice;
- private pipelines: Map;
- private bindGroupLayouts: Map;
-
- /**
- * @constructor
- * @param {GPUDevice} device - WebGPU device
- */
- constructor (device: GPUDevice)
- {
- this.device = device;
- this.pipelines = new Map();
- this.bindGroupLayouts = new Map();
-
- this.initializeBlurPipelines();
- }
-
- /**
- * @description ブラー用Compute Pipelineを初期化
- * @private
- */
- private initializeBlurPipelines (): void
- {
- // ブラーCompute Shader用のBindGroupLayoutを作成
- const blurBindGroupLayout = this.device.createBindGroupLayout({
- "label": "blur_compute_bind_group_layout",
- "entries": [
- {
- // 入力テクスチャ
- "binding": 0,
- "visibility": GPUShaderStage.COMPUTE,
- "texture": {
- "sampleType": "float"
- }
- },
- {
- // 出力テクスチャ(Storage Texture)
- "binding": 1,
- "visibility": GPUShaderStage.COMPUTE,
- "storageTexture": {
- "access": "write-only",
- "format": "rgba8unorm"
- }
- },
- {
- // パラメータ(方向、ブラー半径など)
- "binding": 2,
- "visibility": GPUShaderStage.COMPUTE,
- "buffer": {
- "type": "uniform"
- }
- }
- ]
- });
-
- this.bindGroupLayouts.set("blur_compute", blurBindGroupLayout);
-
- // 水平/垂直ブラーパイプラインを作成
- // 同じシェーダーを使用し、方向はuniformで制御
- this.createBlurComputePipeline("blur_compute_horizontal");
- this.createBlurComputePipeline("blur_compute_vertical");
-
- // 共有メモリ版(大半径用)
- this.createBlurComputePipeline("blur_compute_shared_horizontal", true);
- this.createBlurComputePipeline("blur_compute_shared_vertical", true);
- }
-
- /**
- * @description ブラーCompute Pipelineを作成
- * @param {string} name - パイプライン名
- * @private
- */
- private createBlurComputePipeline (name: string, useSharedMemory: boolean = false): void
- {
- const shaderModule = this.device.createShaderModule({
- "label": `${name}_shader`,
- "code": useSharedMemory ? this.getSharedBlurComputeShaderCode() : this.getBlurComputeShaderCode()
- });
-
- const pipelineLayout = this.device.createPipelineLayout({
- "label": `${name}_layout`,
- "bindGroupLayouts": [this.bindGroupLayouts.get("blur_compute")!]
- });
-
- const pipeline = this.device.createComputePipeline({
- "label": name,
- "layout": pipelineLayout,
- "compute": {
- "module": shaderModule,
- "entryPoint": "main"
- }
- });
-
- this.pipelines.set(name, pipeline);
- }
-
- /**
- * @description ブラーCompute Shaderコードを生成
- * ボックスブラー(均一加重平均)を使用。Fragment Shaderと同一出力。
- * @return {string} WGSLシェーダーコード
- * @private
- */
- private getBlurComputeShaderCode (): string
- {
- return `
-struct BlurParams {
- direction: vec2, // (1,0) or (0,1)
- radius: f32, // ブラー半径
- fraction: f32, // 端ピクセルのブレンド割合
- texSize: vec2, // テクスチャサイズ
- samples: f32, // サンプル数
- padding: f32, // パディング(16バイトアライメント)
-}
-
-@group(0) @binding(0) var inputTexture: texture_2d;
-@group(0) @binding(1) var outputTexture: texture_storage_2d;
-@group(0) @binding(2) var params: BlurParams;
-
-const WORKGROUP_SIZE: u32 = 16u;
-
-@compute @workgroup_size(16, 16, 1)
-fn main(
- @builtin(global_invocation_id) globalId: vec3
-) {
- let texSize = vec2(u32(params.texSize.x), u32(params.texSize.y));
- let radius = i32(params.radius);
-
- let outCoord = globalId.xy;
-
- if (outCoord.x >= texSize.x || outCoord.y >= texSize.y) {
- return;
- }
-
- let direction = vec2(i32(params.direction.x), i32(params.direction.y));
- let samples = params.samples;
- let fraction = params.fraction;
-
- var color = vec4(0.0);
-
- for (var i = -radius; i <= radius; i = i + 1) {
- var sampleCoord = vec2(outCoord) + direction * i;
-
- sampleCoord.x = clamp(sampleCoord.x, 0, i32(texSize.x) - 1);
- sampleCoord.y = clamp(sampleCoord.y, 0, i32(texSize.y) - 1);
-
- let sample = textureLoad(inputTexture, vec2(sampleCoord), 0);
-
- // 端ピクセルにfraction重みを適用(Fragment Shaderと同じロジック)
- if (i == -radius || i == radius) {
- color = color + sample * fraction;
- } else {
- color = color + sample;
- }
- }
-
- color = color / samples;
-
- textureStore(outputTexture, outCoord, color);
-}
-`;
- }
-
- /**
- * @description 共有メモリ版ブラーCompute Shaderコードを生成
- * ワークグループ共有メモリでテクスチャ読み込みの重複を排除。
- * radius >= 8 で通常版より高速。
- * @return {string} WGSLシェーダーコード
- * @private
- */
- private getSharedBlurComputeShaderCode (): string
- {
- return `
-struct BlurParams {
- direction: vec2,
- radius: f32,
- fraction: f32,
- texSize: vec2,
- samples: f32,
- padding: f32,
-}
-
-@group(0) @binding(0) var inputTexture: texture_2d;
-@group(0) @binding(1) var outputTexture: texture_storage_2d;
-@group(0) @binding(2) var params: BlurParams;
-
-const TILE: u32 = 16u;
-const MAX_APRON: u32 = 24u;
-const SHARED_W: u32 = TILE + 2u * MAX_APRON;
-
-var tile: array, ${(16 + 2 * 24) * 16}>;
-
-@compute @workgroup_size(16, 16, 1)
-fn main(
- @builtin(global_invocation_id) globalId: vec3,
- @builtin(local_invocation_id) localId: vec3,
- @builtin(workgroup_id) workgroupId: vec3
-) {
- let texSize = vec2(u32(params.texSize.x), u32(params.texSize.y));
- let radius = u32(params.radius);
- let apron = min(radius, MAX_APRON);
- let isHorizontal = params.direction.x > 0.5;
- let fraction = params.fraction;
- let samples = params.samples;
-
- let threadIdx = localId.x + localId.y * TILE;
- let totalThreads = TILE * TILE;
-
- if (isHorizontal) {
- let sharedWidth = TILE + 2u * apron;
- let baseX = workgroupId.x * TILE;
- let y = globalId.y;
- let clampedY = clamp(y, 0u, max(texSize.y, 1u) - 1u);
-
- // 全スレッドが協調ロード(範囲外スレッドもclampされた座標でロード)
- var idx = threadIdx;
- loop {
- if (idx >= sharedWidth) { break; }
- let gx = i32(baseX) + i32(idx) - i32(apron);
- let cx = u32(clamp(gx, 0, i32(max(texSize.x, 1u)) - 1));
- tile[localId.y * SHARED_W + idx] = textureLoad(inputTexture, vec2(cx, clampedY), 0);
- idx += totalThreads;
- }
-
- // 全スレッドがバリアに到達(早期returnなし)
- workgroupBarrier();
-
- // 範囲内のスレッドのみ出力
- let outX = globalId.x;
- if (outX < texSize.x && y < texSize.y) {
- let iRadius = i32(radius);
- var color = vec4(0.0);
- for (var i = -iRadius; i <= iRadius; i = i + 1) {
- let tileIdx = i32(localId.x) + i32(apron) + i;
- let s = tile[localId.y * SHARED_W + u32(clamp(tileIdx, 0, i32(sharedWidth) - 1))];
- if (i == -iRadius || i == iRadius) {
- color += s * fraction;
- } else {
- color += s;
- }
- }
- textureStore(outputTexture, vec2(outX, y), color / samples);
- }
- } else {
- let sharedHeight = TILE + 2u * apron;
- let baseY = workgroupId.y * TILE;
- let x = globalId.x;
- let clampedX = clamp(x, 0u, max(texSize.x, 1u) - 1u);
-
- // 全スレッドが協調ロード(範囲外スレッドもclampされた座標でロード)
- var idx = threadIdx;
- loop {
- if (idx >= sharedHeight) { break; }
- let gy = i32(baseY) + i32(idx) - i32(apron);
- let cy = u32(clamp(gy, 0, i32(max(texSize.y, 1u)) - 1));
- tile[idx * TILE + localId.x] = textureLoad(inputTexture, vec2(clampedX, cy), 0);
- idx += totalThreads;
- }
-
- // 全スレッドがバリアに到達(早期returnなし)
- workgroupBarrier();
-
- // 範囲内のスレッドのみ出力
- let outY = globalId.y;
- if (x < texSize.x && outY < texSize.y) {
- let iRadius = i32(radius);
- var color = vec4(0.0);
- for (var i = -iRadius; i <= iRadius; i = i + 1) {
- let tileIdx = i32(localId.y) + i32(apron) + i;
- let s = tile[u32(clamp(tileIdx, 0, i32(sharedHeight) - 1)) * TILE + localId.x];
- if (i == -iRadius || i == iRadius) {
- color += s * fraction;
- } else {
- color += s;
- }
- }
- textureStore(outputTexture, vec2(x, outY), color / samples);
- }
- }
-}
-`;
- }
-
- /**
- * @description パイプラインを取得
- * @param {string} name - パイプライン名
- * @return {GPUComputePipeline | undefined}
- */
- getPipeline (name: string): GPUComputePipeline | undefined
- {
- return this.pipelines.get(name);
- }
-
- /**
- * @description BindGroupLayoutを取得
- * @param {string} name - レイアウト名
- * @return {GPUBindGroupLayout | undefined}
- */
- getBindGroupLayout (name: string): GPUBindGroupLayout | undefined
- {
- return this.bindGroupLayouts.get(name);
- }
-
- /**
- * @description リソースを破棄
- */
- destroy (): void
- {
- this.pipelines.clear();
- this.bindGroupLayouts.clear();
- }
-}
diff --git a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts b/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts
deleted file mode 100644
index dd859f05..00000000
--- a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts
+++ /dev/null
@@ -1,318 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import type { IAttachmentObject } from "../../interface/IAttachmentObject";
-import type { ComputePipelineManager } from "../ComputePipelineManager";
-import { execute } from "./ComputeExecuteBlurService";
-
-// Mock GPUBufferUsage
-const GPUBufferUsage = {
- UNIFORM: 0x0040,
- COPY_DST: 0x0008
-};
-(globalThis as any).GPUBufferUsage = GPUBufferUsage;
-
-describe("ComputeExecuteBlurService", () =>
-{
- const createMockDevice = () =>
- {
- const mockBuffer = { "label": "mockBuffer" };
- return {
- "createBuffer": vi.fn(() => mockBuffer),
- "createBindGroup": vi.fn(() => ({ "label": "mockBindGroup" })),
- "queue": {
- "writeBuffer": vi.fn()
- }
- } as unknown as GPUDevice;
- };
-
- const createMockCommandEncoder = () =>
- {
- const mockComputePass = {
- "setPipeline": vi.fn(),
- "setBindGroup": vi.fn(),
- "dispatchWorkgroups": vi.fn(),
- "end": vi.fn()
- };
- return {
- "beginComputePass": vi.fn(() => mockComputePass),
- "_mockComputePass": mockComputePass
- } as unknown as GPUCommandEncoder & { _mockComputePass: any };
- };
-
- const createMockComputePipelineManager = (hasPipeline: boolean = true) =>
- {
- return {
- "getPipeline": vi.fn(() => hasPipeline ? { "label": "mockPipeline" } : null),
- "getBindGroupLayout": vi.fn(() => hasPipeline ? { "label": "mockLayout" } : null)
- } as unknown as ComputePipelineManager;
- };
-
- const createMockAttachment = (width: number, height: number): IAttachmentObject =>
- {
- return {
- "id": 1,
- "width": width,
- "height": height,
- "clipLevel": 0,
- "msaa": false,
- "mask": false,
- "color": null,
- "texture": {
- "id": 1,
- "width": width,
- "height": height,
- "area": width * height,
- "smooth": true,
- "resource": {} as GPUTexture,
- "view": { "label": "mockView" } as unknown as GPUTextureView
- },
- "stencil": null,
- "msaaTexture": null,
- "msaaStencil": null
- };
- };
-
- beforeEach(() =>
- {
- vi.spyOn(console, "error").mockImplementation(() => {});
- });
-
- describe("pipeline selection", () =>
- {
- it("should use horizontal pipeline when isHorizontal is true", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(pipelineManager.getPipeline).toHaveBeenCalledWith("blur_compute_horizontal");
- });
-
- it("should use vertical pipeline when isHorizontal is false", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, false, 8);
-
- expect(pipelineManager.getPipeline).toHaveBeenCalledWith("blur_compute_vertical");
- });
-
- it("should return early when pipeline not found", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager(false);
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(commandEncoder.beginComputePass).not.toHaveBeenCalled();
- });
- });
-
- describe("parameter buffer", () =>
- {
- it("should create uniform buffer with correct usage", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(device.createBuffer).toHaveBeenCalledWith(
- expect.objectContaining({
- "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
- })
- );
- });
-
- it("should write parameter data to buffer", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(device.queue.writeBuffer).toHaveBeenCalled();
- });
-
- it("should set horizontal direction vector when isHorizontal is true", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- // Check the params passed to writeBuffer
- const writeBufferCall = (device.queue.writeBuffer as ReturnType).mock.calls[0];
- const params = writeBufferCall[2] as Float32Array;
- expect(params[0]).toBe(1.0); // direction.x
- expect(params[1]).toBe(0.0); // direction.y
- });
-
- it("should set vertical direction vector when isHorizontal is false", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, false, 8);
-
- const writeBufferCall = (device.queue.writeBuffer as ReturnType).mock.calls[0];
- const params = writeBufferCall[2] as Float32Array;
- expect(params[0]).toBe(0.0); // direction.x
- expect(params[1]).toBe(1.0); // direction.y
- });
- });
-
- describe("bind group", () =>
- {
- it("should create bind group with correct layout", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(pipelineManager.getBindGroupLayout).toHaveBeenCalledWith("blur_compute");
- });
-
- it("should create bind group with source and dest textures", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(device.createBindGroup).toHaveBeenCalled();
- });
- });
-
- describe("compute pass", () =>
- {
- it("should begin compute pass with correct label for horizontal", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(commandEncoder.beginComputePass).toHaveBeenCalledWith({
- "label": "blur_compute_pass_h"
- });
- });
-
- it("should begin compute pass with correct label for vertical", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, false, 8);
-
- expect(commandEncoder.beginComputePass).toHaveBeenCalledWith({
- "label": "blur_compute_pass_v"
- });
- });
-
- it("should set pipeline", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(commandEncoder._mockComputePass.setPipeline).toHaveBeenCalled();
- });
-
- it("should set bind group", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(commandEncoder._mockComputePass.setBindGroup).toHaveBeenCalledWith(0, expect.anything());
- });
-
- it("should end compute pass", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- expect(commandEncoder._mockComputePass.end).toHaveBeenCalled();
- });
- });
-
- describe("workgroup dispatch", () =>
- {
- it("should calculate correct workgroup count for 256x256", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(256, 256);
- const dest = createMockAttachment(256, 256);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- // 256 / 16 = 16 workgroups in each dimension
- expect(commandEncoder._mockComputePass.dispatchWorkgroups).toHaveBeenCalledWith(16, 16, 1);
- });
-
- it("should round up workgroup count for non-aligned size", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const pipelineManager = createMockComputePipelineManager();
- const source = createMockAttachment(100, 100);
- const dest = createMockAttachment(100, 100);
-
- execute(device, commandEncoder, pipelineManager, source, dest, true, 8);
-
- // ceil(100 / 16) = 7 workgroups in each dimension
- expect(commandEncoder._mockComputePass.dispatchWorkgroups).toHaveBeenCalledWith(7, 7, 1);
- });
- });
-});
diff --git a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts b/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts
deleted file mode 100644
index 1026baa3..00000000
--- a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import type { IAttachmentObject } from "../../interface/IAttachmentObject";
-import type { ComputePipelineManager } from "../ComputePipelineManager";
-
-/**
- * @description プリアロケートされたFloat32Array (サイズ8)
- */
-const $params8 = new Float32Array(8);
-
-/**
- * @description プリアロケートされたBindGroupEntry配列 (バインディング3つ)
- */
-const $computeEntries3: GPUBindGroupEntry[] = [
- { "binding": 0, "resource": null as unknown as GPUTextureView },
- { "binding": 1, "resource": null as unknown as GPUTextureView },
- { "binding": 2, "resource": { "buffer": null as unknown as GPUBuffer } }
-];
-
-/**
- * @description プリアロケートされたComputePassDescriptor
- */
-const $labelH: GPUComputePassDescriptor = { "label": "blur_compute_pass_h" };
-const $labelV: GPUComputePassDescriptor = { "label": "blur_compute_pass_v" };
-
-/**
- * @description Compute Shaderでブラーを実行(ボックスブラー)
- * Execute box blur using Compute Shader
- *
- * Fragment Shaderと同一のボックスブラーアルゴリズムを使用。
- *
- * @param {GPUDevice} device - WebGPU device
- * @param {GPUCommandEncoder} commandEncoder - コマンドエンコーダー
- * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager
- * @param {IAttachmentObject} source - 入力アタッチメント
- * @param {IAttachmentObject} dest - 出力アタッチメント
- * @param {boolean} isHorizontal - 水平ブラーかどうか
- * @param {number} blur - ブラー量(bufferBlurX/Y相当)
- * @param {object} [bufferManager] - バッファマネージャー(プール化用)
- * @return {void}
- */
-export const execute = (
- device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- computePipelineManager: ComputePipelineManager,
- source: IAttachmentObject,
- dest: IAttachmentObject,
- isHorizontal: boolean,
- blur: number,
- bufferManager?: { acquireAndWriteUniformBuffer(data: Float32Array, byteLength?: number): GPUBuffer }
-): void => {
-
- // radius 8~24 の場合は共有メモリ版を使用(MAX_APRON=24の制限)
- const halfBlur = Math.ceil(blur * 0.5);
- const useShared = halfBlur >= 8 && halfBlur <= 24;
- const pipelineName = useShared
- ? isHorizontal ? "blur_compute_shared_horizontal" : "blur_compute_shared_vertical"
- : isHorizontal ? "blur_compute_horizontal" : "blur_compute_vertical";
- const pipeline = computePipelineManager.getPipeline(pipelineName);
- const bindGroupLayout = computePipelineManager.getBindGroupLayout("blur_compute");
-
- if (!pipeline || !bindGroupLayout) {
- return;
- }
-
- // ボックスブラーパラメータ(Fragment ShaderのcalculateDirectionalBlurParamsと同一ロジック)
- const fraction = 1 - (halfBlur - blur * 0.5);
- const samples = 1 + blur;
-
- $params8[0] = isHorizontal ? 1.0 : 0.0; // direction.x
- $params8[1] = isHorizontal ? 0.0 : 1.0; // direction.y
- $params8[2] = halfBlur; // radius (halfBlur)
- $params8[3] = fraction; // fraction
- $params8[4] = source.width; // texSize.x
- $params8[5] = source.height; // texSize.y
- $params8[6] = samples; // samples
- $params8[7] = 0.0; // padding
-
- const paramsBuffer = bufferManager
- ? bufferManager.acquireAndWriteUniformBuffer($params8)
- : (() => {
- const buf = device.createBuffer({
- "size": $params8.byteLength,
- "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
- });
- device.queue.writeBuffer(buf, 0, $params8);
- return buf;
- })();
-
- $computeEntries3[0].resource = source.texture!.view;
- $computeEntries3[1].resource = dest.texture!.view;
- ($computeEntries3[2].resource as GPUBufferBinding).buffer = paramsBuffer;
- const bindGroup = device.createBindGroup({
- "layout": bindGroupLayout,
- "entries": $computeEntries3
- });
-
- const computePass = commandEncoder.beginComputePass(isHorizontal ? $labelH : $labelV);
-
- computePass.setPipeline(pipeline);
- computePass.setBindGroup(0, bindGroup);
-
- const workgroupsX = Math.ceil(dest.width / 16);
- const workgroupsY = Math.ceil(dest.height / 16);
-
- computePass.dispatchWorkgroups(workgroupsX, workgroupsY, 1);
- computePass.end();
-};
diff --git a/packages/webgpu/src/Context.test.ts b/packages/webgpu/src/Context.test.ts
index a3bc7133..1dd90849 100644
--- a/packages/webgpu/src/Context.test.ts
+++ b/packages/webgpu/src/Context.test.ts
@@ -211,15 +211,6 @@ describe("Context", () =>
});
});
- describe("clearRect", () =>
- {
- it("should be a no-op in WebGPU (clear happens at render pass start)", () =>
- {
- // clearRect does nothing in WebGPU - clear is done at render pass start
- expect(() => context.clearRect(0, 0, 100, 100)).not.toThrow();
- });
- });
-
describe("fillStyle", () =>
{
it("should update fill style when set", () =>
@@ -422,7 +413,7 @@ describe("Context", () =>
vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline);
// Mock buffer manager
- vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer);
+ vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer);
const mockNode = { "x": 100, "y": 200, "w": 50, "h": 30 };
@@ -464,7 +455,7 @@ describe("Context", () =>
"depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" }
} as unknown as GPURenderPassDescriptor);
vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline);
- vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer);
+ vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer);
const mockNode = { "x": 0, "y": 0, "w": 100, "h": 100 };
@@ -502,7 +493,7 @@ describe("Context", () =>
"depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" }
} as unknown as GPURenderPassDescriptor);
vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline);
- vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer);
+ vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer);
const mockNode = { "x": 0, "y": 0, "w": 10, "h": 10 };
const mockPixels = new Uint8Array(10 * 10 * 4);
@@ -559,7 +550,7 @@ describe("Context", () =>
"depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" }
} as unknown as GPURenderPassDescriptor);
vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline);
- vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer);
+ vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer);
// Mock copyExternalImageToTexture
mockQueue.copyExternalImageToTexture = vi.fn();
diff --git a/packages/webgpu/src/Context.ts b/packages/webgpu/src/Context.ts
index 3e80c193..5c47fa79 100644
--- a/packages/webgpu/src/Context.ts
+++ b/packages/webgpu/src/Context.ts
@@ -9,9 +9,7 @@ import { PathCommand } from "./PathCommand";
import { BufferManager } from "./BufferManager";
import { TextureManager } from "./TextureManager";
import { FrameBufferManager } from "./FrameBufferManager";
-import { AttachmentManager } from "./AttachmentManager";
import { PipelineManager } from "./Shader/PipelineManager";
-import { ComputePipelineManager } from "./Compute/ComputePipelineManager";
import {
$rootNodes,
$resetAtlas,
@@ -76,16 +74,19 @@ import { execute as contextContainerEndLayerUseCase } from "./Context/usecase/Co
/**
* @description スワップチェーン転送用のIdentity UV定数: scale=(1,1), offset=(0,0)
+ * Identity UV constant for swap-chain transfer: scale=(1,1), offset=(0,0)
*/
const $IDENTITY_UV = new Float32Array([1.0, 1.0, 0.0, 0.0]);
/**
* @description save()/restore()用の Float32Array プール
+ * Float32Array pool for save()/restore() operations
*/
const $matrixPool: Float32Array[] = [];
/**
* @description leaveMask() 用フルスクリーンメッシュ定数
+ * Full-screen mesh constant for leaveMask()
*/
const $FULLSCREEN_MESH = new Float32Array([
// Triangle 1: (0,0), (1,0), (0,1)
@@ -100,6 +101,7 @@ const $FULLSCREEN_MESH = new Float32Array([
/**
* @description clearNodeArea() 用クワッド頂点定数
+ * Quad vertex constant for clearNodeArea()
*/
const $QUAD_VERTICES = new Float32Array([
0, 0, // 左上
@@ -112,11 +114,19 @@ const $QUAD_VERTICES = new Float32Array([
/**
* @description containerDrawCachedFilter() 用 CT uniform プリアロケート
+ * Pre-allocated CT uniform for containerDrawCachedFilter()
*/
const $ctUniform8 = new Float32Array(8);
+/**
+ * @description copyTempToAtlasNode() 用 uniform プリアロケート (scale=1,-1, offset=0,1)
+ * Pre-allocated uniform for atlas node copy with Y-flip
+ */
+const $atlasNodeCopyUniform = new Float32Array([1, -1, 0, 1]);
+
/**
* @description fill() 用 uniform プリアロケート (color + matrix = 16 floats = 64 bytes)
+ * Pre-allocated uniform for fill() (color + matrix = 16 floats = 64 bytes)
*/
const $fillUniform16 = new Float32Array(16);
@@ -187,97 +197,132 @@ const $msaaDescriptor: GPURenderPassDescriptor = {
*/
export class Context
{
+ /** @description 変換行列スタック / Transform matrix stack */
public readonly $stack: Float32Array[];
+ /** @description 現在の2D変換行列 / Current 2D transform matrix */
public readonly $matrix: Float32Array;
+ /** @description 背景クリア色R / Background clear color R */
public $clearColorR: number;
+ /** @description 背景クリア色G / Background clear color G */
public $clearColorG: number;
+ /** @description 背景クリア色B / Background clear color B */
public $clearColorB: number;
+ /** @description 背景クリア色A / Background clear color A */
public $clearColorA: number;
+ /** @description メインアタッチメントオブジェクト / Main attachment object */
public $mainAttachmentObject: IAttachmentObject | null;
+ /** @description アタッチメントオブジェクトのスタック / Attachment object stack */
public readonly $stackAttachmentObject: IAttachmentObject[];
+ /** @description グローバルアルファ値 / Global alpha value */
public globalAlpha: number;
+ /** @description グローバル合成操作 / Global composite operation */
public globalCompositeOperation: IBlendMode;
+ /** @description 画像スムージングの有効/無効 / Whether image smoothing is enabled */
public imageSmoothingEnabled: boolean;
+ /** @description 塗りつぶしスタイル / Fill style color */
public $fillStyle: Float32Array;
+ /** @description 線スタイル / Stroke style color */
public $strokeStyle: Float32Array;
+ /** @description マスク描画範囲 / Mask drawing bounds */
public readonly maskBounds: IBounds;
+ /** @description 線の太さ / Line thickness */
public thickness: number;
+ /** @description 線端の形状 / Line cap style */
public caps: number;
+ /** @description 線の結合スタイル / Line joint style */
public joints: number;
+ /** @description マイターリミット / Miter limit */
public miterLimit: number;
+ /** @description GPUデバイス / GPU device instance */
private device: GPUDevice;
+ /** @description GPUキャンバスコンテキスト / GPU canvas context */
private canvasContext: GPUCanvasContext;
+ /** @description 優先テクスチャフォーマット / Preferred texture format */
private preferredFormat: GPUTextureFormat;
+ /** @description コマンドエンコーダー / Command encoder */
private commandEncoder: GPUCommandEncoder | null = null;
+ /** @description レンダーパスエンコーダー / Render pass encoder */
private renderPassEncoder: GPURenderPassEncoder | null = null;
- // Main canvas texture (for final display) - acquired once per frame
+ /** @description メインキャンバステクスチャ(最終表示用、フレームごとに1回取得) / Main canvas texture (for final display, acquired once per frame) */
private mainTexture: GPUTexture | null = null;
+ /** @description メインキャンバステクスチャビュー / Main canvas texture view */
private mainTextureView: GPUTextureView | null = null;
+ /** @description フレーム開始済みフラグ / Whether the frame has been started */
private frameStarted: boolean = false;
- // フレームごとの一時テクスチャ(endFrame()でdestroy)
+ /** @description フレームごとの一時テクスチャ(endFrame()でdestroy) / Per-frame temporary textures (destroyed in endFrame()) */
private frameTextures: GPUTexture[] = [];
- // フレームごとのプール管理テクスチャ(endFrame()でプールに返却)
+ /** @description フレームごとのプール管理テクスチャ(endFrame()でプールに返却) / Per-frame pooled textures (returned to pool in endFrame()) */
private pooledTextures: GPUTexture[] = [];
- // フレームごとのレンダーテクスチャプール管理(endFrame()でプールに返却)
+ /** @description フレームごとのレンダーテクスチャプール管理(endFrame()でプールに返却) / Per-frame render texture pool (returned to pool in endFrame()) */
private pooledRenderTextures: GPUTexture[] = [];
- // Current rendering target (could be main or atlas)
+ /** @description 現在のレンダーターゲット(メインまたはアトラス) / Current render target (could be main or atlas) */
private currentRenderTarget: GPUTextureView | null = null;
- // Current viewport size (WebGL版と同じ: アトラス描画時はアトラスサイズを使用)
+ /** @description 現在のビューポート幅(アトラス描画時はアトラスサイズ) / Current viewport width (atlas size during atlas rendering) */
private viewportWidth: number = 0;
+ /** @description 現在のビューポート高さ(アトラス描画時はアトラスサイズ) / Current viewport height (atlas size during atlas rendering) */
private viewportHeight: number = 0;
+ /** @description パスコマンド / Path command handler */
private pathCommand: PathCommand;
+ /** @description バッファマネージャー / Buffer manager */
private bufferManager: BufferManager;
+ /** @description テクスチャマネージャー / Texture manager */
private textureManager: TextureManager;
+ /** @description フレームバッファマネージャー / Frame buffer manager */
private frameBufferManager: FrameBufferManager;
+ /** @description パイプラインマネージャー / Pipeline manager */
private pipelineManager: PipelineManager;
- private computePipelineManager: ComputePipelineManager;
- private attachmentManager: AttachmentManager;
+ /** @description 新しい描画状態フラグ / New draw state flag */
public newDrawState: boolean = false;
- // コンテナレイヤースタック(フィルター/ブレンド用)
+ /** @description cacheAsBitmap用の保留中アトラスノードスタック / Pending atlas nodes stack for cacheAsBitmap */
+ private readonly _pendingAtlasNodes: Node[] = [];
+
+ /** @description コンテナレイヤースタック(フィルター/ブレンド用) / Container layer stack (for filter/blend) */
private readonly $containerLayerStack: IAttachmentObject[] = [];
+ /** @description コンテナレイヤーのコンテンツサイズ / Container layer content sizes */
private containerLayerContentSizes: { width: number; height: number }[] = [];
- // マスク描画モードフラグ(beginMask〜endMask間でtrue)
+ /** @description マスク描画モードフラグ(beginMask〜endMask間でtrue) / Mask drawing mode flag (true between beginMask and endMask) */
private inMaskMode: boolean = false;
- // ノード領域クリア済みフラグ(beginNodeRendering〜endNodeRendering間で使用)
+ /** @description ノード領域クリア済みフラグ(beginNodeRendering〜endNodeRendering間で使用) / Node area cleared flag (used between beginNodeRendering and endNodeRendering) */
private nodeAreaCleared: boolean = false;
- // 現在のノードのシザー範囲(クリア後に戻すため)
+ /** @description 現在のノードのシザー範囲(クリア後に戻すため) / Current node scissor rect (to restore after clearing) */
private currentNodeScissor: { x: number; y: number; w: number; h: number } | null = null;
- // アトラスレンダーパス統合: 同一アトラスへの連続描画でパスを再利用
+ /** @description アトラスレンダーパス統合: 同一アトラスへの連続描画でパスを再利用 / Atlas render pass integration: reuse pass for consecutive draws to the same atlas */
private nodeRenderPassAtlasIndex: number = -1;
- // Dynamic Uniform BindGroup(fill/stencilパイプライン共有、フレームごとに1回作成)
+ /** @description Dynamic Uniform BindGroup(fill/stencilパイプライン共有、フレームごとに1回作成) / Dynamic Uniform BindGroup (shared by fill/stencil pipelines, created once per frame) */
private fillDynamicBindGroup: GPUBindGroup | null = null;
+ /** @description Dynamic Uniform BindGroupのバッファ / Dynamic Uniform BindGroup buffer */
private fillDynamicBindGroupBuffer: GPUBuffer | null = null;
- // clearNodeArea() 用頂点バッファキャッシュ
+ /** @description clearNodeArea() 用頂点バッファキャッシュ / Vertex buffer cache for clearNodeArea() */
private nodeClearQuadBuffer: GPUBuffer | null = null;
- // Storage Buffer + Indirect Drawing を使用するかどうか
+ /** @description Storage Buffer + Indirect Drawing を使用するかどうか / Whether to use Storage Buffer + Indirect Drawing */
private useOptimizedInstancing: boolean = true;
- // リサイズ後にcanvasContextの再設定が必要かどうか
- // ($resizeComplete()でcanvas.width/heightが設定された後、ensureMainTexture()でconfigure()を呼ぶ)
+ /** @description リサイズ後にcanvasContextの再設定が必要かどうか / Whether canvasContext reconfiguration is needed after resize */
private $needsReconfigure: boolean = false;
- // Hot Path 用の事前割り当てバッファ
+ /** @description Hot Path 用の事前割り当てバッファ / Pre-allocated buffer for hot path */
private readonly $uniformData8 = new Float32Array(8);
+ /** @description Hot Path 用の事前割り当てシザーレクト / Pre-allocated scissor rect for hot path */
private readonly $scissorRect: { "x": number; "y": number; "w": number; "h": number } = { "x": 0, "y": 0, "w": 0, "h": 0 };
- // フィルター/コンテナレイヤー用のプリアロケートされた設定オブジェクト
+ /** @description フィルター/コンテナレイヤー用のプリアロケートされた設定オブジェクト / Pre-allocated config object for filter/container layers */
private readonly $filterConfig: {
device: GPUDevice;
commandEncoder: GPUCommandEncoder;
@@ -286,10 +331,18 @@ export class Context
pipelineManager: PipelineManager;
textureManager: TextureManager;
mainAttachment?: IAttachmentObject;
- computePipelineManager: ComputePipelineManager;
frameTextures: GPUTexture[];
};
+ /**
+ * @description WebGPUコンテキストを初期化する
+ * Initialize the WebGPU context
+ *
+ * @param {GPUDevice} device - GPUデバイス / GPU device instance
+ * @param {GPUCanvasContext} canvas_context - GPUキャンバスコンテキスト / GPU canvas context
+ * @param {GPUTextureFormat} preferred_format - 優先テクスチャフォーマット / Preferred texture format
+ * @param {number} device_pixel_ratio - デバイスピクセル比 / Device pixel ratio
+ */
constructor (
device: GPUDevice,
canvas_context: GPUCanvasContext,
@@ -356,8 +409,6 @@ export class Context
this.pipelineManager = new PipelineManager(device, preferred_format);
// 遅延パイプライン群を即座に先行作成(初回アクセス時のレイテンシ解消)
this.pipelineManager.preloadLazyGroups();
- this.computePipelineManager = new ComputePipelineManager(device);
- this.attachmentManager = new AttachmentManager(device);
// グラデーションLUT共有アタッチメントにGPUDeviceを設定
$setGradientLUTDevice(device);
@@ -383,7 +434,6 @@ export class Context
"frameBufferManager": this.frameBufferManager,
"pipelineManager": this.pipelineManager,
"textureManager": this.textureManager,
- "computePipelineManager": this.computePipelineManager,
"frameTextures": this.frameTextures
};
@@ -393,6 +443,9 @@ export class Context
/**
* @description 転送範囲をリセット(フレーム開始)
+ * Reset transfer bounds (frame start)
+ *
+ * @return {void}
*/
clearTransferBounds (): void
{
@@ -403,6 +456,13 @@ export class Context
/**
* @description 背景色を更新
+ * Update the background color
+ *
+ * @param {number} red - 赤色成分 / Red component
+ * @param {number} green - 緑色成分 / Green component
+ * @param {number} blue - 青色成分 / Blue component
+ * @param {number} alpha - アルファ成分 / Alpha component
+ * @return {void}
*/
updateBackgroundColor (red: number, green: number, blue: number, alpha: number): void
{
@@ -414,6 +474,9 @@ export class Context
/**
* @description 背景色で塗りつぶす(メインアタッチメント)
+ * Fill with background color (main attachment)
+ *
+ * @return {void}
*/
fillBackgroundColor (): void
{
@@ -469,6 +532,12 @@ export class Context
/**
* @description メインcanvasのサイズを変更
+ * Resize the main canvas
+ *
+ * @param {number} width - 新しい幅 / New width
+ * @param {number} height - 新しい高さ / New height
+ * @param {boolean} cache_clear - キャッシュをクリアするか / Whether to clear cache
+ * @return {void}
*/
resize (width: number, height: number, cache_clear: boolean = true): void
{
@@ -574,18 +643,11 @@ export class Context
this.bind(this.$mainAttachmentObject);
}
- /**
- * @description 指定範囲をクリアする
- */
- clearRect (_x: number, _y: number, _w: number, _h: number): void
- {
- // WebGPU clear rect implementation
- // WebGPUではclearはレンダーパス開始時に行うため、ここでは何もしない
- // 実際のクリアはbeginNodeRenderingやbeginFrameで行われる
- }
-
/**
* @description 現在の2D変換行列を保存
+ * Save the current 2D transform matrix
+ *
+ * @return {void}
*/
save (): void
{
@@ -596,6 +658,9 @@ export class Context
/**
* @description 2D変換行列を復元
+ * Restore the 2D transform matrix
+ *
+ * @return {void}
*/
restore (): void
{
@@ -608,6 +673,15 @@ export class Context
/**
* @description 2D変換行列を設定
+ * Set the 2D transform matrix
+ *
+ * @param {number} a - 水平スケール / Horizontal scale
+ * @param {number} b - 垂直スキュー / Vertical skew
+ * @param {number} c - 水平スキュー / Horizontal skew
+ * @param {number} d - 垂直スケール / Vertical scale
+ * @param {number} e - 水平移動 / Horizontal translation
+ * @param {number} f - 垂直移動 / Vertical translation
+ * @return {void}
*/
setTransform (
a: number, b: number, c: number,
@@ -623,6 +697,15 @@ export class Context
/**
* @description 現在の2D変換行列に対して乗算を行います
+ * Multiply the current 2D transform matrix
+ *
+ * @param {number} a - 水平スケール / Horizontal scale
+ * @param {number} b - 垂直スキュー / Vertical skew
+ * @param {number} c - 水平スキュー / Horizontal skew
+ * @param {number} d - 垂直スケール / Vertical scale
+ * @param {number} e - 水平移動 / Horizontal translation
+ * @param {number} f - 垂直移動 / Vertical translation
+ * @return {void}
*/
transform (
a: number, b: number, c: number,
@@ -641,6 +724,9 @@ export class Context
/**
* @description コンテキストの値を初期化する
+ * Reset all context values to their initial state
+ *
+ * @return {void}
*/
reset (): void
{
@@ -654,6 +740,9 @@ export class Context
/**
* @description パスを開始
+ * Begin a new path
+ *
+ * @return {void}
*/
beginPath (): void
{
@@ -662,6 +751,11 @@ export class Context
/**
* @description パスを移動
+ * Move the path to the specified point
+ *
+ * @param {number} x - X座標 / X coordinate
+ * @param {number} y - Y座標 / Y coordinate
+ * @return {void}
*/
moveTo (x: number, y: number): void
{
@@ -670,6 +764,11 @@ export class Context
/**
* @description パスを線で結ぶ
+ * Draw a line to the specified point
+ *
+ * @param {number} x - X座標 / X coordinate
+ * @param {number} y - Y座標 / Y coordinate
+ * @return {void}
*/
lineTo (x: number, y: number): void
{
@@ -678,6 +777,13 @@ export class Context
/**
* @description 二次ベジェ曲線を描画
+ * Draw a quadratic Bézier curve
+ *
+ * @param {number} cx - 制御点X / Control point X
+ * @param {number} cy - 制御点Y / Control point Y
+ * @param {number} x - 終点X / End point X
+ * @param {number} y - 終点Y / End point Y
+ * @return {void}
*/
quadraticCurveTo (cx: number, cy: number, x: number, y: number): void
{
@@ -686,6 +792,13 @@ export class Context
/**
* @description 塗りつぶしスタイルを設定
+ * Set the fill style color
+ *
+ * @param {number} red - 赤色成分 / Red component
+ * @param {number} green - 緑色成分 / Green component
+ * @param {number} blue - 青色成分 / Blue component
+ * @param {number} alpha - アルファ成分 / Alpha component
+ * @return {void}
*/
fillStyle (red: number, green: number, blue: number, alpha: number): void
{
@@ -697,6 +810,13 @@ export class Context
/**
* @description 線のスタイルを設定
+ * Set the stroke style color
+ *
+ * @param {number} red - 赤色成分 / Red component
+ * @param {number} green - 緑色成分 / Green component
+ * @param {number} blue - 青色成分 / Blue component
+ * @param {number} alpha - アルファ成分 / Alpha component
+ * @return {void}
*/
strokeStyle (red: number, green: number, blue: number, alpha: number): void
{
@@ -708,6 +828,9 @@ export class Context
/**
* @description パスを閉じる
+ * Close the current path
+ *
+ * @return {void}
*/
closePath (): void
{
@@ -716,6 +839,12 @@ export class Context
/**
* @description 円弧を描画
+ * Draw an arc
+ *
+ * @param {number} x - 中心X / Center X
+ * @param {number} y - 中心Y / Center Y
+ * @param {number} radius - 半径 / Radius
+ * @return {void}
*/
arc (x: number, y: number, radius: number): void
{
@@ -724,6 +853,15 @@ export class Context
/**
* @description 3次ベジェ曲線を描画
+ * Draw a cubic Bézier curve
+ *
+ * @param {number} cx1 - 第1制御点X / First control point X
+ * @param {number} cy1 - 第1制御点Y / First control point Y
+ * @param {number} cx2 - 第2制御点X / Second control point X
+ * @param {number} cy2 - 第2制御点Y / Second control point Y
+ * @param {number} x - 終点X / End point X
+ * @param {number} y - 終点Y / End point Y
+ * @return {void}
*/
bezierCurveTo (cx1: number, cy1: number, cx2: number, cy2: number, x: number, y: number): void
{
@@ -732,7 +870,10 @@ export class Context
/**
* @description 描画メソッド共通: レンダーパスの確保とノード領域クリア
+ * Common drawing method: ensure render pass and clear node area
* fill(), stroke(), gradientFill(), bitmapFill(), gradientStroke(), bitmapStroke() で使用
+ *
+ * @return {void}
*/
private ensureFillRenderPass (): void
{
@@ -854,6 +995,9 @@ export class Context
/**
* @description 塗りつぶしを実行(Loop-Blinn方式対応)
+ * Execute fill operation (with Loop-Blinn support)
+ *
+ * @return {void}
*/
fill (): void
{
@@ -901,6 +1045,9 @@ export class Context
/**
* @description Dynamic Uniform BindGroupを取得(フレーム内で初回呼び出し時に作成)
+ * Get or create the Dynamic Uniform BindGroup (created on first call within a frame)
+ *
+ * @return {GPUBindGroup} Dynamic Uniform BindGroup
*/
private getOrCreateFillDynamicBindGroup(): GPUBindGroup
{
@@ -927,14 +1074,28 @@ export class Context
/**
* @description fill/stroke用のcolor/matrix uniformを書き込む
- * FillUniforms構造体: color(vec4) + matrix0(vec4) + matrix1(vec4) + matrix2(vec4) = 64 bytes
- * @return Dynamic Uniform Buffer内のアライメント済みオフセット
+ * Write color/matrix uniform for fill/stroke operations
+ * FillUniforms構造体: color(vec4) + matrix0(vec4) + matrix1(vec4) + matrix2(vec4) = 64 bytes
+ *
+ * @param {number} red - 赤色成分 / Red component
+ * @param {number} green - 緑色成分 / Green component
+ * @param {number} blue - 青色成分 / Blue component
+ * @param {number} alpha - アルファ成分 / Alpha component
+ * @param {number} a - 変換行列a / Transform matrix a
+ * @param {number} b - 変換行列b / Transform matrix b
+ * @param {number} c - 変換行列c / Transform matrix c
+ * @param {number} d - 変換行列d / Transform matrix d
+ * @param {number} tx - 変換行列tx / Transform matrix tx
+ * @param {number} ty - 変換行列ty / Transform matrix ty
+ * @param {number} viewport_width - ビューポート幅 / Viewport width
+ * @param {number} viewport_height - ビューポート高さ / Viewport height
+ * @return {number} Dynamic Uniform Buffer内のアライメント済みオフセット / Aligned offset in the Dynamic Uniform Buffer
*/
private writeFillUniform(
red: number, green: number, blue: number, alpha: number,
a: number, b: number, c: number, d: number,
tx: number, ty: number,
- viewportWidth: number, viewportHeight: number
+ viewport_width: number, viewport_height: number
): number
{
// color
@@ -943,18 +1104,18 @@ export class Context
$fillUniform16[2] = blue;
$fillUniform16[3] = alpha;
// matrix0 (a, b, 0, pad) — ビューポート正規化
- $fillUniform16[4] = a / viewportWidth;
- $fillUniform16[5] = b / viewportHeight;
+ $fillUniform16[4] = a / viewport_width;
+ $fillUniform16[5] = b / viewport_height;
$fillUniform16[6] = 0;
$fillUniform16[7] = 0;
// matrix1 (c, d, 0, pad)
- $fillUniform16[8] = c / viewportWidth;
- $fillUniform16[9] = d / viewportHeight;
+ $fillUniform16[8] = c / viewport_width;
+ $fillUniform16[9] = d / viewport_height;
$fillUniform16[10] = 0;
$fillUniform16[11] = 0;
// matrix2 (tx, ty, 1, pad)
- $fillUniform16[12] = tx / viewportWidth;
- $fillUniform16[13] = ty / viewportHeight;
+ $fillUniform16[12] = tx / viewport_width;
+ $fillUniform16[13] = ty / viewport_height;
$fillUniform16[14] = 1;
$fillUniform16[15] = 0;
@@ -963,53 +1124,75 @@ export class Context
/**
* @description 2パスステンシルフィル(アトラス用)
+ * Two-pass stencil fill (for atlas)
+ *
+ * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer
+ * @param {number} vertex_count - 頂点数 / Vertex count
+ * @param {GPUBindGroup} bind_group - バインドグループ / Bind group
+ * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset
+ * @return {void}
*/
private fillWithStencil(
- vertexBuffer: GPUBuffer,
- vertexCount: number,
- bindGroup: GPUBindGroup,
- uniformOffset: number
+ vertex_buffer: GPUBuffer,
+ vertex_count: number,
+ bind_group: GPUBindGroup,
+ uniform_offset: number
): void
{
contextFillWithStencilService(
this.renderPassEncoder!,
this.pipelineManager,
- vertexBuffer,
- vertexCount,
- bindGroup,
- uniformOffset
+ vertex_buffer,
+ vertex_count,
+ bind_group,
+ uniform_offset
);
}
/**
* @description 2パスステンシルフィル(メインキャンバス用)
+ * Two-pass stencil fill (for main canvas)
+ *
+ * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer
+ * @param {number} vertex_count - 頂点数 / Vertex count
+ * @param {GPUBindGroup} bind_group - バインドグループ / Bind group
+ * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset
+ * @return {void}
*/
private fillWithStencilMain(
- vertexBuffer: GPUBuffer,
- vertexCount: number,
- bindGroup: GPUBindGroup,
- uniformOffset: number
+ vertex_buffer: GPUBuffer,
+ vertex_count: number,
+ bind_group: GPUBindGroup,
+ uniform_offset: number
): void
{
contextFillWithStencilMainService(
this.renderPassEncoder!,
this.pipelineManager,
- vertexBuffer,
- vertexCount,
- bindGroup,
- uniformOffset
+ vertex_buffer,
+ vertex_count,
+ bind_group,
+ uniform_offset
);
}
/**
* @description 単純なフィル(ステンシルなし、キャンバス描画用)
+ * Simple fill (no stencil, for canvas rendering)
+ *
+ * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer
+ * @param {number} vertex_count - 頂点数 / Vertex count
+ * @param {boolean} use_stencil_pipeline - ステンシルパイプラインを使用するか / Whether to use stencil pipeline
+ * @param {GPUBindGroup} bind_group - バインドグループ / Bind group
+ * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset
+ * @return {void}
*/
private fillSimple(
- vertexBuffer: GPUBuffer,
- vertexCount: number,
- useStencilPipeline: boolean,
- bindGroup: GPUBindGroup,
- uniformOffset: number
+ vertex_buffer: GPUBuffer,
+ vertex_count: number,
+ use_stencil_pipeline: boolean,
+ bind_group: GPUBindGroup,
+ uniform_offset: number
): void
{
const clipLevel = this.$mainAttachmentObject?.clipLevel ?? 1;
@@ -1017,63 +1200,22 @@ export class Context
contextFillSimpleService(
this.renderPassEncoder!,
this.pipelineManager,
- vertexBuffer,
- vertexCount,
- bindGroup,
- uniformOffset,
- !!this.currentRenderTarget,
- useStencilPipeline,
+ vertex_buffer,
+ vertex_count,
+ bind_group,
+ uniform_offset,
+ this.currentRenderTarget,
+ use_stencil_pipeline,
clipLevel
);
}
- /**
- * @description オフスクリーンアタッチメントにバインド
- * WebGL: FrameBufferManagerBindAttachmentObjectService
- */
- bindAttachment(attachment: IAttachmentObject): void
- {
- this.attachmentManager.bindAttachment(attachment);
-
- // 現在のレンダーターゲットをオフスクリーンに切り替え
- // color?.view または texture?.view を使用
- const view = attachment.color?.view ?? attachment.texture?.view;
- if (view) {
- this.currentRenderTarget = view;
- }
- }
-
- /**
- * @description メインキャンバスにバインド
- * WebGL: FrameBufferManagerUnBindAttachmentObjectService
- */
- unbindAttachment(): void
- {
- this.attachmentManager.unbindAttachment();
- this.currentRenderTarget = null;
- }
-
- /**
- * @description アタッチメントオブジェクトを取得
- * WebGL: FrameBufferManagerGetAttachmentObjectUseCase
- */
- getAttachmentObject(width: number, height: number, msaa: boolean = false): IAttachmentObject
- {
- return this.attachmentManager.getAttachmentObject(width, height, msaa);
- }
-
- /**
- * @description アタッチメントオブジェクトを解放
- * WebGL: FrameBufferManagerReleaseAttachmentObjectUseCase
- */
- releaseAttachment(attachment: IAttachmentObject): void
- {
- this.attachmentManager.releaseAttachment(attachment);
- }
-
/**
* @description 線の描画を実行(WebGL版と同じ仕様)
+ * Execute stroke drawing (same specification as WebGL version)
* WebGL版と同様に、ストロークを塗りとして描画する
+ *
+ * @return {void}
*/
stroke (): void
{
@@ -1124,6 +1266,15 @@ export class Context
/**
* @description グラデーションの塗りつぶしを実行
+ * Execute gradient fill
+ *
+ * @param {number} type - グラデーションタイプ / Gradient type
+ * @param {number[]} stops - カラーストップ配列 / Color stop array
+ * @param {Float32Array} matrix - グラデーション変換行列 / Gradient transform matrix
+ * @param {number} spread - スプレッドメソッド / Spread method
+ * @param {number} interpolation - 補間方法 / Interpolation method
+ * @param {number} focal - 焦点距離 / Focal point ratio
+ * @return {void}
*/
gradientFill (
type: number,
@@ -1148,7 +1299,7 @@ export class Context
const useStencilPipeline = useMainStencil;
// アトラスへの描画かどうか
- const useAtlasTarget = !!this.currentRenderTarget;
+ const useAtlasTarget = this.currentRenderTarget;
const lutTexture = contextGradientFillUseCase(
this.device,
@@ -1181,6 +1332,15 @@ export class Context
/**
* @description ビットマップの塗りつぶしを実行
+ * Execute bitmap fill
+ *
+ * @param {Uint8Array} pixels - ピクセルデータ / Pixel data
+ * @param {Float32Array} matrix - ビットマップ変換行列 / Bitmap transform matrix
+ * @param {number} width - ビットマップ幅 / Bitmap width
+ * @param {number} height - ビットマップ高さ / Bitmap height
+ * @param {boolean} repeat - 繰り返しフラグ / Repeat flag
+ * @param {boolean} smooth - スムージングフラグ / Smoothing flag
+ * @return {void}
*/
bitmapFill (
pixels: Uint8Array,
@@ -1224,7 +1384,7 @@ export class Context
smooth,
this.viewportWidth,
this.viewportHeight,
- !!this.currentRenderTarget,
+ this.currentRenderTarget,
!!useStencilPipeline,
clipLevel
);
@@ -1241,6 +1401,15 @@ export class Context
/**
* @description グラデーション線の描画を実行
+ * Execute gradient stroke drawing
+ *
+ * @param {number} type - グラデーションタイプ / Gradient type
+ * @param {number[]} stops - カラーストップ配列 / Color stop array
+ * @param {Float32Array} matrix - グラデーション変換行列 / Gradient transform matrix
+ * @param {number} spread - スプレッドメソッド / Spread method
+ * @param {number} interpolation - 補間方法 / Interpolation method
+ * @param {number} focal - 焦点距離 / Focal point ratio
+ * @return {void}
*/
gradientStroke (
type: number,
@@ -1282,7 +1451,7 @@ export class Context
focal,
this.viewportWidth,
this.viewportHeight,
- !!this.currentRenderTarget,
+ this.currentRenderTarget,
useStencilPipeline
);
@@ -1298,6 +1467,15 @@ export class Context
/**
* @description ビットマップ線の描画を実行
+ * Execute bitmap stroke drawing
+ *
+ * @param {Uint8Array} pixels - ピクセルデータ / Pixel data
+ * @param {Float32Array} matrix - ビットマップ変換行列 / Bitmap transform matrix
+ * @param {number} width - ビットマップ幅 / Bitmap width
+ * @param {number} height - ビットマップ高さ / Bitmap height
+ * @param {boolean} repeat - 繰り返しフラグ / Repeat flag
+ * @param {boolean} smooth - スムージングフラグ / Smoothing flag
+ * @return {void}
*/
bitmapStroke (
pixels: Uint8Array,
@@ -1339,7 +1517,7 @@ export class Context
smooth,
this.viewportWidth,
this.viewportHeight,
- !!this.currentRenderTarget,
+ this.currentRenderTarget,
useStencilPipeline
);
@@ -1355,8 +1533,11 @@ export class Context
/**
* @description マスク処理を実行
+ * Execute mask clipping operation
* WebGL版と同様にステンシルバッファを使用したクリッピング
* メインアタッチメントとアトラス両方でマスク処理をサポート
+ *
+ * @return {void}
*/
clip (): void
{
@@ -1425,6 +1606,10 @@ export class Context
/**
* @description アタッチメントオブジェクトをバインド
+ * Bind an attachment object
+ *
+ * @param {IAttachmentObject} attachment_object - バインドするアタッチメント / Attachment to bind
+ * @return {void}
*/
bind (attachment_object: IAttachmentObject): void
{
@@ -1437,8 +1622,11 @@ export class Context
/**
* @description 現在のアタッチメントオブジェクトを取得
+ * Get the current attachment object
* アトラスがバインドされていない場合はメインアタッチメントを返す
* When no atlas is bound, returns the main attachment
+ *
+ * @return {IAttachmentObject | null} 現在のアタッチメント / Current attachment
*/
get currentAttachmentObject (): IAttachmentObject | null
{
@@ -1450,6 +1638,9 @@ export class Context
/**
* @description アトラス専用のアタッチメントオブジェクトを取得
+ * Get the atlas-specific attachment object
+ *
+ * @return {IAttachmentObject | null} アトラスアタッチメント / Atlas attachment
*/
get atlasAttachmentObject (): IAttachmentObject | null
{
@@ -1458,6 +1649,10 @@ export class Context
/**
* @description グリッドの描画データをセット
+ * Set grid drawing data
+ *
+ * @param {Float32Array | null} grid_data - グリッドデータ / Grid data
+ * @return {void}
*/
useGrid (grid_data: Float32Array | null): void
{
@@ -1466,7 +1661,11 @@ export class Context
/**
* @description 指定のノード範囲で描画を開始(アトラステクスチャへの描画)
+ * Begin rendering for the specified node region (drawing to atlas texture)
* 2パスステンシルフィル対応: ステンシルバッファ付きレンダーパスを使用
+ *
+ * @param {Node} node - 描画対象ノード / Target node for rendering
+ * @return {void}
*/
beginNodeRendering (node: Node): void
{
@@ -1558,7 +1757,10 @@ export class Context
/**
* @description ノード領域がまだクリアされていない場合にクリアを実行
+ * Clear the node area if it has not been cleared yet
* 最初の描画操作(fill, gradientFill, gradientStroke等)で呼び出される
+ *
+ * @return {void}
*/
private ensureNodeAreaCleared (): void
{
@@ -1569,7 +1771,10 @@ export class Context
/**
* @description ノード領域をクリア(透明色 + ステンシル=0)
+ * Clear the node area (transparent color + stencil=0)
* WebGL版の gl.clear(COLOR_BUFFER_BIT | STENCIL_BUFFER_BIT) と同等
+ *
+ * @return {void}
*/
private clearNodeArea (): void
{
@@ -1615,7 +1820,10 @@ export class Context
/**
* @description 指定のノード範囲で描画を終了
+ * End rendering for the current node region
* レンダーパスは終了しない(次のbeginNodeRenderingで再利用するため)
+ *
+ * @return {void}
*/
endNodeRendering (): void
{
@@ -1634,6 +1842,9 @@ export class Context
/**
* @description 塗りの描画を実行
+ * Execute fill drawing
+ *
+ * @return {void}
*/
drawFill (): void
{
@@ -1650,6 +1861,15 @@ export class Context
/**
* @description インスタンスを描画
+ * Draw a display object instance
+ *
+ * @param {Node} node - 描画対象ノード / Target node
+ * @param {number} x_min - バウンディングボックス左端 / Bounding box left
+ * @param {number} y_min - バウンディングボックス上端 / Bounding box top
+ * @param {number} x_max - バウンディングボックス右端 / Bounding box right
+ * @param {number} y_max - バウンディングボックス下端 / Bounding box bottom
+ * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters
+ * @return {void}
*/
drawDisplayObject (
node: Node,
@@ -1681,10 +1901,11 @@ export class Context
* @description インスタンス配列を描画
* Draw instanced arrays
*
- * useOptimizedInstancingがtrueの場合、Storage BufferとIndirect Drawingを使用。
- * - Storage Buffer: メモリアロケーション削減、CPU負荷15-25%軽減
- * - Indirect Drawing: CPU-GPUオーバーヘッド5-15%削減
+ * useOptimizedInstancingがtrueの場合、Storage BufferとIndirect Drawingを使用。
+ * - Storage Buffer: メモリアロケーション削減、CPU負荷15-25%軽減
+ * - Indirect Drawing: CPU-GPUオーバーヘッド5-15%削減
*
+ * @return {void}
*/
drawArraysInstanced (): void
{
@@ -1741,28 +1962,11 @@ export class Context
this.processComplexBlendQueue();
}
- /**
- * @description 最適化インスタンス描画の有効/無効を設定
- * Enable or disable optimized instancing
- *
- */
- setOptimizedInstancing (enabled: boolean): void
- {
- this.useOptimizedInstancing = enabled;
- }
-
- /**
- * @description 最適化インスタンス描画が有効かどうか
- * Whether optimized instancing is enabled
- *
- */
- isOptimizedInstancingEnabled (): boolean
- {
- return this.useOptimizedInstancing;
- }
-
/**
* @description 複雑なブレンドモードのキューを処理
+ * Process the complex blend mode queue
+ *
+ * @return {void}
*/
private processComplexBlendQueue (): void
{
@@ -1783,6 +1987,9 @@ export class Context
/**
* @description インスタンス配列をクリア
+ * Clear instanced arrays
+ *
+ * @return {void}
*/
clearArraysInstanced (): void
{
@@ -1793,8 +2000,13 @@ export class Context
/**
* @description ピクセルバッファをNodeの指定箇所に転送
+ * Transfer pixel buffer to the specified position of the Node
* WebGPUでは、Shapeのシェーダーが-ndc.yでY軸反転しているため、
* Bitmapも同じ方向になるよう画像を上下反転して書き込む
+ *
+ * @param {Node} node - 描画対象ノード / Target node
+ * @param {Uint8Array} pixels - ピクセルデータ / Pixel data
+ * @return {void}
*/
drawPixels (node: Node, pixels: Uint8Array): void
{
@@ -1845,6 +2057,14 @@ export class Context
/**
* @description 一時テクスチャ経由でピクセルデータをMSAAテクスチャに描画
+ * Draw pixel data to MSAA texture via a temporary texture
+ *
+ * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object
+ * @param {Node} node - 描画対象ノード / Target node
+ * @param {Uint8Array} pixels - ピクセルデータ / Pixel data
+ * @param {number} width - 幅 / Width
+ * @param {number} height - 高さ / Height
+ * @return {void}
*/
private drawPixelsToMsaa (
attachment: IAttachmentObject,
@@ -1933,10 +2153,16 @@ export class Context
/**
* @description OffscreenCanvasをNodeの指定箇所に転送
+ * Transfer OffscreenCanvas to the specified position of the Node
* WebGPUでは、Shapeのシェーダーが-ndc.yでY軸反転しているため、
* Bitmapも同じ方向になるよう画像を上下反転して書き込む
+ *
+ * @param {Node} node - 描画対象ノード / Target node
+ * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw
+ * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag
+ * @return {void}
*/
- drawElement (node: Node, element: OffscreenCanvas | ImageBitmap, flipY: boolean = false): void
+ drawElement (node: Node, element: OffscreenCanvas | ImageBitmap, flip_y: boolean = false): void
{
// WebGPU draw element
// OffscreenCanvasまたはImageBitmapをアトラステクスチャに描画
@@ -1960,14 +2186,23 @@ export class Context
// MSAAが有効な場合は一時テクスチャ経由でMSAAテクスチャに直接描画
// MSAAが無効な場合もシェーダー経由で描画(WebGLと同じ処理フロー)
if (attachment.msaa && attachment.msaaTexture?.view) {
- this.drawElementToMsaa(attachment, node, element, width, height, flipY);
+ this.drawElementToMsaa(attachment, node, element, width, height, flip_y);
} else {
- this.drawElementToTexture(attachment, node, element, width, height, flipY);
+ this.drawElementToTexture(attachment, node, element, width, height, flip_y);
}
}
/**
* @description 一時テクスチャ経由でMSAAテクスチャに直接描画
+ * Draw to MSAA texture directly via a temporary texture
+ *
+ * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object
+ * @param {Node} node - 描画対象ノード / Target node
+ * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw
+ * @param {number} width - 幅 / Width
+ * @param {number} height - 高さ / Height
+ * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag
+ * @return {void}
*/
private drawElementToMsaa (
attachment: IAttachmentObject,
@@ -1975,7 +2210,7 @@ export class Context
element: OffscreenCanvas | ImageBitmap,
width: number,
height: number,
- flipY: boolean
+ flip_y: boolean
): void
{
// 一時テクスチャをプールから取得
@@ -1984,7 +2219,7 @@ export class Context
this.device.queue.copyExternalImageToTexture(
{
"source": element as ImageBitmap,
- "flipY": flipY
+ "flipY": flip_y
},
{
"texture": tempTexture,
@@ -2057,6 +2292,15 @@ export class Context
/**
* @description 一時テクスチャ経由で通常テクスチャに描画(非MSAA版)
+ * Draw to a regular texture via a temporary texture (non-MSAA version)
+ *
+ * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object
+ * @param {Node} node - 描画対象ノード / Target node
+ * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw
+ * @param {number} width - 幅 / Width
+ * @param {number} height - 高さ / Height
+ * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag
+ * @return {void}
*/
private drawElementToTexture (
attachment: IAttachmentObject,
@@ -2064,7 +2308,7 @@ export class Context
element: OffscreenCanvas | ImageBitmap,
width: number,
height: number,
- flipY: boolean
+ flip_y: boolean
): void
{
// 一時テクスチャをプールから取得
@@ -2075,7 +2319,7 @@ export class Context
this.device.queue.copyExternalImageToTexture(
{
"source": element as ImageBitmap,
- "flipY": flipY
+ "flipY": flip_y
},
{
"texture": tempTexture,
@@ -2148,6 +2392,20 @@ export class Context
/**
* @description フィルターを適用
+ * Apply filter effects
+ *
+ * @param {Node} node - 描画対象ノード / Target node
+ * @param {string} _unique_key - ユニークキー / Unique key
+ * @param {boolean} _updated - 更新フラグ / Updated flag
+ * @param {number} width - 幅 / Width
+ * @param {number} height - 高さ / Height
+ * @param {boolean} _is_bitmap - ビットマップかどうか / Whether it is a bitmap
+ * @param {Float32Array} matrix - 変換行列 / Transform matrix
+ * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters
+ * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode
+ * @param {Float32Array} bounds - バウンディングボックス / Bounding box
+ * @param {Float32Array} params - フィルターパラメータ / Filter parameters
+ * @return {void}
*/
applyFilter (
node: Node,
@@ -2203,6 +2461,9 @@ export class Context
* @description コンテナのフィルター/ブレンド用のレイヤーを開始
* Begin a container layer for filter/blend processing
*
+ * @param {number} width - レイヤー幅 / Layer width
+ * @param {number} height - レイヤー高さ / Layer height
+ * @return {void}
*/
containerBeginLayer (width: number, height: number): void
{
@@ -2243,14 +2504,15 @@ export class Context
* @description コンテナのフィルター/ブレンド用レイヤーを終了し、結果を元のメインに合成
* End the container layer and composite the result back to the original main
*
- * @param {IBlendMode} blend_mode
- * @param {Float32Array} matrix
- * @param {Float32Array | null} color_transform
- * @param {boolean} use_filter
- * @param {Float32Array | null} filter_bounds
- * @param {Float32Array | null} filter_params
- * @param {string} unique_key
- * @param {string} filter_key
+ * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode
+ * @param {Float32Array} matrix - 変換行列 / Transform matrix
+ * @param {Float32Array | null} color_transform - カラー変換パラメータ / Color transform parameters
+ * @param {boolean} use_filter - フィルター使用フラグ / Whether to use filter
+ * @param {Float32Array | null} filter_bounds - フィルターバウンド / Filter bounds
+ * @param {Float32Array | null} filter_params - フィルターパラメータ / Filter parameters
+ * @param {string} unique_key - ユニークキー / Unique key
+ * @param {string} filter_key - フィルターキー / Filter key
+ * @return {void}
*/
containerEndLayer (
blend_mode: IBlendMode,
@@ -2303,16 +2565,157 @@ export class Context
this.bind(this.$mainAttachmentObject);
}
+ /**
+ * @description cacheAsBitmap: temp FBO作成→子要素描画開始
+ * Begin container cacheAsBitmap: create temp bgra8unorm FBO for children,
+ * allocate atlas node for later copy
+ *
+ * @param {number} width - ノード幅 / Node width
+ * @param {number} height - ノード高さ / Node height
+ * @return {Node}
+ */
+ containerBeginAtlasNode (width: number, height: number): Node
+ {
+ this.drawArraysInstanced();
+
+ // アトラスノードを確保(後でコピー先として使用)
+ const node = this.createNode(width, height);
+ this._pendingAtlasNodes.push(node);
+
+ // mainをスタックに保存
+ const mainAttachment = this.$mainAttachmentObject as IAttachmentObject;
+ this.$containerLayerStack.push(mainAttachment);
+
+ // 既存のレンダーパスを終了
+ if (this.renderPassEncoder) {
+ this.renderPassEncoder.end();
+ this.renderPassEncoder = null;
+ }
+
+ // bgra8unormのtemp FBOを作成(全パイプラインが互換)
+ const tempAttachment = this.frameBufferManager.createAttachment(
+ "container_layer", width, height, mainAttachment.msaa, true
+ );
+
+ this.$mainAttachmentObject = tempAttachment;
+ this.bind(tempAttachment);
+
+ return node;
+ }
+
+ /**
+ * @description cacheAsBitmap: temp FBO→アトラスノードへコピー
+ * End container cacheAsBitmap: copy temp FBO content to atlas node,
+ * release temp FBO
+ *
+ * @return {void}
+ */
+ containerEndAtlasNode (): void
+ {
+ this.drawArraysInstanced();
+
+ // 既存のレンダーパスを終了(temp FBOのMSAAリゾルブが実行される)
+ if (this.renderPassEncoder) {
+ this.renderPassEncoder.end();
+ this.renderPassEncoder = null;
+ }
+
+ this.ensureCommandEncoder();
+
+ const tempAttachment = this.$mainAttachmentObject as IAttachmentObject;
+ const node = this._pendingAtlasNodes.pop()!;
+
+ // mainを復元
+ this.$mainAttachmentObject = this.$containerLayerStack.pop() as IAttachmentObject;
+
+ // temp FBO → アトラスノードへコピー
+ this.copyTempToAtlasNode(tempAttachment, node);
+
+ // temp FBOをリリース
+ this.frameBufferManager.releaseTemporaryAttachment(tempAttachment);
+
+ // メインをバインド
+ this.bind(this.$mainAttachmentObject);
+ }
+
+ /**
+ * @description temp FBOの内容をアトラスノード領域にコピー
+ * Copy temp FBO content to the atlas node region using texture_copy pipeline
+ *
+ * @param {IAttachmentObject} temp_attachment - コピー元のtemp FBO / Source temp FBO
+ * @param {Node} node - コピー先のアトラスノード / Destination atlas node
+ * @return {void}
+ */
+ private copyTempToAtlasNode (temp_attachment: IAttachmentObject, node: Node): void
+ {
+ const atlas = $getAtlasAttachmentObjectByIndex(node.index) || $getAtlasAttachmentObject();
+ if (!atlas || !atlas.texture || !temp_attachment.texture) {
+ return;
+ }
+
+ // アトラスはrgba8unormフォーマット(FrameBufferManagerCreateAttachmentUseCase参照)
+ // atlas_*テクスチャはcopyExternalImageToTextureとの互換性のためrgba8unormで作成される
+ const useMsaa = atlas.msaa && atlas.msaaTexture?.view;
+ const pipelineName = useMsaa ? "texture_copy_rgba8_msaa" : "texture_copy_rgba8";
+ const pipeline = this.pipelineManager.getPipeline(pipelineName);
+ const bindGroupLayout = this.pipelineManager.getBindGroupLayout("texture_copy");
+ if (!pipeline || !bindGroupLayout) {
+ return;
+ }
+
+ // uniform: temp FBO全体をサンプリング(Y軸反転してアトラスに格納)
+ // アトラスのShape描画はFillVertexのyFlipSign=1.0によりY反転で格納されるため、
+ // cacheAsBitmapのコピーも同じ規則に合わせてY反転する
+ // UV.y = texCoord.y * scaleY + offsetY = texCoord.y * (-1) + 1 = 1 - texCoord.y
+ const uniformBuffer = this.bufferManager.acquireAndWriteUniformBuffer($atlasNodeCopyUniform);
+
+ // サンプラーとソーステクスチャ(MSAAリゾルブ済みのテクスチャ)
+ const sampler = this.textureManager.createSampler("container_atlas_copy", false);
+ const srcView = temp_attachment.texture.view;
+
+ const bindGroup = this.device.createBindGroup({
+ "layout": bindGroupLayout,
+ "entries": [
+ { "binding": 0, "resource": { "buffer": uniformBuffer } },
+ { "binding": 1, "resource": sampler },
+ { "binding": 2, "resource": srcView }
+ ]
+ });
+
+ // アトラスのノード領域にレンダーパスを作成
+ const colorView = useMsaa ? atlas.msaaTexture!.view : atlas.texture.view;
+ const resolveTarget = useMsaa ? atlas.texture.view : null;
+
+ const renderPassDescriptor = this.frameBufferManager.createRenderPassDescriptor(
+ colorView, 0, 0, 0, 0, "load", resolveTarget
+ );
+
+ const passEncoder = this.commandEncoder!.beginRenderPass(renderPassDescriptor);
+
+ // ビューポートとシザーでノード領域に制限
+ passEncoder.setViewport(node.x, node.y, node.w, node.h, 0, 1);
+ passEncoder.setScissorRect(node.x, node.y, node.w, node.h);
+
+ passEncoder.setPipeline(pipeline);
+ passEncoder.setBindGroup(0, bindGroup);
+ passEncoder.draw(6, 1, 0, 0);
+ passEncoder.end();
+
+ // アトラスレンダーパスインデックスをリセット(次のbeginNodeRenderingで新規作成させる)
+ this.nodeRenderPassAtlasIndex = -1;
+ }
+
/**
* @description キャッシュされたコンテナフィルターテクスチャをメインに描画
* Draw a cached container filter texture to the main attachment
*
- * @param {IBlendMode} blend_mode
- * @param {Float32Array} matrix
- * @param {Float32Array} color_transform
- * @param {Float32Array} filter_bounds
- * @param {string} unique_key
- * @param {string} filter_key
+ * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode
+ * @param {Float32Array} matrix - 変換行列 / Transform matrix
+ * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters
+ * @param {Float32Array} filter_bounds - フィルターバウンド / Filter bounds
+ * @param {string} unique_key - ユニークキー / Unique key
+ * @param {string} filter_key - フィルターキー / Filter key
+ * @return {void}
*/
containerDrawCachedFilter (
blend_mode: IBlendMode,
@@ -2482,6 +2885,9 @@ export class Context
/**
* @description メインテクスチャを確保(フレーム開始時に一度だけgetCurrentTexture呼び出し)
+ * Ensure the main texture is acquired (calls getCurrentTexture once per frame)
+ *
+ * @return {void}
*/
private ensureMainTexture(): void
{
@@ -2503,6 +2909,9 @@ export class Context
/**
* @description 現在の描画ターゲットのテクスチャビューを取得
+ * Get the texture view of the current render target
+ *
+ * @return {GPUTextureView} 現在のテクスチャビュー / Current texture view
*/
private getCurrentTextureView(): GPUTextureView
{
@@ -2518,6 +2927,9 @@ export class Context
/**
* @description コマンドエンコーダーが存在することを保証
+ * Ensure the command encoder exists
+ *
+ * @return {void}
*/
private ensureCommandEncoder(): void
{
@@ -2531,6 +2943,9 @@ export class Context
/**
* @description フレーム開始(レンダリング開始前に呼ぶ)
+ * Begin a new frame (call before rendering starts)
+ *
+ * @return {void}
*/
beginFrame(): void
{
@@ -2547,6 +2962,10 @@ export class Context
/**
* @description フレームごとのプール管理テクスチャを追加(endFrame()でプールに返却)
+ * Add a pooled texture for the current frame (returned to pool in endFrame())
+ *
+ * @param {GPUTexture} texture - プール管理テクスチャ / Pooled texture
+ * @return {void}
*/
addFrameTexture (texture: GPUTexture): void
{
@@ -2555,6 +2974,9 @@ export class Context
/**
* @description フレーム終了とコマンド送信(レンダリング完了後に呼ぶ)
+ * End the frame and submit commands (call after rendering is complete)
+ *
+ * @return {void}
*/
endFrame(): void
{
@@ -2626,6 +3048,9 @@ export class Context
/**
* @description コマンドを送信(後方互換性のため残す)
+ * Submit commands (kept for backward compatibility)
+ *
+ * @return {void}
*/
submit (): void
{
@@ -2634,7 +3059,12 @@ export class Context
/**
* @description ノードを作成
+ * Create a node in the texture atlas
* アトラスがいっぱいの場合は新しいアトラスを作成して再試行
+ *
+ * @param {number} width - ノード幅 / Node width
+ * @param {number} height - ノード高さ / Node height
+ * @return {Node} 作成されたノード / Created node
*/
createNode (width: number, height: number): Node
{
@@ -2660,6 +3090,10 @@ export class Context
/**
* @description ノードを削除
+ * Remove a node from the texture atlas
+ *
+ * @param {Node} node - 削除対象ノード / Node to remove
+ * @return {void}
*/
removeNode (node: Node): void
{
@@ -2674,7 +3108,10 @@ export class Context
/**
* @description フレームバッファの描画情報をキャンバスに転送
+ * Transfer frame buffer contents to the canvas
* スワップチェーンはCopyDstをサポートしないため、レンダーパスでブリット
+ *
+ * @return {void}
*/
transferMainCanvas (): void
{
@@ -2743,6 +3180,11 @@ export class Context
/**
* @description ImageBitmapを生成
+ * Create an ImageBitmap from the current rendering result
+ *
+ * @param {number} width - 画像幅 / Image width
+ * @param {number} height - 画像高さ / Image height
+ * @return {Promise} 生成されたImageBitmap / Created ImageBitmap
*/
async createImageBitmap (width: number, height: number): Promise
{
@@ -2865,6 +3307,7 @@ export class Context
* @description マスク描画の開始準備
* Prepare to start drawing the mask
*
+ * @return {void}
*/
beginMask(): void
{
@@ -2932,10 +3375,11 @@ export class Context
* @description マスクの描画範囲を設定
* Set the mask drawing bounds
*
- * @param {number} x_min
- * @param {number} y_min
- * @param {number} x_max
- * @param {number} y_max
+ * @param {number} x_min - 最小X座標 / Minimum X coordinate
+ * @param {number} y_min - 最小Y座標 / Minimum Y coordinate
+ * @param {number} x_max - 最大X座標 / Maximum X coordinate
+ * @param {number} y_max - 最大Y座標 / Maximum Y coordinate
+ * @return {void}
*/
setMaskBounds(
x_min: number,
@@ -2950,6 +3394,7 @@ export class Context
* @description マスクの描画を終了
* End mask drawing
*
+ * @return {void}
*/
endMask(): void
{
@@ -2967,8 +3412,9 @@ export class Context
/**
* @description マスクの終了処理
- * Mask end processing
+ * Mask end processing (leave the mask)
*
+ * @return {void}
*/
leaveMask(): void
{
diff --git a/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts b/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts
index 1d0f48b2..1b4cd789 100644
--- a/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts
+++ b/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts
@@ -1,3 +1,10 @@
+/**
+ * @description ビットマップ行列とコンテキスト行列からテクスチャマッピング用の逆行列を計算する
+ * Computes the inverse matrix for texture mapping from bitmap and context matrices
+ * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix
+ * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix
+ * @return {Float32Array} 列優先形式の3x3逆行列 / Column-major 3x3 inverse matrix
+ */
export const execute = (bitmap_matrix: Float32Array, context_matrix: Float32Array): Float32Array => {
// ビットマップ行列 [a, b, c, d, tx, ty]
const ba = bitmap_matrix[0];
diff --git a/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts b/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts
index fb90e4e3..d3142308 100644
--- a/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts
+++ b/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts
@@ -1,15 +1,23 @@
+/**
+ * @description グラデーション行列からグラデーション描画用の逆行列とリニアポイントを計算する
+ * Computes inverse matrix and linear points for gradient rendering from gradient matrix
+ * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix
+ * @param {Float32Array} _context_matrix コンテキスト変換行列(未使用) / Context transformation matrix (unused)
+ * @param {number} type グラデーションタイプ (0: linear, 1: radial) / Gradient type (0: linear, 1: radial)
+ * @return {{ inverseMatrix: Float32Array; linearPoints: Float32Array | null }} 逆行列とリニアポイント / Inverse matrix and linear points
+ */
export const execute = (
- gradientMatrix: Float32Array,
- _contextMatrix: Float32Array,
+ gradient_matrix: Float32Array,
+ _context_matrix: Float32Array,
type: number
): { inverseMatrix: Float32Array; linearPoints: Float32Array | null } => {
// グラデーション行列
- const ga = gradientMatrix[0];
- const gb = gradientMatrix[1];
- const gc = gradientMatrix[2];
- const gd = gradientMatrix[3];
- const gtx = gradientMatrix[4];
- const gty = gradientMatrix[5];
+ const ga = gradient_matrix[0];
+ const gb = gradient_matrix[1];
+ const gc = gradient_matrix[2];
+ const gd = gradient_matrix[3];
+ const gtx = gradient_matrix[4];
+ const gty = gradient_matrix[5];
if (type === 0) {
// === Linear gradient ===
diff --git a/packages/webgpu/src/Context/service/ContextFillSimpleService.ts b/packages/webgpu/src/Context/service/ContextFillSimpleService.ts
index b6c48365..397e605c 100644
--- a/packages/webgpu/src/Context/service/ContextFillSimpleService.ts
+++ b/packages/webgpu/src/Context/service/ContextFillSimpleService.ts
@@ -1,6 +1,20 @@
import type { PipelineManager } from "../../Shader/PipelineManager";
import { $isMaskDrawing, $getMaskStencilReference } from "../../Mask";
+/**
+ * @description シンプルなフィル描画を実行する
+ * Executes simple fill rendering
+ * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer
+ * @param {number} vertex_count 頂点数 / Vertex count
+ * @param {GPUBindGroup} bind_group バインドグループ / Bind group
+ * @param {number} uniform_offset ユニフォームオフセット / Uniform offset
+ * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target
+ * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline
+ * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused)
+ * @return {void}
+ */
export const execute = (
render_pass_encoder: GPURenderPassEncoder,
pipeline_manager: PipelineManager,
diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts
new file mode 100644
index 00000000..8be54d7c
--- /dev/null
+++ b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts
@@ -0,0 +1,103 @@
+import { execute } from "./ContextFillWithStencilMainService";
+import { describe, expect, it, vi } from "vitest";
+
+describe("ContextFillWithStencilMainService.js test", () => {
+
+ const createMockRenderPassEncoder = () => ({
+ "setPipeline": vi.fn(),
+ "setVertexBuffer": vi.fn(),
+ "setStencilReference": vi.fn(),
+ "setBindGroup": vi.fn(),
+ "draw": vi.fn()
+ }) as unknown as GPURenderPassEncoder;
+
+ const createMockPipelineManager = (hasWrite = true, hasFill = true) => ({
+ "getPipeline": vi.fn((name: string) => {
+ if (name === "stencil_write_main" && hasWrite) {
+ return { "label": "stencil_write_main" };
+ }
+ if (name === "stencil_fill_main" && hasFill) {
+ return { "label": "stencil_fill_main" };
+ }
+ return null;
+ })
+ }) as any;
+
+ it("execute test case1 - stencil write pass", () =>
+ {
+ const encoder = createMockRenderPassEncoder();
+ const pipelineManager = createMockPipelineManager();
+ const vertexBuffer = {} as unknown as GPUBuffer;
+ const bindGroup = {} as unknown as GPUBindGroup;
+
+ execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0);
+
+ expect(pipelineManager.getPipeline).toHaveBeenCalledWith("stencil_write_main");
+ expect(encoder.setStencilReference).toHaveBeenCalledWith(0);
+ expect(encoder.setVertexBuffer).toHaveBeenCalledWith(0, vertexBuffer);
+ });
+
+ it("execute test case2 - stencil fill pass", () =>
+ {
+ const encoder = createMockRenderPassEncoder();
+ const pipelineManager = createMockPipelineManager();
+ const vertexBuffer = {} as unknown as GPUBuffer;
+ const bindGroup = {} as unknown as GPUBindGroup;
+
+ execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0);
+
+ expect(pipelineManager.getPipeline).toHaveBeenCalledWith("stencil_fill_main");
+ });
+
+ it("execute test case3 - both passes execute in order", () =>
+ {
+ const encoder = createMockRenderPassEncoder();
+ const pipelineManager = createMockPipelineManager();
+ const vertexBuffer = {} as unknown as GPUBuffer;
+ const bindGroup = {} as unknown as GPUBindGroup;
+
+ execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0);
+
+ expect(pipelineManager.getPipeline).toHaveBeenCalledTimes(2);
+ expect(encoder.setPipeline).toHaveBeenCalledTimes(2);
+ expect(encoder.draw).toHaveBeenCalledTimes(2);
+ expect(encoder.draw).toHaveBeenCalledWith(12, 1, 0, 0);
+ });
+
+ it("execute test case4 - bind group with dynamic offset", () =>
+ {
+ const encoder = createMockRenderPassEncoder();
+ const pipelineManager = createMockPipelineManager();
+ const vertexBuffer = {} as unknown as GPUBuffer;
+ const bindGroup = {} as unknown as GPUBindGroup;
+
+ execute(encoder, pipelineManager, vertexBuffer, 6, bindGroup, 256);
+
+ expect(encoder.setBindGroup).toHaveBeenCalledWith(0, bindGroup, [256]);
+ });
+
+ it("execute test case5 - no write pipeline available", () =>
+ {
+ const encoder = createMockRenderPassEncoder();
+ const pipelineManager = createMockPipelineManager(false, true);
+ const vertexBuffer = {} as unknown as GPUBuffer;
+ const bindGroup = {} as unknown as GPUBindGroup;
+
+ execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0);
+
+ // write pass skipped, but fill pass should still execute
+ expect(encoder.draw).toHaveBeenCalledTimes(1);
+ });
+
+ it("execute test case6 - no pipelines available", () =>
+ {
+ const encoder = createMockRenderPassEncoder();
+ const pipelineManager = createMockPipelineManager(false, false);
+ const vertexBuffer = {} as unknown as GPUBuffer;
+ const bindGroup = {} as unknown as GPUBindGroup;
+
+ execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0);
+
+ expect(encoder.draw).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts
index ce39f071..35ab516a 100644
--- a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts
+++ b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts
@@ -1,5 +1,16 @@
import type { PipelineManager } from "../../Shader/PipelineManager";
+/**
+ * @description メインキャンバス向けのステンシル書き込みとフィル描画を2パスで実行する
+ * Executes two-pass stencil write and fill rendering for the main canvas
+ * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer
+ * @param {number} vertex_count 頂点数 / Vertex count
+ * @param {GPUBindGroup} bind_group バインドグループ / Bind group
+ * @param {number} uniform_offset ユニフォームオフセット / Uniform offset
+ * @return {void}
+ */
export const execute = (
render_pass_encoder: GPURenderPassEncoder,
pipeline_manager: PipelineManager,
diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts
index d6a68ae9..b64cbe5c 100644
--- a/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts
+++ b/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts
@@ -1,5 +1,16 @@
import type { PipelineManager } from "../../Shader/PipelineManager";
+/**
+ * @description アトラスターゲット向けのステンシル書き込みとフィル描画を2パスで実行する
+ * Executes two-pass stencil write and fill rendering for the atlas target
+ * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer
+ * @param {number} vertex_count 頂点数 / Vertex count
+ * @param {GPUBindGroup} bind_group バインドグループ / Bind group
+ * @param {number} uniform_offset ユニフォームオフセット / Uniform offset
+ * @return {void}
+ */
export const execute = (
render_pass_encoder: GPURenderPassEncoder,
pipeline_manager: PipelineManager,
diff --git a/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts b/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts
index 37b789c3..4f17dd6c 100644
--- a/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts
@@ -17,36 +17,90 @@ import { execute as filterApplyGradientBevelFilterUseCase } from "../../Filter/G
import { execute as filterApplyGradientGlowFilterUseCase } from "../../Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase";
import { execute as filterApplyDisplacementMapFilterUseCase } from "../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase";
import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/BlendApplyComplexBlendUseCase";
-
+import {
+ $isMaskTestEnabled,
+ $getMaskStencilReference
+} from "../../Mask";
+
+/**
+ * @description ユニフォームデータの事前確保配列(4要素)
+ * Pre-allocated uniform data array (4 elements)
+ */
const $uniform4 = new Float32Array(4);
+/**
+ * @description ユニフォームデータの事前確保配列A(6要素)
+ * Pre-allocated uniform data array A (6 elements)
+ */
const $uniform6a = new Float32Array(6);
+/**
+ * @description ユニフォームデータの事前確保配列B(6要素)
+ * Pre-allocated uniform data array B (6 elements)
+ */
const $uniform6b = new Float32Array(6);
+/**
+ * @description ユニフォームデータの事前確保配列(8要素)
+ * Pre-allocated uniform data array (8 elements)
+ */
const $uniform8 = new Float32Array(8);
+/**
+ * @description ユニフォームデータの事前確保配列(12要素)
+ * Pre-allocated uniform data array (12 elements)
+ */
const $uniform12 = new Float32Array(12);
+/**
+ * @description ユニフォームデータの事前確保配列(20要素)
+ * Pre-allocated uniform data array (20 elements)
+ */
const $uniform20 = new Float32Array(20);
// プリアロケート BindGroup Entry 配列
+/**
+ * @description バインドグループエントリの事前確保配列
+ * Pre-allocated bind group entry array
+ */
const $entries3: GPUBindGroupEntry[] = [
{ "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
{ "binding": 1, "resource": null as unknown as GPUSampler },
{ "binding": 2, "resource": null as unknown as GPUTextureView }
];
-const SIMPLE_BLEND_MODES: ReadonlySet = new Set([
+/**
+ * @description シンプルなブレンドモードのセット
+ * Set of simple blend modes
+ */
+const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([
"normal", "layer", "add", "screen", "alpha", "erase", "copy"
] as IBlendMode[]);
-const Y_FLIP_UNIFORM = new Float32Array([1, -1, 0, 1]);
-
-const isIdentityColorTransform = (ct: Float32Array): boolean => {
+/**
+ * @description Y軸反転用ユニフォームデータ
+ * Uniform data for Y-axis flip
+ */
+const $Y_FLIP_UNIFORM = new Float32Array([1, -1, 0, 1]);
+
+/**
+ * @description カラートランスフォームが恒等変換かどうかを判定する
+ * Checks whether the color transform is an identity transform
+ * @param {Float32Array} ct カラートランスフォーム配列 / Color transform array
+ * @return {boolean} 恒等変換の場合true / True if identity transform
+ */
+const $isIdentityColorTransform = (ct: Float32Array): boolean => {
return ct[0] === 1 && ct[1] === 1 && ct[2] === 1 && ct[3] === 1
&& ct[4] === 0 && ct[5] === 0 && ct[6] === 0 && ct[7] === 0;
};
-const applyColorTransform = (
+/**
+ * @description フィルター結果にカラートランスフォームを適用する
+ * Applies color transform to the filter result
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {IAttachmentObject} attachment ソースアタッチメント / Source attachment
+ * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array
+ * @return {IAttachmentObject} カラートランスフォーム適用後のアタッチメント / Attachment with color transform applied
+ */
+const $applyColorTransform = (
config: ILocalFilterConfig,
attachment: IAttachmentObject,
- colorTransform: Float32Array
+ color_transform: Float32Array
): IAttachmentObject => {
const ctAttachment = config.frameBufferManager.createTemporaryAttachment(
attachment.width, attachment.height
@@ -61,13 +115,13 @@ const applyColorTransform = (
// uniform: mul(vec4) + add(vec4) = 32 bytes
// add値は0-255スケールの生値をそのまま渡す(WebGLのフィルターCTパスと同じ)
- $uniform8[0] = colorTransform[0];
- $uniform8[1] = colorTransform[1];
- $uniform8[2] = colorTransform[2];
- $uniform8[3] = colorTransform[3];
- $uniform8[4] = colorTransform[4];
- $uniform8[5] = colorTransform[5];
- $uniform8[6] = colorTransform[6];
+ $uniform8[0] = color_transform[0];
+ $uniform8[1] = color_transform[1];
+ $uniform8[2] = color_transform[2];
+ $uniform8[3] = color_transform[3];
+ $uniform8[4] = color_transform[4];
+ $uniform8[5] = color_transform[5];
+ $uniform8[6] = color_transform[6];
$uniform8[7] = 0;
const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8);
@@ -94,7 +148,15 @@ const applyColorTransform = (
return ctAttachment;
};
-const getTextureFromNode = (
+/**
+ * @description アトラスノードからテクスチャを取得する
+ * Extracts texture from an atlas node
+ * @param {Node} node テクスチャパッカーノード / Texture packer node
+ * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder
+ * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager
+ * @return {IAttachmentObject} 取得したアタッチメント / Extracted attachment
+ */
+const $getTextureFromNode = (
node: Node,
command_encoder: GPUCommandEncoder,
frame_buffer_manager: FrameBufferManager
@@ -122,19 +184,36 @@ const getTextureFromNode = (
}
);
} else {
- console.error("[WebGPU Filter] getTextureFromNode: FAILED - missing atlas or textures");
+ console.error("[WebGPU Filter] $getTextureFromNode: FAILED - missing atlas or textures");
}
return attachment;
};
-const isSimpleBlendMode = (blendMode: IBlendMode): boolean => {
- return SIMPLE_BLEND_MODES.has(blendMode);
+/**
+ * @description ブレンドモードがシンプルかどうかを判定する
+ * Checks whether the blend mode is a simple blend mode
+ * @param {IBlendMode} blend_mode ブレンドモード / Blend mode
+ * @return {boolean} シンプルブレンドモードの場合true / True if simple blend mode
+ */
+const $isSimpleBlendMode = (blend_mode: IBlendMode): boolean => {
+ return $SIMPLE_BLEND_MODES.has(blend_mode);
};
-const copyMainAttachmentRegion = (
+/**
+ * @description メインアタッチメントの矩形領域をコピーする
+ * Copies a rectangular region from the main attachment
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment
+ * @param {number} x X座標 / X coordinate
+ * @param {number} y Y座標 / Y coordinate
+ * @param {number} width 幅 / Width
+ * @param {number} height 高さ / Height
+ * @return {IAttachmentObject} コピーされたアタッチメント / Copied attachment
+ */
+const $copyMainAttachmentRegion = (
config: ILocalFilterConfig,
- mainAttachment: IAttachmentObject,
+ main_attachment: IAttachmentObject,
x: number,
y: number,
width: number,
@@ -147,15 +226,15 @@ const copyMainAttachmentRegion = (
const pipeline = config.pipelineManager.getPipeline("complex_blend_copy");
const bindGroupLayout = config.pipelineManager.getBindGroupLayout("texture_copy");
- if (!pipeline || !bindGroupLayout || !mainAttachment.texture || !dstAttachment.texture) {
+ if (!pipeline || !bindGroupLayout || !main_attachment.texture || !dstAttachment.texture) {
return dstAttachment;
}
// ユニフォームバッファ: scale (vec2) + offset (vec2)
- const scaleX = width / mainAttachment.width;
- const scaleY = height / mainAttachment.height;
- const offsetX = x / mainAttachment.width;
- const offsetY = y / mainAttachment.height;
+ const scaleX = width / main_attachment.width;
+ const scaleY = height / main_attachment.height;
+ const offsetX = x / main_attachment.width;
+ const offsetY = y / main_attachment.height;
$uniform4[0] = scaleX;
$uniform4[1] = scaleY;
@@ -167,7 +246,7 @@ const copyMainAttachmentRegion = (
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
- $entries3[2].resource = mainAttachment.texture.view;
+ $entries3[2].resource = main_attachment.texture.view;
const bindGroup = config.device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries3
@@ -188,31 +267,41 @@ const copyMainAttachmentRegion = (
return dstAttachment;
};
-const drawBlendResultToMain = (
+/**
+ * @description ブレンド結果をメインアタッチメントに描画する
+ * Draws blend result to the main attachment
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment
+ * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment
+ * @param {number} x X座標 / X coordinate
+ * @param {number} y Y座標 / Y coordinate
+ * @return {void}
+ */
+const $drawBlendResultToMain = (
config: ILocalFilterConfig,
- srcAttachment: IAttachmentObject,
- mainAttachment: IAttachmentObject,
+ src_attachment: IAttachmentObject,
+ main_attachment: IAttachmentObject,
x: number,
y: number
): void => {
// フィルター+複雑なブレンド用のパイプライン(Y軸反転あり)を使用
// MSAA有効時はMSAA版パイプラインを使用してmsaaTextureに描画→texture.viewにresolve
- const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view;
+ const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view;
const pipelineName = useMsaa ? "filter_complex_blend_output_msaa" : "filter_complex_blend_output";
const pipeline = config.pipelineManager.getPipeline(pipelineName);
const bindGroupLayout = config.pipelineManager.getBindGroupLayout("positioned_texture");
- if (!pipeline || !bindGroupLayout || !srcAttachment.texture || !mainAttachment.texture) {
+ if (!pipeline || !bindGroupLayout || !src_attachment.texture || !main_attachment.texture) {
return;
}
// ユニフォームデータ: offset, size, viewport, padding
$uniform8[0] = x;
$uniform8[1] = y;
- $uniform8[2] = srcAttachment.width;
- $uniform8[3] = srcAttachment.height;
- $uniform8[4] = mainAttachment.width;
- $uniform8[5] = mainAttachment.height;
+ $uniform8[2] = src_attachment.width;
+ $uniform8[3] = src_attachment.height;
+ $uniform8[4] = main_attachment.width;
+ $uniform8[5] = main_attachment.height;
$uniform8[6] = 0;
$uniform8[7] = 0;
const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8);
@@ -221,7 +310,7 @@ const drawBlendResultToMain = (
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
- $entries3[2].resource = srcAttachment.texture.view;
+ $entries3[2].resource = src_attachment.texture.view;
const bindGroup = config.device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries3
@@ -229,8 +318,8 @@ const drawBlendResultToMain = (
// メインアタッチメントへの描画(loadで既存内容を保持)
// MSAA有効時はmsaaTextureに描画してtexture.viewにresolve
- const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view;
- const resolveTarget = useMsaa ? mainAttachment.texture.view : null;
+ const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view;
+ const resolveTarget = useMsaa ? main_attachment.texture.view : null;
const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor(
colorView,
0, 0, 0, 0,
@@ -245,7 +334,20 @@ const drawBlendResultToMain = (
passEncoder.end();
};
-const drawFilterToMain = (
+/**
+ * @description フィルター適用結果をメインキャンバスに描画する
+ * Draws filter result to the main canvas
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment
+ * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array
+ * @param {IBlendMode} blend_mode ブレンドモード / Blend mode
+ * @param {number} x X座標 / X coordinate
+ * @param {number} y Y座標 / Y coordinate
+ * @param {GPUTextureView} _main_texture_view メインテクスチャビュー(未使用) / Main texture view (unused)
+ * @param {BufferManager} _buffer_manager バッファマネージャ(未使用) / Buffer manager (unused)
+ * @return {void}
+ */
+const $drawFilterToMain = (
config: ILocalFilterConfig,
filter_attachment: IAttachmentObject,
color_transform: Float32Array,
@@ -300,32 +402,42 @@ const drawFilterToMain = (
}
// シンプルなブレンドモードの場合
- if (isSimpleBlendMode(blend_mode)) {
+ if ($isSimpleBlendMode(blend_mode)) {
// MSAA有効時はMSAA版パイプラインを使用してmsaaTextureに描画→texture.viewにresolve
const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view;
+ // マスクが有効かチェック
+ const isMasked = $isMaskTestEnabled();
+ const useStencil = isMasked
+ && (mainAttachment.msaaStencil?.view || mainAttachment.stencil?.view);
+
// ブレンドモードに応じたパイプラインを選択
let pipelineName: string;
- switch (blend_mode) {
- case "add":
- pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add";
- break;
- case "screen":
- pipelineName = useMsaa ? "filter_output_screen_msaa" : "filter_output_screen";
- break;
- case "alpha":
- pipelineName = useMsaa ? "filter_output_alpha_msaa" : "filter_output_alpha";
- break;
- case "erase":
- pipelineName = useMsaa ? "filter_output_erase_msaa" : "filter_output_erase";
- break;
- case "copy":
- pipelineName = useMsaa ? "texture_copy_bgra_msaa" : "texture_copy_bgra";
- break;
- default:
- // normal, layer
- pipelineName = useMsaa ? "filter_output_msaa" : "filter_output";
- break;
+ if (useStencil) {
+ // マスク有効時はステンシルテスト付きパイプラインを使用
+ pipelineName = useMsaa ? "filter_output_masked_msaa" : "filter_output_masked";
+ } else {
+ switch (blend_mode) {
+ case "add":
+ pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add";
+ break;
+ case "screen":
+ pipelineName = useMsaa ? "filter_output_screen_msaa" : "filter_output_screen";
+ break;
+ case "alpha":
+ pipelineName = useMsaa ? "filter_output_alpha_msaa" : "filter_output_alpha";
+ break;
+ case "erase":
+ pipelineName = useMsaa ? "filter_output_erase_msaa" : "filter_output_erase";
+ break;
+ case "copy":
+ pipelineName = useMsaa ? "texture_copy_bgra_msaa" : "texture_copy_bgra";
+ break;
+ default:
+ // normal, layer
+ pipelineName = useMsaa ? "filter_output_msaa" : "filter_output";
+ break;
+ }
}
let pipeline = config.pipelineManager.getPipeline(pipelineName);
@@ -365,12 +477,27 @@ const drawFilterToMain = (
// MSAA有効時はmsaaTextureに描画してtexture.viewにresolve
const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view;
const resolveTarget = useMsaa ? mainAttachment.texture.view : null;
- const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor(
- colorView,
- 0, 0, 0, 0,
- "load",
- resolveTarget
- );
+
+ let renderPassDescriptor: GPURenderPassDescriptor;
+ if (useStencil) {
+ // マスク有効時はステンシル付きレンダーパスを使用
+ const stencilView = useMsaa && mainAttachment.msaaStencil?.view
+ ? mainAttachment.msaaStencil.view : mainAttachment.stencil!.view;
+ renderPassDescriptor = config.frameBufferManager.createStencilRenderPassDescriptor(
+ colorView,
+ stencilView,
+ "load",
+ "load",
+ resolveTarget
+ );
+ } else {
+ renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor(
+ colorView,
+ 0, 0, 0, 0,
+ "load",
+ resolveTarget
+ );
+ }
// Viewportはfloat値でサブピクセル精度を維持(WebGLのsetTransform相当)
// ScissorはGPUIntegerCoordinate必須のため整数化し、viewport領域を包含する
@@ -391,6 +518,10 @@ const drawFilterToMain = (
const passEncoder = config.commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
+ // マスク有効時はステンシル参照値を設定
+ if (useStencil) {
+ passEncoder.setStencilReference($getMaskStencilReference());
+ }
passEncoder.setViewport(vpX, vpY, vpW, vpH, 0, 1);
passEncoder.setScissorRect(scissorX, scissorY, scissorW, scissorH);
passEncoder.draw(6, 1, 0, 0);
@@ -398,7 +529,7 @@ const drawFilterToMain = (
} else {
// 複雑なブレンドモード(multiply, overlay, darken, lighten, hardlight等)
// 1. メインアタッチメントから描画先の矩形をコピー
- const dstAttachment = copyMainAttachmentRegion(
+ const dstAttachment = $copyMainAttachmentRegion(
config, mainAttachment,
drawX, drawY, drawWidth, drawHeight
);
@@ -431,7 +562,7 @@ const drawFilterToMain = (
);
// 4. 結果をメインアタッチメントに描画
- drawBlendResultToMain(
+ $drawBlendResultToMain(
config,
blendedAttachment,
mainAttachment,
@@ -445,6 +576,23 @@ const drawFilterToMain = (
}
};
+/**
+ * @description フィルターを適用してメインキャンバスに描画する
+ * Applies filters and draws the result to the main canvas
+ * @param {Node} node テクスチャパッカーノード / Texture packer node
+ * @param {number} width 幅 / Width
+ * @param {number} height 高さ / Height
+ * @param {boolean} is_bitmap ビットマップフラグ / Whether the source is a bitmap
+ * @param {Float32Array} matrix 変換行列 / Transformation matrix
+ * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array
+ * @param {IBlendMode} blend_mode ブレンドモード / Blend mode
+ * @param {Float32Array} bounds バウンディングボックス / Bounding box
+ * @param {Float32Array} params フィルターパラメータ配列 / Filter parameters array
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {GPUTextureView} main_texture_view メインテクスチャビュー / Main texture view
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @return {void}
+ */
export const execute = (
node: Node,
width: number,
@@ -464,7 +612,7 @@ export const execute = (
$offset.y = 0;
// ノードからテクスチャを取得
- let filterAttachment = getTextureFromNode(node, config.commandEncoder, config.frameBufferManager);
+ let filterAttachment = $getTextureFromNode(node, config.commandEncoder, config.frameBufferManager);
// アトラスのY反転を補正
// WebGPUではアトラスに描画する際にY軸が反転して格納される:
@@ -480,7 +628,7 @@ export const execute = (
const sampler = config.textureManager.createSampler("filter_flip_sampler", false);
// scale=(1, -1), offset=(0, 1) で UV.y = texCoord.y * (-1) + 1 = 1 - texCoord.y
- const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer(Y_FLIP_UNIFORM);
+ const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($Y_FLIP_UNIFORM);
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
@@ -906,8 +1054,8 @@ export const execute = (
// ColorTransformが恒等変換でない場合、フィルター結果に適用
// WebGL版と同じ: フィルターチェーン適用後、メイン描画前にColorTransformを適用
- if (!isIdentityColorTransform(color_transform)) {
- const ctAttachment = applyColorTransform(config, filterAttachment, color_transform);
+ if (!$isIdentityColorTransform(color_transform)) {
+ const ctAttachment = $applyColorTransform(config, filterAttachment, color_transform);
config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
filterAttachment = ctAttachment;
}
@@ -921,7 +1069,7 @@ export const execute = (
const drawX = -offsetX + xMin + matrix[4];
const drawY = -offsetY + yMin + matrix[5];
- drawFilterToMain(
+ $drawFilterToMain(
config,
filterAttachment,
color_transform,
diff --git a/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts b/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts
index a92aaa51..501550ce 100644
--- a/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts
@@ -9,20 +9,67 @@ import {
$getMaskStencilReference
} from "../../Mask";
+/**
+ * @description ビットマップサンプラーのキャッシュ
+ * Cache for bitmap samplers
+ */
const $bitmapSamplerCache = new Map();
+/**
+ * @description ユニフォームデータの事前確保配列(32要素)
+ * Pre-allocated uniform data array (32 elements)
+ */
const $uniformData32 = new Float32Array(32);
+/**
+ * @description ステンシルデータの事前確保配列(16要素)
+ * Pre-allocated stencil data array (16 elements)
+ */
const $stencilData16 = new Float32Array(16);
+/**
+ * @description ステンシル用動的バインドグループのキャッシュ
+ * Cached dynamic bind group for stencil operations
+ */
let $stencilDynamicBindGroup: GPUBindGroup | null = null;
+/**
+ * @description ステンシル用動的バッファのキャッシュ
+ * Cached dynamic buffer for stencil operations
+ */
let $stencilDynamicBuffer: GPUBuffer | null = null;
+/**
+ * @description バインドグループエントリの事前確保配列
+ * Pre-allocated bind group entry array
+ */
const $entries3: GPUBindGroupEntry[] = [
{ "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
{ "binding": 1, "resource": null as unknown as GPUSampler },
{ "binding": 2, "resource": null as unknown as GPUTextureView }
];
+/**
+ * @description ビットマップフィル描画を実行する
+ * Executes bitmap fill rendering
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array
+ * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix
+ * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA)
+ * @param {Uint8Array} pixels ピクセルデータ / Pixel data
+ * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix
+ * @param {number} width テクスチャ幅 / Texture width
+ * @param {number} height テクスチャ高さ / Texture height
+ * @param {boolean} repeat リピート有無 / Whether to repeat
+ * @param {boolean} smooth スムーズフィルタ有無 / Whether to use smooth filtering
+ * @param {number} viewport_width ビューポート幅 / Viewport width
+ * @param {number} viewport_height ビューポート高さ / Viewport height
+ * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target
+ * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline
+ * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused)
+ * @return {GPUTexture | null} ビットマップテクスチャまたはnull / Bitmap texture or null
+ */
export const execute = (
device: GPUDevice,
render_pass_encoder: GPURenderPassEncoder,
diff --git a/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts b/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts
index eaed4048..6a4a3cf6 100644
--- a/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts
@@ -5,16 +5,51 @@ import { execute as meshBitmapStrokeGenerateUseCase } from "../../Mesh/usecase/M
import { execute as contextComputeBitmapMatrixService } from "../service/ContextComputeBitmapMatrixService";
import { $acquireFillTexture, $releaseFillTexture } from "../../FillTexturePool";
+/**
+ * @description ビットマップサンプラーのキャッシュ
+ * Cache for bitmap samplers
+ */
const $bitmapSamplerCache = new Map();
+/**
+ * @description ユニフォームデータの事前確保配列(32要素)
+ * Pre-allocated uniform data array (32 elements)
+ */
const $uniformData32 = new Float32Array(32);
+/**
+ * @description バインドグループエントリの事前確保配列
+ * Pre-allocated bind group entry array
+ */
const $entries3: GPUBindGroupEntry[] = [
{ "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
{ "binding": 1, "resource": null as unknown as GPUSampler },
{ "binding": 2, "resource": null as unknown as GPUTextureView }
];
+/**
+ * @description ビットマップストローク描画を実行する
+ * Executes bitmap stroke rendering
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {IPath[]} vertices パス頂点配列 / Path vertices array
+ * @param {number} thickness ストローク太さ / Stroke thickness
+ * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix
+ * @param {Float32Array} stroke_style ストロークスタイル(RGBA) / Stroke style (RGBA)
+ * @param {Uint8Array} pixels ピクセルデータ / Pixel data
+ * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix
+ * @param {number} width テクスチャ幅 / Texture width
+ * @param {number} height テクスチャ高さ / Texture height
+ * @param {boolean} repeat リピート有無 / Whether to repeat
+ * @param {boolean} smooth スムーズフィルタ有無 / Whether to use smooth filtering
+ * @param {number} viewport_width ビューポート幅 / Viewport width
+ * @param {number} viewport_height ビューポート高さ / Viewport height
+ * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target
+ * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline
+ * @return {GPUTexture | null} ビットマップテクスチャまたはnull / Bitmap texture or null
+ */
export const execute = (
device: GPUDevice,
render_pass_encoder: GPURenderPassEncoder,
diff --git a/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts b/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts
index c86eb906..bb5f6641 100644
--- a/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts
@@ -6,8 +6,27 @@ import { execute as meshFillGenerateUseCase } from "../../Mesh/usecase/MeshFillG
import { execute as maskUnionMaskService } from "../../Mask/service/MaskUnionMaskService";
import { $clipLevels } from "../../Mask";
+/**
+ * @description クリップ用ユニフォームデータの事前確保配列(16要素)
+ * Pre-allocated uniform data array for clipping (16 elements)
+ */
const $clipUniform16 = new Float32Array(16);
+/**
+ * @description クリップ(マスク)描画を実行する
+ * Executes clip (mask) rendering
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {IAttachmentObject} current_attachment 現在のアタッチメント / Current attachment
+ * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array
+ * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix
+ * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA)
+ * @param {number} global_alpha グローバルアルファ値 / Global alpha value
+ * @param {boolean} is_main_attachment メインアタッチメントフラグ / Whether this is the main attachment
+ * @return {void}
+ */
export const execute = (
device: GPUDevice,
render_pass_encoder: GPURenderPassEncoder,
diff --git a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts
new file mode 100644
index 00000000..1c558ee8
--- /dev/null
+++ b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts
@@ -0,0 +1,231 @@
+import { execute } from "./ContextContainerEndLayerUseCase";
+import { describe, expect, it, vi, beforeEach } from "vitest";
+
+const mockAttachment = {
+ "width": 800,
+ "height": 600,
+ "texture": { "view": {} },
+ "msaa": false,
+ "msaaTexture": null
+};
+
+const mockMainAttachment = {
+ "width": 800,
+ "height": 600,
+ "texture": { "view": {} },
+ "msaa": false,
+ "msaaTexture": null
+};
+
+const mockPassEncoder = {
+ "setPipeline": vi.fn(),
+ "setBindGroup": vi.fn(),
+ "setViewport": vi.fn(),
+ "setScissorRect": vi.fn(),
+ "draw": vi.fn(),
+ "end": vi.fn()
+};
+
+vi.mock("@next2d/cache", () => ({
+ "$cacheStore": {
+ "set": vi.fn(),
+ "get": vi.fn()
+ }
+}));
+
+vi.mock("../../Filter/FilterOffset", () => ({
+ "$offset": { "x": 0, "y": 0 }
+}));
+
+vi.mock("../../WebGPUUtil", () => ({
+ "WebGPUUtil": {
+ "getDevicePixelRatio": vi.fn(() => 1)
+ }
+}));
+
+vi.mock("../../Filter/BlurFilter/FilterApplyBlurFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Filter/GlowFilter/FilterApplyGlowFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Filter/BevelFilter/FilterApplyBevelFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase", () => ({
+ "execute": vi.fn((_att: any) => mockAttachment)
+}));
+vi.mock("../../Blend/usecase/BlendApplyComplexBlendUseCase", () => ({
+ "execute": vi.fn(() => mockAttachment)
+}));
+
+describe("ContextContainerEndLayerUseCase.js test", () => {
+
+ const createMockConfig = () => ({
+ "device": {
+ "createBindGroup": vi.fn(() => ({}))
+ },
+ "commandEncoder": {
+ "beginRenderPass": vi.fn(() => mockPassEncoder)
+ },
+ "bufferManager": {
+ "acquireAndWriteUniformBuffer": vi.fn(() => ({}))
+ },
+ "frameBufferManager": {
+ "createTemporaryAttachment": vi.fn(() => ({
+ ...mockAttachment,
+ "texture": { "view": {} }
+ })),
+ "releaseTemporaryAttachment": vi.fn(),
+ "createRenderPassDescriptor": vi.fn(() => ({}))
+ },
+ "pipelineManager": {
+ "getPipeline": vi.fn(() => ({})),
+ "getBindGroupLayout": vi.fn(() => ({}))
+ },
+ "textureManager": {
+ "createSampler": vi.fn(() => ({}))
+ },
+ "frameTextures": []
+ }) as any;
+
+ const createMockBufferManager = () => ({
+ "acquireAndWriteUniformBuffer": vi.fn(() => ({}))
+ }) as any;
+
+ beforeEach(() =>
+ {
+ vi.clearAllMocks();
+ mockPassEncoder.setPipeline.mockClear();
+ mockPassEncoder.setBindGroup.mockClear();
+ mockPassEncoder.draw.mockClear();
+ mockPassEncoder.end.mockClear();
+ });
+
+ it("execute test case1 - blend only (no filter)", () =>
+ {
+ const config = createMockConfig();
+ const bufferManager = createMockBufferManager();
+
+ const matrix = new Float32Array([1, 0, 0, 1, 10, 20]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+
+ execute(
+ mockAttachment as any,
+ mockMainAttachment as any,
+ "temp",
+ "normal",
+ matrix,
+ colorTransform,
+ false, null, null,
+ "", "",
+ 800, 600,
+ config,
+ bufferManager
+ );
+
+ // Should copy region and draw to main
+ expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled();
+ expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled();
+ });
+
+ it("execute test case2 - with filter (blur)", () =>
+ {
+ const config = createMockConfig();
+ const bufferManager = createMockBufferManager();
+
+ const matrix = new Float32Array([1, 0, 0, 1, 10, 20]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+ const filterBounds = new Float32Array([-5, -5, 110, 110]);
+ // BlurFilter: type=1, blurX=4, blurY=4, quality=1
+ const filterParams = new Float32Array([1, 4, 4, 1]);
+
+ execute(
+ mockAttachment as any,
+ mockMainAttachment as any,
+ "temp",
+ "normal",
+ matrix,
+ colorTransform,
+ true, filterBounds, filterParams,
+ "uk1", "fk1",
+ 800, 600,
+ config,
+ bufferManager
+ );
+
+ expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled();
+ });
+
+ it("execute test case3 - non-identity color transform in blend-only mode", () =>
+ {
+ const config = createMockConfig();
+ const bufferManager = createMockBufferManager();
+
+ const matrix = new Float32Array([1, 0, 0, 1, 10, 20]);
+ // Non-identity color transform
+ const colorTransform = new Float32Array([0.5, 0.5, 0.5, 1, 10, 10, 10, 0]);
+
+ execute(
+ mockAttachment as any,
+ mockMainAttachment as any,
+ "temp",
+ "normal",
+ matrix,
+ colorTransform,
+ false, null, null,
+ "", "",
+ 800, 600,
+ config,
+ bufferManager
+ );
+
+ // Should apply color transform (extra createTemporaryAttachment call)
+ expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled();
+ expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled();
+ });
+
+ it("execute test case4 - caches filter result with uniqueKey", async () =>
+ {
+ const { $cacheStore } = await import("@next2d/cache");
+ const config = createMockConfig();
+ const bufferManager = createMockBufferManager();
+
+ const matrix = new Float32Array([1, 0, 0, 1, 0, 0]);
+ const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
+ const filterBounds = new Float32Array([0, 0, 100, 100]);
+ const filterParams = new Float32Array([1, 2, 2, 1]); // BlurFilter
+
+ execute(
+ mockAttachment as any,
+ mockMainAttachment as any,
+ "temp",
+ "normal",
+ matrix,
+ colorTransform,
+ true, filterBounds, filterParams,
+ "cacheKey", "filterKey",
+ 800, 600,
+ config,
+ bufferManager
+ );
+
+ expect($cacheStore.set).toHaveBeenCalledWith("cacheKey", "fKey", "filterKey");
+ expect($cacheStore.set).toHaveBeenCalledWith("cacheKey", "fTexture", expect.anything());
+ });
+});
diff --git a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts
index e33c8475..8ad48391 100644
--- a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts
@@ -16,24 +16,54 @@ import { execute as filterApplyGradientGlowFilterUseCase } from "../../Filter/Gr
import { execute as filterApplyDisplacementMapFilterUseCase } from "../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase";
import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/BlendApplyComplexBlendUseCase";
+/**
+ * @description ユニフォームデータの事前確保配列(4要素)
+ * Pre-allocated uniform data array (4 elements)
+ */
const $uniform4 = new Float32Array(4);
+/**
+ * @description ユニフォームデータの事前確保配列(8要素)
+ * Pre-allocated uniform data array (8 elements)
+ */
const $uniform8 = new Float32Array(8);
+/**
+ * @description ユニフォームデータの事前確保配列(20要素)
+ * Pre-allocated uniform data array (20 elements)
+ */
const $uniform20 = new Float32Array(20);
// プリアロケート BindGroup Entry 配列
+/**
+ * @description バインドグループエントリの事前確保配列
+ * Pre-allocated bind group entry array
+ */
const $entries3: GPUBindGroupEntry[] = [
{ "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
{ "binding": 1, "resource": null as unknown as GPUSampler },
{ "binding": 2, "resource": null as unknown as GPUTextureView }
];
-const SIMPLE_BLEND_MODES: ReadonlySet = new Set([
+/**
+ * @description シンプルなブレンドモードのセット
+ * Set of simple blend modes
+ */
+const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([
"normal", "layer", "add", "screen", "alpha", "erase", "copy"
] as IBlendMode[]);
+/**
+ * @description 恒等カラートランスフォーム
+ * Identity color transform
+ */
const $identityColorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]);
-const isIdentityColorTransform = (ct: Float32Array | null): boolean => {
+/**
+ * @description カラートランスフォームが恒等変換かどうかを判定する
+ * Checks whether the color transform is an identity transform
+ * @param {Float32Array | null} ct カラートランスフォーム配列 / Color transform array
+ * @return {boolean} 恒等変換の場合true / True if identity transform
+ */
+const $isIdentityColorTransform = (ct: Float32Array | null): boolean => {
if (!ct) {
return true;
}
@@ -41,10 +71,18 @@ const isIdentityColorTransform = (ct: Float32Array | null): boolean => {
&& ct[4] === 0 && ct[5] === 0 && ct[6] === 0 && ct[7] === 0;
};
-const applyColorTransform = (
+/**
+ * @description アタッチメントにカラートランスフォームを適用する
+ * Applies color transform to an attachment
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {IAttachmentObject} attachment ソースアタッチメント / Source attachment
+ * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array
+ * @return {IAttachmentObject} カラートランスフォーム適用後のアタッチメント / Attachment with color transform applied
+ */
+const $applyColorTransform = (
config: ILocalFilterConfig,
attachment: IAttachmentObject,
- colorTransform: Float32Array
+ color_transform: Float32Array
): IAttachmentObject => {
const ctAttachment = config.frameBufferManager.createTemporaryAttachment(
attachment.width, attachment.height
@@ -57,13 +95,13 @@ const applyColorTransform = (
return attachment;
}
- $uniform8[0] = colorTransform[0];
- $uniform8[1] = colorTransform[1];
- $uniform8[2] = colorTransform[2];
- $uniform8[3] = colorTransform[3];
- $uniform8[4] = colorTransform[4];
- $uniform8[5] = colorTransform[5];
- $uniform8[6] = colorTransform[6];
+ $uniform8[0] = color_transform[0];
+ $uniform8[1] = color_transform[1];
+ $uniform8[2] = color_transform[2];
+ $uniform8[3] = color_transform[3];
+ $uniform8[4] = color_transform[4];
+ $uniform8[5] = color_transform[5];
+ $uniform8[6] = color_transform[6];
$uniform8[7] = 0;
const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8);
@@ -90,9 +128,20 @@ const applyColorTransform = (
return ctAttachment;
};
-const copyRegionToFilterAttachment = (
+/**
+ * @description ソースアタッチメントの領域をフィルター用アタッチメントにコピーする
+ * Copies a region from source attachment to a filter attachment
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment
+ * @param {number} x X座標 / X coordinate
+ * @param {number} y Y座標 / Y coordinate
+ * @param {number} width 幅 / Width
+ * @param {number} height 高さ / Height
+ * @return {IAttachmentObject} コピーされたアタッチメント / Copied attachment
+ */
+const $copyRegionToFilterAttachment = (
config: ILocalFilterConfig,
- srcAttachment: IAttachmentObject,
+ src_attachment: IAttachmentObject,
x: number,
y: number,
width: number,
@@ -101,21 +150,20 @@ const copyRegionToFilterAttachment = (
const dstAttachment = config.frameBufferManager.createTemporaryAttachment(width, height);
- const pipeline = config.pipelineManager.getPipeline("complex_blend_copy");
+ // texture_copy_rgba8 (BlurFilterVertex, yFlipTexCoord=true) を使用
+ const pipeline = config.pipelineManager.getPipeline("texture_copy_rgba8");
const bindGroupLayout = config.pipelineManager.getBindGroupLayout("texture_copy");
- if (!pipeline || !bindGroupLayout || !srcAttachment.texture || !dstAttachment.texture) {
+ if (!pipeline || !bindGroupLayout || !src_attachment.texture || !dstAttachment.texture) {
return dstAttachment;
}
- const scaleX = width / srcAttachment.width;
- const offsetX = x / srcAttachment.width;
-
- // ComplexBlendCopyVertexはOpenGL座標系のtexCoord(Y軸反転)を使用するため、
- // UV uniformでY反転を補正して正しい向きの出力を得る
- // texCoord.y=1(fb上端) → uv.y=y/H(ソース上端), texCoord.y=0(fb下端) → uv.y=(y+h)/H(ソース下端)
- const scaleY = -(height / srcAttachment.height);
- const offsetY = (y + height) / srcAttachment.height;
+ // BlurFilterVertex (yFlipTexCoord=true):
+ // texCoord.y=0(fb上端) → uv.y=y/H, texCoord.y=1(fb下端) → uv.y=(y+h)/H
+ const scaleX = width / src_attachment.width;
+ const scaleY = height / src_attachment.height;
+ const offsetX = x / src_attachment.width;
+ const offsetY = y / src_attachment.height;
$uniform4[0] = scaleX;
$uniform4[1] = scaleY;
@@ -126,7 +174,7 @@ const copyRegionToFilterAttachment = (
const sampler = config.textureManager.createSampler("container_copy_sampler", false);
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
- $entries3[2].resource = srcAttachment.texture.view;
+ $entries3[2].resource = src_attachment.texture.view;
const bindGroup = config.device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries3
@@ -145,35 +193,47 @@ const copyRegionToFilterAttachment = (
return dstAttachment;
};
-const drawFilterResultToMain = (
+/**
+ * @description フィルター結果をメインアタッチメントに描画する
+ * Draws filter result to the main attachment
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment
+ * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment
+ * @param {IBlendMode} blend_mode ブレンドモード / Blend mode
+ * @param {number} x X座標 / X coordinate
+ * @param {number} y Y座標 / Y coordinate
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @return {void}
+ */
+const $drawFilterResultToMain = (
config: ILocalFilterConfig,
- filterAttachment: IAttachmentObject,
- mainAttachment: IAttachmentObject,
- blendMode: IBlendMode,
+ filter_attachment: IAttachmentObject,
+ main_attachment: IAttachmentObject,
+ blend_mode: IBlendMode,
x: number,
y: number,
- bufferManager: BufferManager
+ buffer_manager: BufferManager
): void => {
- if (!mainAttachment.texture || !filterAttachment.texture) {
+ if (!main_attachment.texture || !filter_attachment.texture) {
return;
}
// WebGLと同じサブピクセル精度を維持するため、Math.floorを使用しない
let drawX = x;
let drawY = y;
- let drawWidth = filterAttachment.width;
- let drawHeight = filterAttachment.height;
+ let drawWidth = filter_attachment.width;
+ let drawHeight = filter_attachment.height;
let uvOffsetX = 0;
let uvOffsetY = 0;
if (drawX < 0) {
- uvOffsetX = -drawX / filterAttachment.width;
+ uvOffsetX = -drawX / filter_attachment.width;
drawWidth += drawX;
drawX = 0;
}
if (drawY < 0) {
- uvOffsetY = -drawY / filterAttachment.height;
+ uvOffsetY = -drawY / filter_attachment.height;
drawHeight += drawY;
drawY = 0;
}
@@ -182,8 +242,8 @@ const drawFilterResultToMain = (
return;
}
- const mainWidth = mainAttachment.width;
- const mainHeight = mainAttachment.height;
+ const mainWidth = main_attachment.width;
+ const mainHeight = main_attachment.height;
if (drawX + drawWidth > mainWidth) {
drawWidth = mainWidth - drawX;
}
@@ -191,12 +251,12 @@ const drawFilterResultToMain = (
drawHeight = mainHeight - drawY;
}
- if (SIMPLE_BLEND_MODES.has(blendMode)) {
+ if ($SIMPLE_BLEND_MODES.has(blend_mode)) {
- const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view;
+ const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view;
let pipelineName: string;
- switch (blendMode) {
+ switch (blend_mode) {
case "add":
pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add";
break;
@@ -226,24 +286,24 @@ const drawFilterResultToMain = (
const sampler = config.textureManager.createSampler("container_output_sampler", true);
- const uvScaleX = drawWidth / filterAttachment.width;
- const uvScaleY = drawHeight / filterAttachment.height;
+ const uvScaleX = drawWidth / filter_attachment.width;
+ const uvScaleY = drawHeight / filter_attachment.height;
$uniform4[0] = uvScaleX;
$uniform4[1] = uvScaleY;
$uniform4[2] = uvOffsetX;
$uniform4[3] = uvOffsetY;
- const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform4);
+ const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform4);
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
- $entries3[2].resource = filterAttachment.texture.view;
+ $entries3[2].resource = filter_attachment.texture.view;
const bindGroup = config.device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries3
});
- const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view;
- const resolveTarget = useMsaa ? mainAttachment.texture.view : null;
+ const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view;
+ const resolveTarget = useMsaa ? main_attachment.texture.view : null;
const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor(
colorView, 0, 0, 0, 0, "load", resolveTarget
);
@@ -273,8 +333,8 @@ const drawFilterResultToMain = (
} else {
// 複雑なブレンドモード
- const dstAttachment = copyRegionToFilterAttachment(
- config, mainAttachment, drawX, drawY, drawWidth, drawHeight
+ const dstAttachment = $copyRegionToFilterAttachment(
+ config, main_attachment, drawX, drawY, drawWidth, drawHeight
);
$uniform8[0] = $identityColorTransform[0];
@@ -287,7 +347,7 @@ const drawFilterResultToMain = (
$uniform8[7] = 0;
const blendedAttachment = blendApplyComplexBlendUseCase(
- filterAttachment, dstAttachment, blendMode, $uniform8, {
+ filter_attachment, dstAttachment, blend_mode, $uniform8, {
"device": config.device,
"commandEncoder": config.commandEncoder,
"bufferManager": config.bufferManager,
@@ -299,22 +359,22 @@ const drawFilterResultToMain = (
);
// 結果をメインに描画
- const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view;
+ const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view;
const resultPipelineName = useMsaa ? "filter_complex_blend_output_msaa" : "filter_complex_blend_output";
const resultPipeline = config.pipelineManager.getPipeline(resultPipelineName);
const resultLayout = config.pipelineManager.getBindGroupLayout("positioned_texture");
- if (resultPipeline && resultLayout && blendedAttachment.texture && mainAttachment.texture) {
+ if (resultPipeline && resultLayout && blendedAttachment.texture && main_attachment.texture) {
$uniform8[0] = drawX;
$uniform8[1] = drawY;
$uniform8[2] = blendedAttachment.width;
$uniform8[3] = blendedAttachment.height;
- $uniform8[4] = mainAttachment.width;
- $uniform8[5] = mainAttachment.height;
+ $uniform8[4] = main_attachment.width;
+ $uniform8[5] = main_attachment.height;
$uniform8[6] = 0;
$uniform8[7] = 0;
- const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform8);
+ const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform8);
const sampler = config.textureManager.createSampler("container_blend_output_sampler", false);
@@ -326,8 +386,8 @@ const drawFilterResultToMain = (
"entries": $entries3
});
- const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view;
- const resolveTarget = useMsaa ? mainAttachment.texture.view : null;
+ const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view;
+ const resolveTarget = useMsaa ? main_attachment.texture.view : null;
const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor(
colorView, 0, 0, 0, 0, "load", resolveTarget
);
@@ -344,11 +404,21 @@ const drawFilterResultToMain = (
}
};
-const applyFilterChain = (
- filterAttachment: IAttachmentObject,
+/**
+ * @description フィルターチェーンをアタッチメントに適用する
+ * Applies a chain of filters to an attachment
+ * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment
+ * @param {Float32Array} matrix 変換行列 / Transformation matrix
+ * @param {Float32Array} params フィルターパラメータ配列 / Filter parameters array
+ * @param {number} device_pixel_ratio デバイスピクセル比 / Device pixel ratio
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @return {IAttachmentObject} フィルター適用後のアタッチメント / Attachment with filters applied
+ */
+const $applyFilterChain = (
+ filter_attachment: IAttachmentObject,
matrix: Float32Array,
params: Float32Array,
- devicePixelRatio: number,
+ device_pixel_ratio: number,
config: ILocalFilterConfig
): IAttachmentObject => {
@@ -363,30 +433,30 @@ const applyFilterChain = (
case 0: // BevelFilter
{
const newAtt = filterApplyBevelFilterUseCase(
- filterAttachment, matrix,
+ filter_attachment, matrix,
params[idx++], params[idx++], params[idx++], params[idx++],
params[idx++], params[idx++], params[idx++], params[idx++],
params[idx++], params[idx++], params[idx++], Boolean(params[idx++]),
- devicePixelRatio, config
+ device_pixel_ratio, config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
case 1: // BlurFilter
{
const newAtt = filterApplyBlurFilterUseCase(
- filterAttachment, matrix,
+ filter_attachment, matrix,
params[idx++], params[idx++], params[idx++],
- devicePixelRatio, config
+ device_pixel_ratio, config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
@@ -413,12 +483,12 @@ const applyFilterChain = (
$uniform20[18] = params[idx++];
$uniform20[19] = params[idx++];
const newAtt = filterApplyColorMatrixFilterUseCase(
- filterAttachment, $uniform20, config
+ filter_attachment, $uniform20, config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
@@ -433,17 +503,17 @@ const applyFilterChain = (
}
const newAtt = filterApplyConvolutionFilterUseCase(
- filterAttachment,
+ filter_attachment,
matrixX, matrixY, convMatrix,
params[idx++], params[idx++],
Boolean(params[idx++]), Boolean(params[idx++]),
params[idx++], params[idx++],
config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
@@ -456,50 +526,49 @@ const applyFilterChain = (
}
const newAtt = filterApplyDisplacementMapFilterUseCase(
- filterAttachment, matrix,
+ filter_attachment, matrix,
dmBuffer, params[idx++], params[idx++],
params[idx++], params[idx++],
params[idx++], params[idx++],
params[idx++], params[idx++],
params[idx++], params[idx++], params[idx++],
- devicePixelRatio, config
+ device_pixel_ratio, config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
case 5: // DropShadowFilter
{
const newAtt = filterApplyDropShadowFilterUseCase(
- filterAttachment, matrix,
+ filter_attachment, matrix,
params[idx++], params[idx++], params[idx++], params[idx++],
params[idx++], params[idx++], params[idx++], params[idx++],
Boolean(params[idx++]), Boolean(params[idx++]), Boolean(params[idx++]),
- devicePixelRatio, config
+ device_pixel_ratio, config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
case 6: // GlowFilter
{
const newAtt = filterApplyGlowFilterUseCase(
- filterAttachment, matrix,
+ filter_attachment, matrix,
params[idx++], params[idx++], params[idx++], params[idx++],
- params[idx++], params[idx++],
- Boolean(params[idx++]), Boolean(params[idx++]),
- devicePixelRatio, config
+ params[idx++], params[idx++], Boolean(params[idx++]), Boolean(params[idx++]),
+ device_pixel_ratio, config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
@@ -521,16 +590,16 @@ const applyFilterChain = (
for (let i = 0; i < gbRatiosLen; i++) { gbRatios[i] = params[idx++] }
const newAtt = filterApplyGradientBevelFilterUseCase(
- filterAttachment, matrix,
+ filter_attachment, matrix,
gbDist, gbAngle, gbColors, gbAlphas, gbRatios,
params[idx++], params[idx++], params[idx++],
params[idx++], params[idx++], Boolean(params[idx++]),
- devicePixelRatio, config
+ device_pixel_ratio, config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
@@ -552,43 +621,63 @@ const applyFilterChain = (
for (let i = 0; i < ggRatiosLen; i++) { ggRatios[i] = params[idx++] }
const newAtt = filterApplyGradientGlowFilterUseCase(
- filterAttachment, matrix,
+ filter_attachment, matrix,
ggDist, ggAngle, ggColors, ggAlphas, ggRatios,
params[idx++], params[idx++], params[idx++],
params[idx++], params[idx++], Boolean(params[idx++]),
- devicePixelRatio, config
+ device_pixel_ratio, config
);
- if (filterAttachment !== newAtt) {
- config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
+ if (filter_attachment !== newAtt) {
+ config.frameBufferManager.releaseTemporaryAttachment(filter_attachment);
}
- filterAttachment = newAtt;
+ filter_attachment = newAtt;
}
break;
}
}
- return filterAttachment;
+ return filter_attachment;
};
+/**
+ * @description コンテナレイヤーの終了処理を実行する(フィルター適用+ブレンド+メインへの描画)
+ * Executes container layer end processing (filter application + blending + drawing to main)
+ * @param {IAttachmentObject} temp_attachment 一時アタッチメント / Temporary attachment
+ * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment
+ * @param {string} _temp_name 一時名(未使用) / Temporary name (unused)
+ * @param {IBlendMode} blend_mode ブレンドモード / Blend mode
+ * @param {Float32Array} matrix 変換行列 / Transformation matrix
+ * @param {Float32Array | null} color_transform カラートランスフォーム配列 / Color transform array
+ * @param {boolean} use_filter フィルター使用フラグ / Whether to use filter
+ * @param {Float32Array | null} filter_bounds フィルターバウンディングボックス / Filter bounding box
+ * @param {Float32Array | null} params フィルターパラメータ配列 / Filter parameters array
+ * @param {string} unique_key ユニークキー / Unique key
+ * @param {string} filter_key フィルターキー / Filter key
+ * @param {number} _content_width コンテンツ幅(未使用) / Content width (unused)
+ * @param {number} _content_height コンテンツ高さ(未使用) / Content height (unused)
+ * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @return {void}
+ */
export const execute = (
- tempAttachment: IAttachmentObject,
- mainAttachment: IAttachmentObject,
- _tempName: string,
- blendMode: IBlendMode,
+ temp_attachment: IAttachmentObject,
+ main_attachment: IAttachmentObject,
+ _temp_name: string,
+ blend_mode: IBlendMode,
matrix: Float32Array,
- colorTransform: Float32Array | null,
- useFilter: boolean,
- filterBounds: Float32Array | null,
+ color_transform: Float32Array | null,
+ use_filter: boolean,
+ filter_bounds: Float32Array | null,
params: Float32Array | null,
- uniqueKey: string,
- filterKey: string,
- _contentWidth: number,
- _contentHeight: number,
+ unique_key: string,
+ filter_key: string,
+ _content_width: number,
+ _content_height: number,
config: ILocalFilterConfig,
- bufferManager: BufferManager
+ buffer_manager: BufferManager
): void => {
- if (useFilter && matrix && filterBounds && params) {
+ if (use_filter && matrix && filter_bounds && params) {
// containerEndLayerが呼ばれる=ディスプレイレイヤーがコンテンツ変更を検出して再レンダリングを要求
// 常に新鮮なテクスチャを抽出してフィルターを適用する
@@ -597,26 +686,26 @@ export const execute = (
// WebGL版と同じ: レイヤー全体をフィルター用にコピー
// レイヤーはコンテンツサイズで作成され、childrenは相対座標で描画されているため
// (0, 0, layerWidth, layerHeight) = コンテンツ全体
- let filterAttachment = copyRegionToFilterAttachment(
- config, tempAttachment,
- 0, 0, tempAttachment.width, tempAttachment.height
+ let filterAttachment = $copyRegionToFilterAttachment(
+ config, temp_attachment,
+ 0, 0, temp_attachment.width, temp_attachment.height
);
// 一時アタッチメントを遅延解放(コマンドバッファsubmit後に解放)
// destroyAttachmentは即座にGPUテクスチャを破棄するため、
// コマンドエンコーダに記録済みのレンダーパスが参照するテクスチャが無効になる
- config.frameBufferManager.releaseTemporaryAttachment(tempAttachment);
+ config.frameBufferManager.releaseTemporaryAttachment(temp_attachment);
// フィルターチェーンを適用
const devicePixelRatio = WebGPUUtil.getDevicePixelRatio();
- filterAttachment = applyFilterChain(
+ filterAttachment = $applyFilterChain(
filterAttachment, matrix, params, devicePixelRatio, config
);
// キャッシュに保存
- if (uniqueKey) {
- $cacheStore.set(uniqueKey, "fKey", filterKey);
- $cacheStore.set(uniqueKey, "fTexture", filterAttachment);
+ if (unique_key) {
+ $cacheStore.set(unique_key, "fKey", filter_key);
+ $cacheStore.set(unique_key, "fTexture", filterAttachment);
}
// フィルター結果をメインに描画
@@ -625,23 +714,23 @@ export const execute = (
// キャッシュにはフィルター結果のみ保存(CTは毎フレーム適用する)
let drawAttachment = filterAttachment;
let ctAttachment: IAttachmentObject | null = null;
- if (!isIdentityColorTransform(colorTransform)) {
- ctAttachment = applyColorTransform(config, filterAttachment, colorTransform!);
+ if (!$isIdentityColorTransform(color_transform)) {
+ ctAttachment = $applyColorTransform(config, filterAttachment, color_transform!);
drawAttachment = ctAttachment;
}
const scaleX = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]);
const scaleY = Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3]);
- const boundsXMin = filterBounds[0] * (scaleX / devicePixelRatio);
- const boundsYMin = filterBounds[1] * (scaleY / devicePixelRatio);
+ const boundsXMin = filter_bounds[0] * (scaleX / devicePixelRatio);
+ const boundsYMin = filter_bounds[1] * (scaleY / devicePixelRatio);
// WebGL版と同じ: boundsXMin + matrix[4] で絶対位置
const drawX = boundsXMin + matrix[4];
const drawY = boundsYMin + matrix[5];
- drawFilterResultToMain(
- config, drawAttachment, mainAttachment,
- blendMode, drawX, drawY, bufferManager
+ $drawFilterResultToMain(
+ config, drawAttachment, main_attachment,
+ blend_mode, drawX, drawY, buffer_manager
);
// CT一時アタッチメントを解放
@@ -649,7 +738,7 @@ export const execute = (
config.frameBufferManager.releaseTemporaryAttachment(ctAttachment);
}
// キャッシュされていないフィルター結果のみ解放
- if (!uniqueKey) {
+ if (!unique_key) {
config.frameBufferManager.releaseTemporaryAttachment(filterAttachment);
}
}
@@ -657,25 +746,25 @@ export const execute = (
} else {
// ブレンドのみ:レイヤー全体をフィルター用にコピーしてメインに描画
- let fullAttachment = copyRegionToFilterAttachment(
- config, tempAttachment,
- 0, 0, tempAttachment.width, tempAttachment.height
+ let fullAttachment = $copyRegionToFilterAttachment(
+ config, temp_attachment,
+ 0, 0, temp_attachment.width, temp_attachment.height
);
// 一時アタッチメントを遅延解放(コマンドバッファsubmit後に解放)
- config.frameBufferManager.releaseTemporaryAttachment(tempAttachment);
+ config.frameBufferManager.releaseTemporaryAttachment(temp_attachment);
// ColorTransformが恒等変換でない場合、適用
- if (!isIdentityColorTransform(colorTransform)) {
- const ctAttachment = applyColorTransform(config, fullAttachment, colorTransform!);
+ if (!$isIdentityColorTransform(color_transform)) {
+ const ctAttachment = $applyColorTransform(config, fullAttachment, color_transform!);
config.frameBufferManager.releaseTemporaryAttachment(fullAttachment);
fullAttachment = ctAttachment;
}
// WebGL版と同じ: matrix[4], matrix[5] = layerBounds の絶対位置に描画
- drawFilterResultToMain(
- config, fullAttachment, mainAttachment,
- blendMode, matrix[4], matrix[5], bufferManager
+ $drawFilterResultToMain(
+ config, fullAttachment, main_attachment,
+ blend_mode, matrix[4], matrix[5], buffer_manager
);
config.frameBufferManager.releaseTemporaryAttachment(fullAttachment);
diff --git a/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts b/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts
index 74b4980c..bd0051b4 100644
--- a/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts
@@ -13,9 +13,50 @@ import {
} from "../../Mask";
import { $getAtlasAttachmentObject } from "../../AtlasManager";
+/**
+ * @description キャッシュ済みバインドグループ
+ * Cached bind group
+ */
let $cachedBindGroup: GPUBindGroup | null = null;
+/**
+ * @description キャッシュ済みアトラステクスチャビュー
+ * Cached atlas texture view
+ */
let $cachedAtlasView: GPUTextureView | null = null;
+/**
+ * @description ブレンドモードに応じたインスタンスパイプライン名を返す
+ */
+const $getPipelineName = (mode: IBlendMode): string => {
+ switch (mode) {
+ case "add":
+ return "instanced_add";
+ case "screen":
+ return "instanced_screen";
+ case "alpha":
+ return "instanced_alpha";
+ case "erase":
+ return "instanced_erase";
+ case "copy":
+ return "instanced_copy";
+ default:
+ return "instanced";
+ }
+};
+
+/**
+ * @description インスタンス描画を実行する
+ * Executes instanced array drawing
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder
+ * @param {GPURenderPassEncoder | null} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager
+ * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @return {GPURenderPassEncoder | null} レンダーパスエンコーダまたはnull / Render pass encoder or null
+ */
export const execute = (
device: GPUDevice,
command_encoder: GPUCommandEncoder,
@@ -44,27 +85,7 @@ export const execute = (
// 現在のブレンドモードを取得
const blendMode: IBlendMode = $currentBlendMode;
- // ブレンドモードに応じたパイプライン名を生成
- // simpleBlendModes: normal, layer, add, screen, alpha, erase, copy
- const getPipelineName = (mode: IBlendMode): string => {
- switch (mode) {
- case "add":
- return "instanced_add";
- case "screen":
- return "instanced_screen";
- case "alpha":
- return "instanced_alpha";
- case "erase":
- return "instanced_erase";
- case "copy":
- return "instanced_copy";
- default:
- // normal, layer
- return "instanced";
- }
- };
-
- const pipelineName = getPipelineName(blendMode);
+ const pipelineName = $getPipelineName(blendMode);
const normalPipeline = pipeline_manager.getPipeline(pipelineName);
const maskedPipeline = pipeline_manager.getPipeline("instanced_masked");
diff --git a/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts b/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts
index b51c01b6..314b552a 100644
--- a/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts
@@ -13,31 +13,54 @@ import {
} from "../../Mask";
import { $getAtlasAttachmentObject } from "../../AtlasManager";
+/**
+ * @description キャッシュ済みバインドグループ
+ * Cached bind group
+ */
let $cachedBindGroup: GPUBindGroup | null = null;
+/**
+ * @description キャッシュ済みアトラステクスチャビュー
+ * Cached atlas texture view
+ */
let $cachedAtlasView: GPUTextureView | null = null;
+/**
+ * @description Indirect描画を使用したインスタンス描画を実行する
+ * Executes instanced drawing with indirect draw support
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder
+ * @param {GPURenderPassEncoder | null} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager
+ * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {boolean} use_indirect Indirect描画使用フラグ / Whether to use indirect drawing
+ * @param {boolean} use_storage_buffer StorageBuffer使用フラグ / Whether to use storage buffer
+ * @return {GPURenderPassEncoder | null} レンダーパスエンコーダまたはnull / Render pass encoder or null
+ */
export const execute = (
device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- renderPassEncoder: GPURenderPassEncoder | null,
- mainAttachment: IAttachmentObject,
- bufferManager: BufferManager,
- frameBufferManager: FrameBufferManager,
- textureManager: TextureManager,
- pipelineManager: PipelineManager,
- useIndirect: boolean = true,
- useStorageBuffer: boolean = true
+ command_encoder: GPUCommandEncoder,
+ render_pass_encoder: GPURenderPassEncoder | null,
+ main_attachment: IAttachmentObject,
+ buffer_manager: BufferManager,
+ frame_buffer_manager: FrameBufferManager,
+ texture_manager: TextureManager,
+ pipeline_manager: PipelineManager,
+ use_indirect: boolean = true,
+ use_storage_buffer: boolean = true
): GPURenderPassEncoder | null => {
const shaderManager = getInstancedShaderManager();
if (shaderManager.count === 0) {
- return renderPassEncoder;
+ return render_pass_encoder;
}
// 既存のレンダーパスを終了
- if (renderPassEncoder) {
- renderPassEncoder.end();
- renderPassEncoder = null;
+ if (render_pass_encoder) {
+ render_pass_encoder.end();
+ render_pass_encoder = null;
}
const isMasked = $isMaskTestEnabled();
@@ -66,11 +89,11 @@ export const execute = (
};
const pipelineName = getPipelineName(blendMode);
- const normalPipeline = pipelineManager.getPipeline(pipelineName);
- const maskedPipeline = pipelineManager.getPipeline("instanced_masked");
+ const normalPipeline = pipeline_manager.getPipeline(pipelineName);
+ const maskedPipeline = pipeline_manager.getPipeline("instanced_masked");
const useStencil = isMasked && maskedPipeline
- && (mainAttachment.msaaStencil?.view || mainAttachment.stencil?.view);
+ && (main_attachment.msaaStencil?.view || main_attachment.stencil?.view);
const pipeline = useStencil ? maskedPipeline : normalPipeline;
@@ -84,32 +107,32 @@ export const execute = (
if (useStencil) {
// MSAA対応
- const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view;
- const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view;
- const stencilView = useMsaa && mainAttachment.msaaStencil?.view
- ? mainAttachment.msaaStencil.view : mainAttachment.stencil!.view;
- const resolveTarget = useMsaa ? mainAttachment.texture!.view : null;
+ const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view;
+ const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view;
+ const stencilView = useMsaa && main_attachment.msaaStencil?.view
+ ? main_attachment.msaaStencil.view : main_attachment.stencil!.view;
+ const resolveTarget = useMsaa ? main_attachment.texture!.view : null;
- const renderPassDescriptor = frameBufferManager.createStencilRenderPassDescriptor(
+ const renderPassDescriptor = frame_buffer_manager.createStencilRenderPassDescriptor(
colorView,
stencilView,
"load",
"load",
resolveTarget
);
- passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder = command_encoder.beginRenderPass(renderPassDescriptor);
} else {
// 通常のレンダーパス(MSAA対応)
- const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view;
- const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view;
- const resolveTarget = useMsaa ? mainAttachment.texture!.view : null;
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
+ const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view;
+ const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view;
+ const resolveTarget = useMsaa ? main_attachment.texture!.view : null;
+ const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor(
colorView,
0, 0, 0, 0,
"load",
resolveTarget
);
- passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ passEncoder = command_encoder.beginRenderPass(renderPassDescriptor);
}
passEncoder.setPipeline(pipeline);
@@ -127,22 +150,22 @@ export const execute = (
// インスタンスバッファを作成または取得
let instanceBuffer: GPUBuffer;
- if (useStorageBuffer) {
+ if (use_storage_buffer) {
// Storage Buffer最適化: プールから再利用してメモリアロケーション削減
// Storage BufferはVERTEXフラグ付きで作成されているため、setVertexBufferで使用可能
- instanceBuffer = bufferManager.acquireStorageBuffer(instanceData.byteLength);
- bufferManager.writeStorageBuffer(instanceBuffer, instanceData);
+ instanceBuffer = buffer_manager.acquireStorageBuffer(instanceData.byteLength);
+ buffer_manager.writeStorageBuffer(instanceBuffer, instanceData);
} else {
// 従来方式: プールから再利用
- instanceBuffer = bufferManager.acquireVertexBuffer(instanceData.byteLength, instanceData);
+ instanceBuffer = buffer_manager.acquireVertexBuffer(instanceData.byteLength, instanceData);
}
// 頂点バッファ(矩形)を取得(キャッシュ済み)
- const vertexBuffer = bufferManager.getUnitRectBuffer();
+ const vertexBuffer = buffer_manager.getUnitRectBuffer();
// アトラステクスチャをバインド(複数アトラス対応)
// AtlasManagerから取得、フォールバックとしてFrameBufferManagerから取得
- const atlasAttachment = $getAtlasAttachmentObject() || frameBufferManager.getAttachment("atlas");
+ const atlasAttachment = $getAtlasAttachmentObject() || frame_buffer_manager.getAttachment("atlas");
if (!atlasAttachment) {
console.error("[WebGPU] Atlas attachment not found");
passEncoder.end();
@@ -150,9 +173,9 @@ export const execute = (
}
// アトラス用サンプラーを取得(キャッシュ済み)
- const sampler = textureManager.createSampler("atlas_instanced_sampler", false);
+ const sampler = texture_manager.createSampler("atlas_instanced_sampler", false);
- const bindGroupLayout = pipelineManager.getBindGroupLayout("instanced");
+ const bindGroupLayout = pipeline_manager.getBindGroupLayout("instanced");
if (!bindGroupLayout) {
console.error("[WebGPU] Instanced bind group layout not found");
passEncoder.end();
@@ -183,13 +206,13 @@ export const execute = (
passEncoder.setVertexBuffer(1, instanceBuffer);
passEncoder.setBindGroup(0, $cachedBindGroup);
- if (useIndirect) {
+ if (use_indirect) {
// Indirect Drawing: CPU-GPU間のオーバーヘッドを削減
// 注意: 1フレーム内で複数回呼び出される場合があるため、
// 毎回新しいIndirect Bufferを作成する必要がある
// (共有バッファを使うとqueue.writeBufferの更新が全てGPU実行前に行われ、
// 全てのdrawIndirectが最後の更新値を使用してしまう)
- const indirectBuffer = bufferManager.createIndirectBuffer(
+ const indirectBuffer = buffer_manager.createIndirectBuffer(
6, // vertexCount (2 triangles = 6 vertices)
shaderManager.count, // instanceCount
0, // firstVertex
diff --git a/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts b/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts
index ccb5073c..f22e9550 100644
--- a/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts
@@ -11,20 +11,67 @@ import {
$getMaskStencilReference
} from "../../Mask";
+/**
+ * @description グラデーションサンプラーのキャッシュ
+ * Cached gradient sampler
+ */
let $gradientSampler: GPUSampler | null = null;
+/**
+ * @description ユニフォームデータの事前確保配列(36要素)
+ * Pre-allocated uniform data array (36 elements)
+ */
const $uniformData36 = new Float32Array(36);
+/**
+ * @description ステンシルデータの事前確保配列(16要素)
+ * Pre-allocated stencil data array (16 elements)
+ */
const $stencilData16 = new Float32Array(16);
+/**
+ * @description ステンシル用動的バインドグループのキャッシュ
+ * Cached dynamic bind group for stencil operations
+ */
let $stencilDynamicBindGroup: GPUBindGroup | null = null;
+/**
+ * @description ステンシル用動的バッファのキャッシュ
+ * Cached dynamic buffer for stencil operations
+ */
let $stencilDynamicBuffer: GPUBuffer | null = null;
+/**
+ * @description バインドグループエントリの事前確保配列
+ * Pre-allocated bind group entry array
+ */
const $entries3: GPUBindGroupEntry[] = [
{ "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
{ "binding": 1, "resource": null as unknown as GPUSampler },
{ "binding": 2, "resource": null as unknown as GPUTextureView }
];
+/**
+ * @description グラデーションフィル描画を実行する
+ * Executes gradient fill rendering
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array
+ * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix
+ * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA)
+ * @param {number} type グラデーションタイプ / Gradient type
+ * @param {number[]} stops グラデーションストップ配列 / Gradient stops array
+ * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix
+ * @param {number} spread スプレッドモード / Spread mode
+ * @param {number} interpolation 補間モード / Interpolation mode
+ * @param {number} focal 焦点距離 / Focal point
+ * @param {number} viewport_width ビューポート幅 / Viewport width
+ * @param {number} viewport_height ビューポート高さ / Viewport height
+ * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target
+ * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline
+ * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused)
+ * @return {GPUTexture | null} LUTテクスチャまたはnull / LUT texture or null
+ */
export const execute = (
device: GPUDevice,
render_pass_encoder: GPURenderPassEncoder,
diff --git a/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts b/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts
index f4a35384..3cd2789b 100644
--- a/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts
@@ -7,16 +7,51 @@ import { execute as contextComputeGradientMatrixService } from "../service/Conte
import { $getLUTFromCache, $putLUTToCache } from "../../Gradient/GradientLUTCache";
import { $acquireFillTexture } from "../../FillTexturePool";
+/**
+ * @description グラデーションサンプラーのキャッシュ
+ * Cached gradient sampler
+ */
let $gradientSampler: GPUSampler | null = null;
+/**
+ * @description ユニフォームデータの事前確保配列(36要素)
+ * Pre-allocated uniform data array (36 elements)
+ */
const $uniformData36 = new Float32Array(36);
+/**
+ * @description バインドグループエントリの事前確保配列
+ * Pre-allocated bind group entry array
+ */
const $entries3: GPUBindGroupEntry[] = [
{ "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
{ "binding": 1, "resource": null as unknown as GPUSampler },
{ "binding": 2, "resource": null as unknown as GPUTextureView }
];
+/**
+ * @description グラデーションストローク描画を実行する
+ * Executes gradient stroke rendering
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {IPath[]} vertices パス頂点配列 / Path vertices array
+ * @param {number} thickness ストローク太さ / Stroke thickness
+ * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix
+ * @param {Float32Array} stroke_style ストロークスタイル(RGBA) / Stroke style (RGBA)
+ * @param {number} type グラデーションタイプ / Gradient type
+ * @param {number[]} stops グラデーションストップ配列 / Gradient stops array
+ * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix
+ * @param {number} spread スプレッドモード / Spread mode
+ * @param {number} interpolation 補間モード / Interpolation mode
+ * @param {number} focal 焦点距離 / Focal point
+ * @param {number} viewport_width ビューポート幅 / Viewport width
+ * @param {number} viewport_height ビューポート高さ / Viewport height
+ * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target
+ * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline
+ * @return {GPUTexture | null} LUTテクスチャまたはnull / LUT texture or null
+ */
export const execute = (
device: GPUDevice,
render_pass_encoder: GPURenderPassEncoder,
diff --git a/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts b/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts
index c3717047..95d86aca 100644
--- a/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts
+++ b/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts
@@ -8,135 +8,201 @@ import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/Bl
import { $getAtlasAttachmentObject } from "../../AtlasManager";
// プリアロケート配列
+/**
+ * @description ユニフォームデータの事前確保配列(4要素)
+ * Pre-allocated uniform data array (4 elements)
+ */
const $uniform4 = new Float32Array(4);
+/**
+ * @description ユニフォームデータの事前確保配列(6要素)
+ * Pre-allocated uniform data array (6 elements)
+ */
const $uniform6 = new Float32Array(6);
+/**
+ * @description ユニフォームデータの事前確保配列(8要素)
+ * Pre-allocated uniform data array (8 elements)
+ */
const $uniform8 = new Float32Array(8);
+/**
+ * @description ユニフォームデータの事前確保配列(12要素)
+ * Pre-allocated uniform data array (12 elements)
+ */
const $uniform12 = new Float32Array(12);
// プリアロケート BindGroup Entry 配列
+/**
+ * @description バインドグループエントリの事前確保配列
+ * Pre-allocated bind group entry array
+ */
const $entries3: GPUBindGroupEntry[] = [
{ "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
{ "binding": 1, "resource": null as unknown as GPUSampler },
{ "binding": 2, "resource": null as unknown as GPUTextureView }
];
-const copyTextureRegionViaRenderPass = (
+/**
+ * @description レンダーパスを使用してテクスチャ領域をコピーする
+ * Copies a texture region via render pass
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder
+ * @param {GPUTextureView} src_view ソーステクスチャビュー / Source texture view
+ * @param {IAttachmentObject} dst_attachment デスティネーションアタッチメント / Destination attachment
+ * @param {number} src_x ソースX座標 / Source X coordinate
+ * @param {number} src_y ソースY座標 / Source Y coordinate
+ * @param {number} src_width ソース幅 / Source width
+ * @param {number} src_height ソース高さ / Source height
+ * @param {number} copy_width コピー幅 / Copy width
+ * @param {number} copy_height コピー高さ / Copy height
+ * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager
+ * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @return {void}
+ */
+const $copyTextureRegionViaRenderPass = (
device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- srcView: GPUTextureView,
- dstAttachment: IAttachmentObject,
- srcX: number,
- srcY: number,
- srcWidth: number,
- srcHeight: number,
- copyWidth: number,
- copyHeight: number,
- frameBufferManager: FrameBufferManager,
- textureManager: TextureManager,
- pipelineManager: PipelineManager,
- bufferManager: BufferManager
+ command_encoder: GPUCommandEncoder,
+ src_view: GPUTextureView,
+ dst_attachment: IAttachmentObject,
+ src_x: number,
+ src_y: number,
+ src_width: number,
+ src_height: number,
+ copy_width: number,
+ copy_height: number,
+ frame_buffer_manager: FrameBufferManager,
+ texture_manager: TextureManager,
+ pipeline_manager: PipelineManager,
+ buffer_manager: BufferManager
): void => {
- const pipeline = pipelineManager.getPipeline("complex_blend_copy");
- const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy");
+ const pipeline = pipeline_manager.getPipeline("complex_blend_copy");
+ const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy");
if (!pipeline || !bindGroupLayout) {
return;
}
- $uniform4[0] = copyWidth / srcWidth;
- $uniform4[1] = copyHeight / srcHeight;
- $uniform4[2] = srcX / srcWidth;
- $uniform4[3] = srcY / srcHeight;
- const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform4);
+ $uniform4[0] = copy_width / src_width;
+ $uniform4[1] = copy_height / src_height;
+ $uniform4[2] = src_x / src_width;
+ $uniform4[3] = src_y / src_height;
+ const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform4);
- const sampler = textureManager.createSampler("complex_blend_copy_sampler", false);
+ const sampler = texture_manager.createSampler("complex_blend_copy_sampler", false);
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
- $entries3[2].resource = srcView;
+ $entries3[2].resource = src_view;
const bindGroup = device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries3
});
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
- dstAttachment.texture!.view,
+ const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor(
+ dst_attachment.texture!.view,
0, 0, 0, 0,
"clear"
);
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(6, 1, 0, 0);
passEncoder.end();
};
-const drawToMainAttachment = (
+/**
+ * @description ブレンド結果をメインアタッチメントに描画する
+ * Draws blend result to the main attachment
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder
+ * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment
+ * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment
+ * @param {number} dst_x デスティネーションX座標 / Destination X coordinate
+ * @param {number} dst_y デスティネーションY座標 / Destination Y coordinate
+ * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager
+ * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @return {void}
+ */
+const $drawToMainAttachment = (
device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- srcAttachment: IAttachmentObject,
- mainAttachment: IAttachmentObject,
- dstX: number,
- dstY: number,
- frameBufferManager: FrameBufferManager,
- textureManager: TextureManager,
- pipelineManager: PipelineManager,
- bufferManager: BufferManager
+ command_encoder: GPUCommandEncoder,
+ src_attachment: IAttachmentObject,
+ main_attachment: IAttachmentObject,
+ dst_x: number,
+ dst_y: number,
+ frame_buffer_manager: FrameBufferManager,
+ texture_manager: TextureManager,
+ pipeline_manager: PipelineManager,
+ buffer_manager: BufferManager
): void => {
- const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view;
+ const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view;
const pipelineName = useMsaa ? "complex_blend_output_msaa" : "complex_blend_output";
- const pipeline = pipelineManager.getPipeline(pipelineName);
- const bindGroupLayout = pipelineManager.getBindGroupLayout("positioned_texture");
+ const pipeline = pipeline_manager.getPipeline(pipelineName);
+ const bindGroupLayout = pipeline_manager.getBindGroupLayout("positioned_texture");
if (!pipeline || !bindGroupLayout) {
return;
}
- $uniform8[0] = dstX;
- $uniform8[1] = dstY;
- $uniform8[2] = srcAttachment.width;
- $uniform8[3] = srcAttachment.height;
- $uniform8[4] = mainAttachment.width;
- $uniform8[5] = mainAttachment.height;
+ $uniform8[0] = dst_x;
+ $uniform8[1] = dst_y;
+ $uniform8[2] = src_attachment.width;
+ $uniform8[3] = src_attachment.height;
+ $uniform8[4] = main_attachment.width;
+ $uniform8[5] = main_attachment.height;
$uniform8[6] = 0;
$uniform8[7] = 0;
- const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform8);
+ const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform8);
- const sampler = textureManager.createSampler("complex_blend_output_sampler", false);
+ const sampler = texture_manager.createSampler("complex_blend_output_sampler", false);
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
- $entries3[2].resource = srcAttachment.texture!.view;
+ $entries3[2].resource = src_attachment.texture!.view;
const bindGroup = device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries3
});
- const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view;
- const resolveTarget = useMsaa ? mainAttachment.texture!.view : null;
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
+ const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view;
+ const resolveTarget = useMsaa ? main_attachment.texture!.view : null;
+ const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor(
colorView,
0, 0, 0, 0,
"load",
resolveTarget
);
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(6, 1, 0, 0);
passEncoder.end();
};
+/**
+ * @description 複雑なブレンドモードキューを処理する
+ * Processes the complex blend mode queue
+ * @param {GPUDevice} device GPUデバイス / GPU device
+ * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder
+ * @param {IAttachmentObject | null} main_attachment メインアタッチメント / Main attachment
+ * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager
+ * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager
+ * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager
+ * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager
+ * @return {void}
+ */
export const execute = (
device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- mainAttachment: IAttachmentObject | null,
- frameBufferManager: FrameBufferManager,
- textureManager: TextureManager,
- pipelineManager: PipelineManager,
- bufferManager: BufferManager
+ command_encoder: GPUCommandEncoder,
+ main_attachment: IAttachmentObject | null,
+ frame_buffer_manager: FrameBufferManager,
+ texture_manager: TextureManager,
+ pipeline_manager: PipelineManager,
+ buffer_manager: BufferManager
): void => {
const queue = getComplexBlendQueue();
@@ -144,12 +210,12 @@ export const execute = (
return;
}
- if (!mainAttachment || !mainAttachment.texture) {
+ if (!main_attachment || !main_attachment.texture) {
clearComplexBlendQueue();
return;
}
- const atlasAttachment = $getAtlasAttachmentObject() || frameBufferManager.getAttachment("atlas");
+ const atlasAttachment = $getAtlasAttachmentObject() || frame_buffer_manager.getAttachment("atlas");
if (!atlasAttachment || !atlasAttachment.texture) {
clearComplexBlendQueue();
return;
@@ -168,7 +234,7 @@ export const execute = (
const dstX = Math.max(0, Math.floor(matrix[6]));
const dstY = Math.max(0, Math.floor(matrix[7]));
- if (dstX >= mainAttachment.width || dstY >= mainAttachment.height) {
+ if (dstX >= main_attachment.width || dstY >= main_attachment.height) {
continue;
}
@@ -177,8 +243,8 @@ export const execute = (
const blendWidth = hasScale ? width : node.w;
const blendHeight = hasScale ? height : node.h;
- const clippedWidth = Math.min(blendWidth, mainAttachment.width - dstX);
- const clippedHeight = Math.min(blendHeight, mainAttachment.height - dstY);
+ const clippedWidth = Math.min(blendWidth, main_attachment.width - dstX);
+ const clippedHeight = Math.min(blendHeight, main_attachment.height - dstY);
if (clippedWidth <= 0 || clippedHeight <= 0) {
continue;
}
@@ -187,10 +253,10 @@ export const execute = (
let srcAttachment: IAttachmentObject;
if (hasScale) {
- srcAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight);
+ srcAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight);
- const scalePipeline = pipelineManager.getPipeline("complex_blend_scale");
- const scaleBindGroupLayout = pipelineManager.getBindGroupLayout("texture_scale");
+ const scalePipeline = pipeline_manager.getPipeline("complex_blend_scale");
+ const scaleBindGroupLayout = pipeline_manager.getBindGroupLayout("texture_scale");
if (scalePipeline && scaleBindGroupLayout) {
const halfW = blendWidth / 2;
@@ -205,8 +271,8 @@ export const execute = (
$uniform6[4] = -halfNodeW * matrix[0] - halfNodeH * matrix[3] + halfW;
$uniform6[5] = -halfNodeW * matrix[1] - halfNodeH * matrix[4] + halfH;
- const originalAttachment = frameBufferManager.createTemporaryAttachment(node.w, node.h);
- commandEncoder.copyTextureToTexture(
+ const originalAttachment = frame_buffer_manager.createTemporaryAttachment(node.w, node.h);
+ command_encoder.copyTextureToTexture(
{
"texture": atlasAttachment.texture.resource,
"origin": { "x": node.x, "y": node.y, "z": 0 }
@@ -230,9 +296,9 @@ export const execute = (
$uniform12[9] = blendHeight;
$uniform12[10] = 0;
$uniform12[11] = 0;
- const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform12, 48);
+ const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform12, 48);
- const sampler = textureManager.createSampler("scale_sampler", true);
+ const sampler = texture_manager.createSampler("scale_sampler", true);
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
$entries3[2].resource = originalAttachment.texture!.view;
@@ -241,21 +307,21 @@ export const execute = (
"entries": $entries3
});
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
+ const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor(
srcAttachment.texture!.view,
0, 0, 0, 0,
"clear"
);
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(scalePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(6, 1, 0, 0);
passEncoder.end();
- frameBufferManager.releaseTemporaryAttachment(originalAttachment);
+ frame_buffer_manager.releaseTemporaryAttachment(originalAttachment);
} else {
- commandEncoder.copyTextureToTexture(
+ command_encoder.copyTextureToTexture(
{
"texture": atlasAttachment.texture.resource,
"origin": { "x": node.x, "y": node.y, "z": 0 }
@@ -268,8 +334,8 @@ export const execute = (
);
}
} else {
- srcAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight);
- commandEncoder.copyTextureToTexture(
+ srcAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight);
+ command_encoder.copyTextureToTexture(
{
"texture": atlasAttachment.texture.resource,
"origin": { "x": node.x, "y": node.y, "z": 0 }
@@ -283,23 +349,23 @@ export const execute = (
}
// 2. デスティネーションテクスチャを作成(メインからレンダーパスでコピー)
- const dstAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight);
+ const dstAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight);
- copyTextureRegionViaRenderPass(
+ $copyTextureRegionViaRenderPass(
device,
- commandEncoder,
- mainAttachment.texture.view,
+ command_encoder,
+ main_attachment.texture.view,
dstAttachment,
dstX,
dstY,
- mainAttachment.width,
- mainAttachment.height,
+ main_attachment.width,
+ main_attachment.height,
blendWidth,
blendHeight,
- frameBufferManager,
- textureManager,
- pipelineManager,
- bufferManager
+ frame_buffer_manager,
+ texture_manager,
+ pipeline_manager,
+ buffer_manager
);
// 3. カラートランスフォームを準備(add値は生値)
@@ -319,34 +385,34 @@ export const execute = (
blend_mode,
$uniform8,
{
- device,
- commandEncoder,
- bufferManager,
- frameBufferManager,
- pipelineManager,
- textureManager,
+ "device": device,
+ "commandEncoder": command_encoder,
+ "bufferManager": buffer_manager,
+ "frameBufferManager": frame_buffer_manager,
+ "pipelineManager": pipeline_manager,
+ "textureManager": texture_manager,
"frameTextures": []
}
);
// 5. 結果をメインアタッチメントに描画
- drawToMainAttachment(
+ $drawToMainAttachment(
device,
- commandEncoder,
+ command_encoder,
blendedAttachment,
- mainAttachment,
+ main_attachment,
dstX,
dstY,
- frameBufferManager,
- textureManager,
- pipelineManager,
- bufferManager
+ frame_buffer_manager,
+ texture_manager,
+ pipeline_manager,
+ buffer_manager
);
// 6. 一時テクスチャを解放
- frameBufferManager.releaseTemporaryAttachment(srcAttachment);
- frameBufferManager.releaseTemporaryAttachment(dstAttachment);
- frameBufferManager.releaseTemporaryAttachment(blendedAttachment);
+ frame_buffer_manager.releaseTemporaryAttachment(srcAttachment);
+ frame_buffer_manager.releaseTemporaryAttachment(dstAttachment);
+ frame_buffer_manager.releaseTemporaryAttachment(blendedAttachment);
}
clearComplexBlendQueue();
diff --git a/packages/webgpu/src/FillTexturePool.ts b/packages/webgpu/src/FillTexturePool.ts
index 75e75797..d6c977bb 100644
--- a/packages/webgpu/src/FillTexturePool.ts
+++ b/packages/webgpu/src/FillTexturePool.ts
@@ -1,6 +1,16 @@
-// GPUTexture → GPUTextureView キャッシュ(createView()呼び出し削減)
+/**
+ * @description GPUTexture → GPUTextureView キャッシュ(createView()呼び出し削減)
+ * GPUTexture to GPUTextureView cache to reduce createView() calls
+ * @type {WeakMap}
+ */
const $viewCache = new WeakMap();
+/**
+ * @description キャッシュからビューを取得、なければ生成してキャッシュに保存
+ * Get view from cache, or create and cache a new one
+ * @param {GPUTexture} texture
+ * @return {GPUTextureView}
+ */
export const $getOrCreateView = (texture: GPUTexture): GPUTextureView => {
let view = $viewCache.get(texture);
if (!view) {
@@ -10,15 +20,44 @@ export const $getOrCreateView = (texture: GPUTexture): GPUTextureView => {
return view;
};
-// GPUTextureUsage.TEXTURE_BINDING(0x04) | GPUTextureUsage.COPY_DST(0x02) = 0x06
-const FILL_TEXTURE_USAGE = 0x06;
+/**
+ * @description 塗りテクスチャ用のGPUTextureUsageフラグ
+ * GPUTextureUsage flags for fill textures
+ * TEXTURE_BINDING(0x04) | COPY_DST(0x02) = 0x06
+ * @type {number}
+ */
+const $FILL_TEXTURE_USAGE = 0x06;
-// GPUTextureUsage.TEXTURE_BINDING(0x04) | GPUTextureUsage.COPY_DST(0x02) | GPUTextureUsage.RENDER_ATTACHMENT(0x10) = 0x16
-const RENDER_TEXTURE_USAGE = 0x16;
+/**
+ * @description レンダーテクスチャ用のGPUTextureUsageフラグ
+ * GPUTextureUsage flags for render textures
+ * TEXTURE_BINDING(0x04) | COPY_DST(0x02) | RENDER_ATTACHMENT(0x10) = 0x16
+ * @type {number}
+ */
+const $RENDER_TEXTURE_USAGE = 0x16;
+/**
+ * @description 塗りテクスチャのオブジェクトプール
+ * Object pool for fill textures
+ * @type {Map}
+ */
const $pool: Map = new Map();
+
+/**
+ * @description レンダーテクスチャのオブジェクトプール
+ * Object pool for render textures
+ * @type {Map}
+ */
const $renderPool: Map = new Map();
+/**
+ * @description プールから塗りテクスチャを取得、なければ新規作成
+ * Acquire a fill texture from the pool, or create a new one
+ * @param {GPUDevice} device
+ * @param {number} width
+ * @param {number} height
+ * @return {GPUTexture}
+ */
export const $acquireFillTexture = (device: GPUDevice, width: number, height: number): GPUTexture =>
{
const key = `${width}_${height}`;
@@ -29,10 +68,16 @@ export const $acquireFillTexture = (device: GPUDevice, width: number, height: nu
return device.createTexture({
"size": { width, height },
"format": "rgba8unorm",
- "usage": FILL_TEXTURE_USAGE
+ "usage": $FILL_TEXTURE_USAGE
});
};
+/**
+ * @description 塗りテクスチャをプールに返却
+ * Release a fill texture back to the pool
+ * @param {GPUTexture} texture
+ * @return {void}
+ */
export const $releaseFillTexture = (texture: GPUTexture): void =>
{
const key = `${texture.width}_${texture.height}`;
@@ -44,6 +89,14 @@ export const $releaseFillTexture = (texture: GPUTexture): void =>
list.push(texture);
};
+/**
+ * @description プールからレンダーテクスチャを取得、なければ新規作成
+ * Acquire a render texture from the pool, or create a new one
+ * @param {GPUDevice} device
+ * @param {number} width
+ * @param {number} height
+ * @return {GPUTexture}
+ */
export const $acquireRenderTexture = (device: GPUDevice, width: number, height: number): GPUTexture =>
{
const key = `${width}_${height}`;
@@ -54,10 +107,16 @@ export const $acquireRenderTexture = (device: GPUDevice, width: number, height:
return device.createTexture({
"size": { width, height },
"format": "rgba8unorm",
- "usage": RENDER_TEXTURE_USAGE
+ "usage": $RENDER_TEXTURE_USAGE
});
};
+/**
+ * @description レンダーテクスチャをプールに返却
+ * Release a render texture back to the pool
+ * @param {GPUTexture} texture
+ * @return {void}
+ */
export const $releaseRenderTexture = (texture: GPUTexture): void =>
{
const key = `${texture.width}_${texture.height}`;
@@ -69,6 +128,11 @@ export const $releaseRenderTexture = (texture: GPUTexture): void =>
list.push(texture);
};
+/**
+ * @description 全テクスチャプールを破棄してクリア
+ * Destroy and clear all texture pools
+ * @return {void}
+ */
export const $clearFillTexturePool = (): void =>
{
for (const [, list] of $pool) {
diff --git a/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts b/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts
index 845fc0b6..9a6f0a8c 100644
--- a/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts
+++ b/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts
@@ -1,13 +1,9 @@
import type { IAttachmentObject } from "../../interface/IAttachmentObject";
import type { IFilterConfig } from "../../interface/IFilterConfig";
import { $offset } from "../FilterOffset";
+import { DEG_TO_RAD, intToPremultipliedRGBA } from "../FilterUtil";
import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase";
-/**
- * @description 度からラジアンへの変換係数
- */
-const DEG_TO_RAD: number = Math.PI / 180;
-
/**
* @description プリアロケートされたFloat32Array
*/
@@ -33,39 +29,48 @@ const $entries4: GPUBindGroupEntry[] = [
{ "binding": 3, "resource": null as unknown as GPUTextureView }
];
-/**
- * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ対応)
- */
-const intToRGBA = (color: number, alpha: number): [number, number, number, number] => {
- const r = (color >> 16 & 0xFF) / 255 * alpha;
- const g = (color >> 8 & 0xFF) / 255 * alpha;
- const b = (color & 0xFF) / 255 * alpha;
- return [r, g, b, alpha];
-};
-
/**
* @description ベベルフィルターを適用
- * WebGL版と同様に、erase前処理で差分テクスチャを作成してからブラーを適用
+ * Apply bevel filter
*
+ * WebGL版と同様に、erase前処理で差分テクスチャを作成してからブラーを適用
* UV変換方式で元テクスチャとブラーテクスチャを直接サンプリング。
* 合成時のcopyTextureToTextureと一時テクスチャを使用しない最適化版。
+ *
+ * @param {IAttachmentObject} source_attachment - 入力テクスチャ
+ * @param {Float32Array} matrix - 変換行列
+ * @param {number} distance - ベベルの距離
+ * @param {number} angle - ベベルの角度(度)
+ * @param {number} highlight_color - ハイライト色 (32bit整数)
+ * @param {number} highlight_alpha - ハイライトアルファ
+ * @param {number} shadow_color - シャドウ色 (32bit整数)
+ * @param {number} shadow_alpha - シャドウアルファ
+ * @param {number} blur_x - X方向ブラー量
+ * @param {number} blur_y - Y方向ブラー量
+ * @param {number} strength - ベベル強度
+ * @param {number} quality - クオリティ
+ * @param {number} type - タイプ (0: full, 1: inner, 2: outer)
+ * @param {boolean} knockout - ノックアウトモード
+ * @param {number} device_pixel_ratio - デバイスピクセル比
+ * @param {IFilterConfig} config - WebGPUリソース設定
+ * @return {IAttachmentObject} - フィルター適用後のアタッチメント
*/
export const execute = (
- sourceAttachment: IAttachmentObject,
+ source_attachment: IAttachmentObject,
matrix: Float32Array,
distance: number,
angle: number,
- highlightColor: number,
- highlightAlpha: number,
- shadowColor: number,
- shadowAlpha: number,
- blurX: number,
- blurY: number,
+ highlight_color: number,
+ highlight_alpha: number,
+ shadow_color: number,
+ shadow_alpha: number,
+ blur_x: number,
+ blur_y: number,
strength: number,
quality: number,
type: number,
knockout: boolean,
- devicePixelRatio: number,
+ device_pixel_ratio: number,
config: IFilterConfig
): IAttachmentObject => {
@@ -74,8 +79,8 @@ export const execute = (
// 元のオフセットを保存
const baseOffsetX = $offset.x;
const baseOffsetY = $offset.y;
- const baseWidth = sourceAttachment.width;
- const baseHeight = sourceAttachment.height;
+ const baseWidth = source_attachment.width;
+ const baseHeight = source_attachment.height;
// スケールを計算
const xScale = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]);
@@ -83,8 +88,8 @@ export const execute = (
// オフセットを計算(WebGL版と同じ)
const radian = angle * DEG_TO_RAD;
- const x = Math.cos(radian) * distance * (xScale / devicePixelRatio);
- const y = Math.sin(radian) * distance * (yScale / devicePixelRatio);
+ const x = Math.cos(radian) * distance * (xScale / device_pixel_ratio);
+ const y = Math.sin(radian) * distance * (yScale / device_pixel_ratio);
// === Erase前処理:差分テクスチャを作成 ===
const eraseAttachment = frameBufferManager.createTemporaryAttachment(baseWidth, baseHeight);
@@ -92,7 +97,7 @@ export const execute = (
// Step 1: ソーステクスチャを元の位置にコピー(erase前処理のcopyTextureToTextureは残す)
commandEncoder.copyTextureToTexture(
{
- "texture": sourceAttachment.texture!.resource,
+ "texture": source_attachment.texture!.resource,
"origin": { "x": 0, "y": 0, "z": 0 }
},
{
@@ -136,7 +141,7 @@ export const execute = (
($entries3[0].resource as GPUBufferBinding).buffer = eraseUniformBuffer;
$entries3[1].resource = eraseSampler;
- $entries3[2].resource = sourceAttachment.texture!.view;
+ $entries3[2].resource = source_attachment.texture!.view;
const eraseBindGroup = device.createBindGroup({
"layout": eraseBindGroupLayout,
"entries": $entries3
@@ -156,8 +161,8 @@ export const execute = (
// === 差分テクスチャにブラーを適用 ===
const blurAttachment = filterApplyBlurFilterUseCase(
eraseAttachment, matrix,
- blurX, blurY, quality,
- devicePixelRatio, config
+ blur_x, blur_y, quality,
+ device_pixel_ratio, config
);
// eraseアタッチメントを解放
@@ -207,7 +212,7 @@ export const execute = (
if (!pipeline || !bindGroupLayout) {
console.error("[WebGPU BevelFilter] Pipeline not found");
frameBufferManager.releaseTemporaryAttachment(blurAttachment);
- return sourceAttachment;
+ return source_attachment;
}
// サンプラーを作成
@@ -220,8 +225,8 @@ export const execute = (
// baseScale, baseOffset (16 bytes)
// blurScale, blurOffset (16 bytes)
// Total: 80 bytes → 16 floats + 4 floats = 20 floats (80 bytes)
- const [hr, hg, hb, ha] = intToRGBA(highlightColor, highlightAlpha);
- const [sr, sg, sb, sa] = intToRGBA(shadowColor, shadowAlpha);
+ const [hr, hg, hb, ha] = intToPremultipliedRGBA(highlight_color, highlight_alpha);
+ const [sr, sg, sb, sa] = intToPremultipliedRGBA(shadow_color, shadow_alpha);
$uniform20[0] = hr;
$uniform20[1] = hg;
@@ -258,7 +263,7 @@ export const execute = (
($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries4[1].resource = sampler;
$entries4[2].resource = blurAttachment.texture!.view;
- $entries4[3].resource = sourceAttachment.texture!.view;
+ $entries4[3].resource = source_attachment.texture!.view;
const bindGroup = device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries4
diff --git a/packages/webgpu/src/Filter/BevelFilterShader.test.ts b/packages/webgpu/src/Filter/BevelFilterShader.test.ts
deleted file mode 100644
index ec27301e..00000000
--- a/packages/webgpu/src/Filter/BevelFilterShader.test.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { describe, it, expect } from "vitest";
-import { getBevelFilterFragmentShader, getBevelFilterShaderKey } from "./BevelFilterShader";
-
-describe("BevelFilterShader", () =>
-{
- describe("getBevelFilterFragmentShader", () =>
- {
- it("should return a valid WGSL shader string for full type", () =>
- {
- const shader = getBevelFilterFragmentShader("full", false, false);
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should return a valid shader for inner type", () =>
- {
- const shader = getBevelFilterFragmentShader("inner", false, false);
-
- expect(shader).toContain("filterColor * baseAlpha");
- });
-
- it("should return a valid shader for outer type", () =>
- {
- const shader = getBevelFilterFragmentShader("outer", false, false);
-
- expect(shader).toContain("filterColor * (1.0 - baseAlpha)");
- });
-
- it("should contain @vertex attribute", () =>
- {
- const shader = getBevelFilterFragmentShader("full", false, false);
-
- expect(shader).toContain("@vertex");
- });
-
- it("should contain @fragment attribute", () =>
- {
- const shader = getBevelFilterFragmentShader("full", false, false);
-
- expect(shader).toContain("@fragment");
- });
-
- it("should define BevelUniforms struct", () =>
- {
- const shader = getBevelFilterFragmentShader("full", false, false);
-
- expect(shader).toContain("struct BevelUniforms");
- });
-
- it("should include highlight and shadow colors", () =>
- {
- const shader = getBevelFilterFragmentShader("full", false, false);
-
- expect(shader).toContain("highlightColor");
- expect(shader).toContain("shadowColor");
- });
-
- it("should handle knockout mode", () =>
- {
- const shader = getBevelFilterFragmentShader("full", true, false);
-
- expect(shader).toContain("finalColor");
- });
-
- it("should include gradient texture binding when isGradient is true", () =>
- {
- const shader = getBevelFilterFragmentShader("full", false, true);
-
- expect(shader).toContain("gradientTexture");
- });
-
- it("should not include gradient texture binding when isGradient is false", () =>
- {
- const shader = getBevelFilterFragmentShader("full", false, false);
-
- expect(shader).not.toContain("gradientTexture");
- });
-
- it("should use gradient LUT when isGradient is true", () =>
- {
- const shader = getBevelFilterFragmentShader("full", false, true);
-
- expect(shader).toContain("gradientCoord");
- });
- });
-
- describe("getBevelFilterShaderKey", () =>
- {
- it("should generate unique key for full type", () =>
- {
- const key = getBevelFilterShaderKey("full", false, false);
-
- expect(key).toBe("bevel_full_nko_ng");
- });
-
- it("should generate unique key for inner type with knockout", () =>
- {
- const key = getBevelFilterShaderKey("inner", true, false);
-
- expect(key).toBe("bevel_inner_ko_ng");
- });
-
- it("should generate unique key for outer type with gradient", () =>
- {
- const key = getBevelFilterShaderKey("outer", false, true);
-
- expect(key).toBe("bevel_outer_nko_g");
- });
-
- it("should generate different keys for different configurations", () =>
- {
- const key1 = getBevelFilterShaderKey("full", false, false);
- const key2 = getBevelFilterShaderKey("full", true, false);
- const key3 = getBevelFilterShaderKey("full", false, true);
-
- expect(key1).not.toBe(key2);
- expect(key1).not.toBe(key3);
- expect(key2).not.toBe(key3);
- });
- });
-});
diff --git a/packages/webgpu/src/Filter/BevelFilterShader.ts b/packages/webgpu/src/Filter/BevelFilterShader.ts
deleted file mode 100644
index 5c390985..00000000
--- a/packages/webgpu/src/Filter/BevelFilterShader.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-export const getBevelFilterFragmentShader = (
- type: string,
- knockout: boolean,
- isGradient: boolean
-): string => {
- const isInner = type === "inner";
- const isOuter = type === "outer";
-
- const gradientBinding = isGradient ? `
-@group(0) @binding(4) var gradientTexture: texture_2d;` : "";
-
- const colorCalculation = isGradient ? `
- let gradientCoord = vec2(blurAlpha, 0.5);
- var filterColor = textureSample(gradientTexture, sourceSampler, gradientCoord);
-` : `
- let highlightWeight = clamp(blurAlpha * 2.0, 0.0, 1.0);
- let shadowWeight = clamp((1.0 - blurAlpha) * 2.0, 0.0, 1.0);
- var filterColor = uniforms.highlightColor * highlightWeight + uniforms.shadowColor * shadowWeight;
-`;
-
- let typeProcessing = "";
- if (isInner) {
- typeProcessing = `
- let baseAlpha = textureSample(baseTexture, sourceSampler, baseTexCoord).a;
- filterColor = filterColor * baseAlpha;
- ${knockout ? "let finalColor = filterColor;" : "let finalColor = mix(baseColor, filterColor, filterColor.a);"}
-`;
- } else if (isOuter) {
- typeProcessing = `
- let baseAlpha = textureSample(baseTexture, sourceSampler, baseTexCoord).a;
- filterColor = filterColor * (1.0 - baseAlpha);
- ${knockout ? "let finalColor = filterColor;" : "let finalColor = filterColor + baseColor * (1.0 - filterColor.a);"}
-`;
- } else {
- typeProcessing = knockout ? `
- let finalColor = filterColor;
-` : `
- let finalColor = filterColor + baseColor * (1.0 - filterColor.a);
-`;
- }
-
- return `
-struct BevelUniforms {
- blurTexCoordScale: vec2,
- blurTexCoordOffset: vec2,
- baseTexCoordScale: vec2,
- baseTexCoordOffset: vec2,
- strength: f32,
- _pad1: f32,
- _pad2: f32,
- _pad3: f32,
- highlightColor: vec4,
- shadowColor: vec4,
-}
-
-@group(0) @binding(0) var uniforms: BevelUniforms;
-@group(0) @binding(1) var sourceSampler: sampler;
-@group(0) @binding(2) var blurTexture: texture_2d;
-@group(0) @binding(3) var baseTexture: texture_2d;
-${gradientBinding}
-
-struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
-}
-
-@vertex
-fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
- var positions = array, 6>(
- vec2(-1.0, -1.0),
- vec2(1.0, -1.0),
- vec2(-1.0, 1.0),
- vec2(-1.0, 1.0),
- vec2(1.0, -1.0),
- vec2(1.0, 1.0)
- );
-
- var texCoords = array, 6>(
- vec2(0.0, 1.0),
- vec2(1.0, 1.0),
- vec2(0.0, 0.0),
- vec2(0.0, 0.0),
- vec2(1.0, 1.0),
- vec2(1.0, 0.0)
- );
-
- var output: VertexOutput;
- output.position = vec4(positions[vertexIndex], 0.0, 1.0);
- output.texCoord = texCoords[vertexIndex];
- return output;
-}
-
-@fragment
-fn fs_main(input: VertexOutput) -> @location(0) vec4 {
- let blurTexCoord = input.texCoord * uniforms.blurTexCoordScale + uniforms.blurTexCoordOffset;
- let baseTexCoord = input.texCoord * uniforms.baseTexCoordScale + uniforms.baseTexCoordOffset;
-
- let blurColor = textureSample(blurTexture, sourceSampler, blurTexCoord);
- var blurAlpha = blurColor.a * uniforms.strength;
- blurAlpha = clamp(blurAlpha, 0.0, 1.0);
-
- let baseColor = textureSample(baseTexture, sourceSampler, baseTexCoord);
-
- ${colorCalculation}
- ${typeProcessing}
-
- return finalColor;
-}
-`;
-};
-
-export const getBevelFilterShaderKey = (
- type: string,
- knockout: boolean,
- isGradient: boolean
-): string => {
- return `bevel_${type}_${knockout ? "ko" : "nko"}_${isGradient ? "g" : "ng"}`;
-};
diff --git a/packages/webgpu/src/Filter/BitmapFilterShader.test.ts b/packages/webgpu/src/Filter/BitmapFilterShader.test.ts
deleted file mode 100644
index 332c8f13..00000000
--- a/packages/webgpu/src/Filter/BitmapFilterShader.test.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { describe, it, expect } from "vitest";
-import { getBitmapFilterFragmentShader, getBitmapFilterShaderKey } from "./BitmapFilterShader";
-
-describe("BitmapFilterShader", () =>
-{
- describe("getBitmapFilterFragmentShader", () =>
- {
- it("should return a valid WGSL shader string", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false);
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should contain @vertex attribute", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false);
-
- expect(shader).toContain("@vertex");
- });
-
- it("should contain @fragment attribute", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false);
-
- expect(shader).toContain("@fragment");
- });
-
- it("should define BitmapFilterUniforms struct", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false);
-
- expect(shader).toContain("struct BitmapFilterUniforms");
- });
-
- it("should include base scale/offset when transformsBase is true", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, false, true, "full", false, false, false);
-
- expect(shader).toContain("baseScale");
- expect(shader).toContain("baseOffset");
- });
-
- it("should include blur scale/offset when transformsBlur is true", () =>
- {
- const shader = getBitmapFilterFragmentShader(false, true, true, "full", false, false, false);
-
- expect(shader).toContain("blurScale");
- expect(shader).toContain("blurOffset");
- });
-
- it("should include strength when appliesStrength is true", () =>
- {
- const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, true, false);
-
- expect(shader).toContain("strength");
- });
-
- it("should include color for glow mode", () =>
- {
- const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, false, false);
-
- expect(shader).toContain("color");
- });
-
- it("should include highlight/shadow colors for bevel mode", () =>
- {
- const shader = getBitmapFilterFragmentShader(false, false, false, "full", false, false, false);
-
- expect(shader).toContain("highlightColor");
- expect(shader).toContain("shadowColor");
- });
-
- it("should include gradient texture when isGradient is true", () =>
- {
- const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, false, true);
-
- expect(shader).toContain("gradientTexture");
- });
-
- it("should handle inner type", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, true, true, "inner", false, true, false);
-
- expect(shader).toContain("blur");
- });
-
- it("should handle outer type", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, true, true, "outer", false, true, false);
-
- expect(shader).toContain("blur");
- });
-
- it("should handle knockout mode", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, true, true, "full", true, true, false);
-
- expect(shader).toBeDefined();
- });
-
- it("should include isInside helper function", () =>
- {
- const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false);
-
- expect(shader).toContain("fn isInside");
- });
- });
-
- describe("getBitmapFilterShaderKey", () =>
- {
- it("should generate unique key for glow configuration", () =>
- {
- const key = getBitmapFilterShaderKey(true, true, true, "full", false, true, false);
-
- expect(key).toBe("bitmap_yygfullnsso");
- });
-
- it("should generate unique key for bevel configuration", () =>
- {
- const key = getBitmapFilterShaderKey(true, true, false, "full", false, true, false);
-
- expect(key).toBe("bitmap_yybfullnsso");
- });
-
- it("should generate different keys for different configurations", () =>
- {
- const key1 = getBitmapFilterShaderKey(true, true, true, "full", false, true, false);
- const key2 = getBitmapFilterShaderKey(true, true, true, "inner", false, true, false);
- const key3 = getBitmapFilterShaderKey(true, true, true, "full", true, true, false);
-
- expect(key1).not.toBe(key2);
- expect(key1).not.toBe(key3);
- });
-
- it("should include gradient flag in key", () =>
- {
- const keyNonGradient = getBitmapFilterShaderKey(true, true, true, "full", false, true, false);
- const keyGradient = getBitmapFilterShaderKey(true, true, true, "full", false, true, true);
-
- expect(keyNonGradient).toContain("so");
- expect(keyGradient).toContain("gr");
- });
- });
-});
diff --git a/packages/webgpu/src/Filter/BitmapFilterShader.ts b/packages/webgpu/src/Filter/BitmapFilterShader.ts
deleted file mode 100644
index 2e6fe223..00000000
--- a/packages/webgpu/src/Filter/BitmapFilterShader.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-export const getBitmapFilterFragmentShader = (
- transformsBase: boolean,
- transformsBlur: boolean,
- isGlow: boolean,
- type: string,
- knockout: boolean,
- appliesStrength: boolean,
- isGradient: boolean
-): string => {
- const isInner = type === "inner";
-
- let textureBindingIndex = 2;
- const blurTextureBinding = textureBindingIndex++;
- const baseTextureBinding = transformsBase ? textureBindingIndex++ : -1;
- const gradientTextureBinding = isGradient ? textureBindingIndex++ : -1;
-
- let uniformsStruct = `struct BitmapFilterUniforms {
-`;
- if (transformsBase) {
- uniformsStruct += ` baseScale: vec2,
- baseOffset: vec2,
-`;
- }
- if (transformsBlur) {
- uniformsStruct += ` blurScale: vec2,
- blurOffset: vec2,
-`;
- }
- if (appliesStrength) {
- uniformsStruct += ` strength: f32,
- _padStrength: vec3,
-`;
- }
- if (!isGradient) {
- if (isGlow) {
- uniformsStruct += ` color: vec4,
-`;
- } else {
- uniformsStruct += ` highlightColor: vec4,
- shadowColor: vec4,
-`;
- }
- }
- uniformsStruct += "}";
-
- let textureBindings = `
-@group(0) @binding(0) var uniforms: BitmapFilterUniforms;
-@group(0) @binding(1) var sourceSampler: sampler;
-@group(0) @binding(${blurTextureBinding}) var blurTexture: texture_2d;`;
-
- if (transformsBase) {
- textureBindings += `
-@group(0) @binding(${baseTextureBinding}) var baseTexture: texture_2d;`;
- }
- if (isGradient) {
- textureBindings += `
-@group(0) @binding(${gradientTextureBinding}) var gradientTexture: texture_2d;`;
- }
-
- let baseStatement = "";
- if (transformsBase) {
- baseStatement = `
- let baseScale = uniforms.baseScale;
- let baseOffset = uniforms.baseOffset;
- let uv = input.texCoord * baseScale - baseOffset;
- let base = mix(vec4(0.0), textureSample(baseTexture, sourceSampler, uv), isInside(uv));`;
- }
-
- let blurStatement = "";
- if (transformsBlur) {
- blurStatement = `
- let blurScale = uniforms.blurScale;
- let blurOffset = uniforms.blurOffset;
- let st = input.texCoord * blurScale - blurOffset;
- var blur = mix(vec4(0.0), textureSample(blurTexture, sourceSampler, st), isInside(st));`;
- } else {
- blurStatement = `
- var blur = textureSample(blurTexture, sourceSampler, input.texCoord);`;
- }
-
- let colorStatement = "";
- if (isGlow) {
- if (isInner) {
- colorStatement += `
- blur.a = 1.0 - blur.a;`;
- }
- if (appliesStrength) {
- colorStatement += `
- let strength = uniforms.strength;
- blur.a = clamp(blur.a * strength, 0.0, 1.0);`;
- }
- if (isGradient) {
- colorStatement += `
- blur = textureSample(gradientTexture, sourceSampler, vec2(blur.a, 0.5));`;
- } else {
- colorStatement += `
- let color = uniforms.color;
- blur = color * blur.a;`;
- }
- } else {
- if (transformsBlur) {
- colorStatement += `
- let pq = (vec2(1.0) - input.texCoord) * blurScale - blurOffset;
- let blur2 = mix(vec4(0.0), textureSample(blurTexture, sourceSampler, pq), isInside(pq));`;
- } else {
- colorStatement += `
- let blur2 = textureSample(blurTexture, sourceSampler, vec2(1.0) - input.texCoord);`;
- }
- colorStatement += `
- var highlightAlpha = blur.a - blur2.a;
- var shadowAlpha = blur2.a - blur.a;`;
-
- if (appliesStrength) {
- colorStatement += `
- let strength = uniforms.strength;
- highlightAlpha = highlightAlpha * strength;
- shadowAlpha = shadowAlpha * strength;`;
- }
-
- colorStatement += `
- highlightAlpha = clamp(highlightAlpha, 0.0, 1.0);
- shadowAlpha = clamp(shadowAlpha, 0.0, 1.0);`;
-
- if (isGradient) {
- colorStatement += `
- blur = textureSample(gradientTexture, sourceSampler, vec2(
- 0.5019607843137255 - 0.5019607843137255 * shadowAlpha + 0.4980392156862745 * highlightAlpha,
- 0.5
- ));`;
- } else {
- colorStatement += `
- let highlightColor = uniforms.highlightColor;
- let shadowColor = uniforms.shadowColor;
- blur = highlightColor * highlightAlpha + shadowColor * shadowAlpha;`;
- }
- }
-
- let modeExpression = "";
- switch (type) {
- case "outer":
- modeExpression = knockout
- ? "blur - blur * base.a"
- : "base + blur - blur * base.a";
- break;
- case "full":
- modeExpression = knockout
- ? "blur"
- : "base - base * blur.a + blur";
- break;
- case "inner":
- default:
- modeExpression = "blur";
- break;
- }
-
- const needsBase = transformsBase || (type === "outer" || type === "full" && !knockout);
- let baseDecl = "";
- if (needsBase && !transformsBase) {
- baseDecl = `
- let base = vec4(0.0);`;
- }
-
- return `
-${uniformsStruct}
-${textureBindings}
-
-struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
-}
-
-fn isInside(uv: vec2) -> f32 {
- let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0));
- return inside.x * inside.y;
-}
-
-@vertex
-fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
- var positions = array, 6>(
- vec2(-1.0, -1.0),
- vec2(1.0, -1.0),
- vec2(-1.0, 1.0),
- vec2(-1.0, 1.0),
- vec2(1.0, -1.0),
- vec2(1.0, 1.0)
- );
-
- var texCoords = array, 6>(
- vec2(0.0, 1.0),
- vec2(1.0, 1.0),
- vec2(0.0, 0.0),
- vec2(0.0, 0.0),
- vec2(1.0, 1.0),
- vec2(1.0, 0.0)
- );
-
- var output: VertexOutput;
- output.position = vec4(positions[vertexIndex], 0.0, 1.0);
- output.texCoord = texCoords[vertexIndex];
- return output;
-}
-
-@fragment
-fn fs_main(input: VertexOutput) -> @location(0) vec4 {
- ${baseDecl}
- ${baseStatement}
- ${blurStatement}
- ${colorStatement}
-
- return ${modeExpression};
-}
-`;
-};
-
-export const getBitmapFilterShaderKey = (
- transformsBase: boolean,
- transformsBlur: boolean,
- isGlow: boolean,
- type: string,
- knockout: boolean,
- appliesStrength: boolean,
- isGradient: boolean
-): string => {
- const key1 = transformsBase ? "y" : "n";
- const key2 = transformsBlur ? "y" : "n";
- const key3 = isGlow ? "g" : "b";
- const key4 = knockout ? "k" : "n";
- const key5 = appliesStrength ? "s" : "n";
- const key6 = isGradient ? "gr" : "so";
- return `bitmap_${key1}${key2}${key3}${type}${key4}${key5}${key6}`;
-};
diff --git a/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts b/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts
index 84f7b231..67a97c91 100644
--- a/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts
+++ b/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts
@@ -2,8 +2,6 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject";
import type { IFilterConfig } from "../../interface/IFilterConfig";
import { $offset } from "../FilterOffset";
import { calculateBlurParams, calculateDirectionalBlurParams } from "../BlurFilterUseCase";
-import { shouldUseComputeShader } from "./service/BlurFilterComputeShaderService";
-import { execute as executeBlurCompute } from "../../Compute/service/ComputeExecuteBlurService";
/**
* @description プリアロケートされたFloat32Array (サイズ4)
@@ -23,29 +21,29 @@ const $entries3: GPUBindGroupEntry[] = [
* @description ブラーフィルターを適用
* Apply blur filter
*
- * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ(アタッチメント)
+ * @param {IAttachmentObject} source_attachment - 入力テクスチャ(アタッチメント)
* @param {Float32Array} matrix - 変換行列
- * @param {number} blurX - X方向のブラー量
- * @param {number} blurY - Y方向のブラー量
+ * @param {number} blur_x - X方向のブラー量
+ * @param {number} blur_y - Y方向のブラー量
* @param {number} quality - クオリティ (1-15)
- * @param {number} devicePixelRatio - デバイスピクセル比
+ * @param {number} device_pixel_ratio - デバイスピクセル比
* @param {IFilterConfig} config - WebGPUリソース設定
* @return {IAttachmentObject} - フィルター適用後のアタッチメント
*/
export const execute = (
- sourceAttachment: IAttachmentObject,
+ source_attachment: IAttachmentObject,
matrix: Float32Array,
- blurX: number,
- blurY: number,
+ blur_x: number,
+ blur_y: number,
quality: number,
- devicePixelRatio: number,
+ device_pixel_ratio: number,
config: IFilterConfig
): IAttachmentObject => {
const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config;
// ブラーパラメータを計算
- const blurParams = calculateBlurParams(matrix, blurX, blurY, quality, devicePixelRatio);
+ const blurParams = calculateBlurParams(matrix, blur_x, blur_y, quality, device_pixel_ratio);
const { baseBlurX, baseBlurY, offsetX, offsetY, bufferScaleX, bufferScaleY } = blurParams;
// オフセットを更新
@@ -53,8 +51,8 @@ export const execute = (
$offset.y += offsetY;
// ブラー用バッファサイズを計算
- const width = sourceAttachment.width + offsetX * 2;
- const height = sourceAttachment.height + offsetY * 2;
+ const width = source_attachment.width + offsetX * 2;
+ const height = source_attachment.height + offsetY * 2;
const bufferWidth = Math.ceil(width * bufferScaleX);
const bufferHeight = Math.ceil(height * bufferScaleY);
@@ -68,7 +66,7 @@ export const execute = (
// ソーステクスチャをattachment0にコピー(スケーリング付き)
copyTextureToAttachment(
device, commandEncoder, frameBufferManager, pipelineManager,
- sourceAttachment, attachment0, sampler,
+ source_attachment, attachment0, sampler,
bufferScaleX, bufferScaleY,
offsetX * bufferScaleX, offsetY * bufferScaleY,
config.bufferManager
@@ -78,53 +76,33 @@ export const execute = (
const bufferBlurX = baseBlurX * bufferScaleX;
const bufferBlurY = baseBlurY * bufferScaleY;
- // Compute Shaderを使用すべきか判定
- const useCompute = config.computePipelineManager
- && shouldUseComputeShader(baseBlurX, baseBlurY, bufferWidth, bufferHeight);
-
// ブラーパスを実行
const attachments = [attachment0, attachment1];
let attachmentIndex = 0;
for (let q = 0; q < quality; ++q) {
// 水平ブラー
- if (blurX > 0) {
+ if (blur_x > 0) {
const srcIndex = attachmentIndex;
attachmentIndex = (attachmentIndex + 1) % 2;
- if (useCompute) {
- executeBlurCompute(
- device, commandEncoder, config.computePipelineManager!,
- attachments[srcIndex], attachments[attachmentIndex],
- true, bufferBlurX, config.bufferManager
- );
- } else {
- applyDirectionalBlur(
- device, commandEncoder, frameBufferManager, pipelineManager,
- attachments[srcIndex], attachments[attachmentIndex], sampler,
- true, bufferBlurX, config.bufferManager
- );
- }
+ applyDirectionalBlur(
+ device, commandEncoder, frameBufferManager, pipelineManager,
+ attachments[srcIndex], attachments[attachmentIndex], sampler,
+ true, bufferBlurX, config.bufferManager
+ );
}
// 垂直ブラー
- if (blurY > 0) {
+ if (blur_y > 0) {
const srcIndex = attachmentIndex;
attachmentIndex = (attachmentIndex + 1) % 2;
- if (useCompute) {
- executeBlurCompute(
- device, commandEncoder, config.computePipelineManager!,
- attachments[srcIndex], attachments[attachmentIndex],
- false, bufferBlurY, config.bufferManager
- );
- } else {
- applyDirectionalBlur(
- device, commandEncoder, frameBufferManager, pipelineManager,
- attachments[srcIndex], attachments[attachmentIndex], sampler,
- false, bufferBlurY, config.bufferManager
- );
- }
+ applyDirectionalBlur(
+ device, commandEncoder, frameBufferManager, pipelineManager,
+ attachments[srcIndex], attachments[attachmentIndex], sampler,
+ false, bufferBlurY, config.bufferManager
+ );
}
}
@@ -138,7 +116,6 @@ export const execute = (
upscaleTexture(
device, commandEncoder, frameBufferManager, pipelineManager,
resultAttachment, finalAttachment, sampler,
- 1 / bufferScaleX, 1 / bufferScaleY,
config.bufferManager
);
@@ -158,31 +135,39 @@ export const execute = (
/**
* @description テクスチャをアタッチメントにコピー(オフセット位置に配置、スケーリング対応)
+ * Copy texture to attachment with offset placement and scaling support
*
- * @param source - ソーステクスチャ
- * @param dest - デストテクスチャ(ソースより大きい)
- * @param bufferScaleX - X方向のバッファスケール
- * @param bufferScaleY - Y方向のバッファスケール
- * @param pixelOffsetX - デスト内でのX方向オフセット(ピクセル単位、スケーリング済み)
- * @param pixelOffsetY - デスト内でのY方向オフセット(ピクセル単位、スケーリング済み)
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー
+ * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー
+ * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー
+ * @param {IAttachmentObject} source - ソーステクスチャ
+ * @param {IAttachmentObject} dest - デストテクスチャ(ソースより大きい)
+ * @param {GPUSampler} sampler - サンプラー
+ * @param {number} buffer_scale_x - X方向のバッファスケール
+ * @param {number} buffer_scale_y - Y方向のバッファスケール
+ * @param {number} pixel_offset_x - デスト内でのX方向オフセット(ピクセル単位、スケーリング済み)
+ * @param {number} pixel_offset_y - デスト内でのY方向オフセット(ピクセル単位、スケーリング済み)
+ * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー
+ * @return {void}
*/
const copyTextureToAttachment = (
device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- frameBufferManager: IFilterConfig["frameBufferManager"],
- pipelineManager: IFilterConfig["pipelineManager"],
+ command_encoder: GPUCommandEncoder,
+ frame_buffer_manager: IFilterConfig["frameBufferManager"],
+ pipeline_manager: IFilterConfig["pipelineManager"],
source: IAttachmentObject,
dest: IAttachmentObject,
sampler: GPUSampler,
- bufferScaleX: number,
- bufferScaleY: number,
- pixelOffsetX: number,
- pixelOffsetY: number,
- bufferManager?: IFilterConfig["bufferManager"]
+ buffer_scale_x: number,
+ buffer_scale_y: number,
+ pixel_offset_x: number,
+ pixel_offset_y: number,
+ buffer_manager?: IFilterConfig["bufferManager"]
): void => {
// texture_copy_rgba8を使用し、ビューポートでオフセットを制御
- const pipeline = pipelineManager.getPipeline("texture_copy_rgba8");
- const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy");
+ const pipeline = pipeline_manager.getPipeline("texture_copy_rgba8");
+ const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy");
if (!pipeline || !bindGroupLayout) {
console.error("[WebGPU BlurFilter] texture_copy_rgba8 pipeline not found");
@@ -190,8 +175,8 @@ const copyTextureToAttachment = (
}
// デスト内でのソース描画サイズ(スケーリング後)
- const scaledSourceWidth = source.width * bufferScaleX;
- const scaledSourceHeight = source.height * bufferScaleY;
+ const scaledSourceWidth = source.width * buffer_scale_x;
+ const scaledSourceHeight = source.height * buffer_scale_y;
// シェーダー: uv = texCoord * scale + offset
// ソース全体をサンプリングするので scale = 1, offset = 0
@@ -205,13 +190,13 @@ const copyTextureToAttachment = (
$uniform4[1] = scaleY;
$uniform4[2] = offsetX;
$uniform4[3] = offsetY;
- const uniformBuffer = bufferManager
- ? bufferManager.acquireAndWriteUniformBuffer($uniform4)
+ const uniformBuffer = buffer_manager
+ ? buffer_manager.acquireAndWriteUniformBuffer($uniform4)
: device.createBuffer({
"size": $uniform4.byteLength,
"usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
- if (!bufferManager) {
+ if (!buffer_manager) {
device.queue.writeBuffer(uniformBuffer, 0, $uniform4);
}
@@ -223,22 +208,22 @@ const copyTextureToAttachment = (
"entries": $entries3
});
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
+ const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor(
dest.texture!.view, 0, 0, 0, 0, "clear"
);
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
// ビューポートを設定してオフセット位置に描画
passEncoder.setViewport(
- pixelOffsetX, pixelOffsetY,
+ pixel_offset_x, pixel_offset_y,
scaledSourceWidth, scaledSourceHeight,
0, 1
);
passEncoder.setScissorRect(
- Math.floor(pixelOffsetX), Math.floor(pixelOffsetY),
+ Math.floor(pixel_offset_x), Math.floor(pixel_offset_y),
Math.ceil(scaledSourceWidth), Math.ceil(scaledSourceHeight)
);
@@ -249,21 +234,34 @@ const copyTextureToAttachment = (
/**
* @description 方向ブラーを適用
+ * Apply directional blur pass
+ *
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー
+ * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー
+ * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー
+ * @param {IAttachmentObject} source - ソーステクスチャ
+ * @param {IAttachmentObject} dest - デストテクスチャ
+ * @param {GPUSampler} sampler - サンプラー
+ * @param {boolean} is_horizontal - 水平方向かどうか
+ * @param {number} blur - ブラー量
+ * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー
+ * @return {void}
*/
const applyDirectionalBlur = (
device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- frameBufferManager: IFilterConfig["frameBufferManager"],
- pipelineManager: IFilterConfig["pipelineManager"],
+ command_encoder: GPUCommandEncoder,
+ frame_buffer_manager: IFilterConfig["frameBufferManager"],
+ pipeline_manager: IFilterConfig["pipelineManager"],
source: IAttachmentObject,
dest: IAttachmentObject,
sampler: GPUSampler,
- isHorizontal: boolean,
+ is_horizontal: boolean,
blur: number,
- bufferManager?: IFilterConfig["bufferManager"]
+ buffer_manager?: IFilterConfig["bufferManager"]
): void => {
const params = calculateDirectionalBlurParams(
- isHorizontal, blur,
+ is_horizontal, blur,
source.width, source.height
);
@@ -271,8 +269,8 @@ const applyDirectionalBlur = (
// halfBlurに対応するパイプラインを取得(1〜16の範囲でクランプ)
const clampedHalfBlur = Math.max(1, Math.min(16, halfBlur));
- const pipeline = pipelineManager.getPipeline(`blur_filter_${clampedHalfBlur}`);
- const bindGroupLayout = pipelineManager.getBindGroupLayout("blur_filter");
+ const pipeline = pipeline_manager.getPipeline(`blur_filter_${clampedHalfBlur}`);
+ const bindGroupLayout = pipeline_manager.getBindGroupLayout("blur_filter");
if (!pipeline || !bindGroupLayout) {
console.error(`[WebGPU BlurFilter] blur_filter_${clampedHalfBlur} pipeline not found`);
@@ -284,13 +282,13 @@ const applyDirectionalBlur = (
$uniform4[1] = offsetY;
$uniform4[2] = fraction;
$uniform4[3] = samples;
- const uniformBuffer = bufferManager
- ? bufferManager.acquireAndWriteUniformBuffer($uniform4)
+ const uniformBuffer = buffer_manager
+ ? buffer_manager.acquireAndWriteUniformBuffer($uniform4)
: device.createBuffer({
"size": $uniform4.byteLength,
"usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
- if (!bufferManager) {
+ if (!buffer_manager) {
device.queue.writeBuffer(uniformBuffer, 0, $uniform4);
}
@@ -302,11 +300,11 @@ const applyDirectionalBlur = (
"entries": $entries3
});
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
+ const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor(
dest.texture!.view, 0, 0, 0, 0, "clear"
);
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(6, 1, 0, 0);
@@ -316,22 +314,31 @@ const applyDirectionalBlur = (
/**
* @description テクスチャをアップスケール(ソース全体をデスト全体にマッピング)
+ * Upscale texture by mapping entire source to entire destination
+ *
+ * @param {GPUDevice} device - GPUデバイス
+ * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー
+ * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー
+ * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー
+ * @param {IAttachmentObject} source - ソーステクスチャ
+ * @param {IAttachmentObject} dest - デストテクスチャ
+ * @param {GPUSampler} sampler - サンプラー
+ * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー
+ * @return {void}
*/
const upscaleTexture = (
device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- frameBufferManager: IFilterConfig["frameBufferManager"],
- pipelineManager: IFilterConfig["pipelineManager"],
+ command_encoder: GPUCommandEncoder,
+ frame_buffer_manager: IFilterConfig["frameBufferManager"],
+ pipeline_manager: IFilterConfig["pipelineManager"],
source: IAttachmentObject,
dest: IAttachmentObject,
sampler: GPUSampler,
- _scaleX: number,
- _scaleY: number,
- bufferManager?: IFilterConfig["bufferManager"]
+ buffer_manager?: IFilterConfig["bufferManager"]
): void => {
// temp_アタッチメントはrgba8unormフォーマットなので、texture_copy_rgba8パイプラインを使用
- const pipeline = pipelineManager.getPipeline("texture_copy_rgba8");
- const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy");
+ const pipeline = pipeline_manager.getPipeline("texture_copy_rgba8");
+ const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy");
if (!pipeline || !bindGroupLayout) {
console.error("[WebGPU BlurFilter] texture_copy_rgba8 pipeline not found");
@@ -345,13 +352,13 @@ const upscaleTexture = (
$uniform4[1] = 1;
$uniform4[2] = 0;
$uniform4[3] = 0;
- const uniformBuffer = bufferManager
- ? bufferManager.acquireAndWriteUniformBuffer($uniform4)
+ const uniformBuffer = buffer_manager
+ ? buffer_manager.acquireAndWriteUniformBuffer($uniform4)
: device.createBuffer({
"size": $uniform4.byteLength,
"usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
- if (!bufferManager) {
+ if (!buffer_manager) {
device.queue.writeBuffer(uniformBuffer, 0, $uniform4);
}
@@ -363,11 +370,11 @@ const upscaleTexture = (
"entries": $entries3
});
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
+ const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor(
dest.texture!.view, 0, 0, 0, 0, "clear"
);
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
+ const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(6, 1, 0, 0);
diff --git a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts b/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts
deleted file mode 100644
index 073925d7..00000000
--- a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts
+++ /dev/null
@@ -1,289 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import type { IAttachmentObject } from "../../../interface/IAttachmentObject";
-import type { IFilterConfig } from "../../../interface/IFilterConfig";
-import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager";
-import { execute, shouldUseComputeShader } from "./BlurFilterComputeShaderService";
-
-// Mock the compute blur service
-vi.mock("../../../Compute/service/ComputeExecuteBlurService", () => ({
- "execute": vi.fn()
-}));
-
-import { execute as mockExecuteBlurCompute } from "../../../Compute/service/ComputeExecuteBlurService";
-
-describe("BlurFilterComputeShaderService", () =>
-{
- const createMockAttachment = (width: number = 256, height: number = 256): IAttachmentObject =>
- {
- return {
- "id": 1,
- "width": width,
- "height": height,
- "clipLevel": 0,
- "msaa": false,
- "mask": false,
- "color": null,
- "texture": {
- "id": 1,
- "width": width,
- "height": height,
- "area": width * height,
- "smooth": true,
- "resource": {} as GPUTexture,
- "view": {} as GPUTextureView
- },
- "stencil": null,
- "msaaTexture": null,
- "msaaStencil": null
- };
- };
-
- const createMockDevice = () =>
- {
- return {} as GPUDevice;
- };
-
- const createMockCommandEncoder = () =>
- {
- return {} as GPUCommandEncoder;
- };
-
- const createMockComputePipelineManager = () =>
- {
- return {} as ComputePipelineManager;
- };
-
- const createMockConfig = () =>
- {
- return {} as IFilterConfig;
- };
-
- beforeEach(() =>
- {
- vi.clearAllMocks();
- });
-
- describe("execute", () =>
- {
- it("should call executeBlurCompute with correct parameters", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const computePipelineManager = createMockComputePipelineManager();
- const config = createMockConfig();
- const source = createMockAttachment();
- const dest = createMockAttachment();
-
- execute(device, commandEncoder, computePipelineManager, config, source, dest, true, 16);
-
- expect(mockExecuteBlurCompute).toHaveBeenCalledWith(
- device,
- commandEncoder,
- computePipelineManager,
- source,
- dest,
- true,
- 8 // radius = ceil(16 / 2) = 8
- );
- });
-
- it("should calculate radius as ceil of blur / 2", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const computePipelineManager = createMockComputePipelineManager();
- const config = createMockConfig();
- const source = createMockAttachment();
- const dest = createMockAttachment();
-
- execute(device, commandEncoder, computePipelineManager, config, source, dest, false, 15);
-
- expect(mockExecuteBlurCompute).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- false,
- 8 // radius = ceil(15 / 2) = 8
- );
- });
-
- it("should pass isHorizontal = true for horizontal blur", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const computePipelineManager = createMockComputePipelineManager();
- const config = createMockConfig();
- const source = createMockAttachment();
- const dest = createMockAttachment();
-
- execute(device, commandEncoder, computePipelineManager, config, source, dest, true, 10);
-
- expect(mockExecuteBlurCompute).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- true,
- expect.any(Number)
- );
- });
-
- it("should pass isHorizontal = false for vertical blur", () =>
- {
- const device = createMockDevice();
- const commandEncoder = createMockCommandEncoder();
- const computePipelineManager = createMockComputePipelineManager();
- const config = createMockConfig();
- const source = createMockAttachment();
- const dest = createMockAttachment();
-
- execute(device, commandEncoder, computePipelineManager, config, source, dest, false, 10);
-
- expect(mockExecuteBlurCompute).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- false,
- expect.any(Number)
- );
- });
- });
-
- describe("shouldUseComputeShader", () =>
- {
- describe("blur threshold", () =>
- {
- it("should return true when blur >= 4 and size >= 128", () =>
- {
- const result = shouldUseComputeShader(4, 4, 128, 128);
-
- expect(result).toBe(true);
- });
-
- it("should return true when blurX >= 4 (using max)", () =>
- {
- const result = shouldUseComputeShader(5, 2, 128, 128);
-
- expect(result).toBe(true);
- });
-
- it("should return true when blurY >= 4 (using max)", () =>
- {
- const result = shouldUseComputeShader(2, 5, 128, 128);
-
- expect(result).toBe(true);
- });
-
- it("should return false when both blurs < 4", () =>
- {
- const result = shouldUseComputeShader(3, 3, 128, 128);
-
- expect(result).toBe(false);
- });
-
- it("should return false when max blur < 4", () =>
- {
- const result = shouldUseComputeShader(2, 3, 512, 512);
-
- expect(result).toBe(false);
- });
- });
-
- describe("size threshold", () =>
- {
- it("should return true when min size >= 128", () =>
- {
- const result = shouldUseComputeShader(10, 10, 128, 128);
-
- expect(result).toBe(true);
- });
-
- it("should return true when width >= 128 and height > 128", () =>
- {
- const result = shouldUseComputeShader(10, 10, 128, 512);
-
- expect(result).toBe(true);
- });
-
- it("should return true when height >= 128 and width > 128", () =>
- {
- const result = shouldUseComputeShader(10, 10, 512, 128);
-
- expect(result).toBe(true);
- });
-
- it("should return false when width < 128", () =>
- {
- const result = shouldUseComputeShader(10, 10, 100, 512);
-
- expect(result).toBe(false);
- });
-
- it("should return false when height < 128", () =>
- {
- const result = shouldUseComputeShader(10, 10, 512, 100);
-
- expect(result).toBe(false);
- });
-
- it("should return false when both dimensions < 128", () =>
- {
- const result = shouldUseComputeShader(20, 20, 100, 100);
-
- expect(result).toBe(false);
- });
- });
-
- describe("edge cases", () =>
- {
- it("should return true at exact thresholds", () =>
- {
- const result = shouldUseComputeShader(4, 0, 128, 128);
-
- expect(result).toBe(true);
- });
-
- it("should return false just below blur threshold", () =>
- {
- const result = shouldUseComputeShader(3.9, 3.9, 128, 128);
-
- expect(result).toBe(false);
- });
-
- it("should return false just below size threshold", () =>
- {
- const result = shouldUseComputeShader(10, 10, 127, 127);
-
- expect(result).toBe(false);
- });
-
- it("should return false when only one condition is met", () =>
- {
- // Large blur but small size
- expect(shouldUseComputeShader(20, 20, 100, 100)).toBe(false);
-
- // Large size but small blur
- expect(shouldUseComputeShader(3, 3, 1024, 1024)).toBe(false);
- });
-
- it("should handle zero blur values", () =>
- {
- const result = shouldUseComputeShader(0, 0, 512, 512);
-
- expect(result).toBe(false);
- });
-
- it("should handle large values", () =>
- {
- const result = shouldUseComputeShader(100, 100, 4096, 4096);
-
- expect(result).toBe(true);
- });
- });
- });
-});
diff --git a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts b/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts
deleted file mode 100644
index 7cfbace5..00000000
--- a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import type { IAttachmentObject } from "../../../interface/IAttachmentObject";
-import type { IFilterConfig } from "../../../interface/IFilterConfig";
-import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager";
-import { execute as executeBlurCompute } from "../../../Compute/service/ComputeExecuteBlurService";
-
-/**
- * @description Compute Shaderでブラーパスを実行
- * Apply blur pass using Compute Shader
- *
- * Fragment Shaderベースの従来実装と比較して:
- * - 並列処理による高速化(大きな半径で20-35%)
- * - 共有メモリを活用したメモリアクセス最適化
- * - ワークグループ内でのデータ共有
- *
- * @param {GPUDevice} device - WebGPU device
- * @param {GPUCommandEncoder} commandEncoder - コマンドエンコーダー
- * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager
- * @param {IFilterConfig} config - フィルター設定
- * @param {IAttachmentObject} source - 入力アタッチメント
- * @param {IAttachmentObject} dest - 出力アタッチメント
- * @param {boolean} isHorizontal - 水平ブラーかどうか
- * @param {number} blur - ブラー量
- * @return {void}
- */
-export const execute = (
- device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- computePipelineManager: ComputePipelineManager,
- _config: IFilterConfig,
- source: IAttachmentObject,
- dest: IAttachmentObject,
- isHorizontal: boolean,
- blur: number
-): void => {
-
- // ブラー半径を計算(ブラー量の半分)
- const radius = Math.ceil(blur / 2);
-
- // Compute Shaderでブラーを実行
- executeBlurCompute(
- device,
- commandEncoder,
- computePipelineManager,
- source,
- dest,
- isHorizontal,
- radius
- );
-};
-
-/**
- * @description Compute Shaderを使用すべきかどうか判定
- * Determine whether to use Compute Shader
- *
- * 以下の条件でCompute Shaderを使用:
- * - ブラー半径が大きい(8以上)
- * - テクスチャサイズが十分大きい(256x256以上)
- *
- * 小さなブラー半径では Fragment Shader の方が効率的な場合がある。
- *
- * @param {number} blurX - X方向のブラー量
- * @param {number} blurY - Y方向のブラー量
- * @param {number} width - テクスチャ幅
- * @param {number} height - テクスチャ高さ
- * @return {boolean} Compute Shaderを使用すべきかどうか
- */
-export const shouldUseComputeShader = (
- blurX: number,
- blurY: number,
- width: number,
- height: number
-): boolean => {
-
- // ブラー半径のしきい値
- const BLUR_THRESHOLD = 4;
-
- // テクスチャサイズのしきい値
- const SIZE_THRESHOLD = 128;
-
- const maxBlur = Math.max(blurX, blurY);
- const minSize = Math.min(width, height);
-
- return maxBlur >= BLUR_THRESHOLD && minSize >= SIZE_THRESHOLD;
-};
diff --git a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts b/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts
deleted file mode 100644
index 9d8d9053..00000000
--- a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts
+++ /dev/null
@@ -1,437 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from "vitest";
-import type { IAttachmentObject } from "../../../interface/IAttachmentObject";
-import type { IFilterConfig } from "../../../interface/IFilterConfig";
-import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager";
-import { execute } from "./FilterApplyBlurComputeUseCase";
-
-// Mock GPUBufferUsage
-const GPUBufferUsage = {
- UNIFORM: 0x40,
- COPY_DST: 0x08
-};
-(globalThis as any).GPUBufferUsage = GPUBufferUsage;
-
-// Mock offset - use object that will be imported
-vi.mock("../../FilterOffset", () => ({
- "$offset": { "x": 0, "y": 0 }
-}));
-
-import { $offset } from "../../FilterOffset";
-
-// Mock calculateBlurParams
-const mockCalculateBlurParams = vi.fn();
-vi.mock("../../BlurFilterUseCase", () => ({
- "calculateBlurParams": (...args: any[]) => mockCalculateBlurParams(...args)
-}));
-
-// Mock BlurFilterComputeShaderService
-const mockBlurComputeService = vi.fn();
-const mockShouldUseComputeShader = vi.fn();
-vi.mock("../service/BlurFilterComputeShaderService", () => ({
- "execute": (...args: any[]) => mockBlurComputeService(...args),
- "shouldUseComputeShader": (...args: any[]) => mockShouldUseComputeShader(...args)
-}));
-
-// Mock FilterApplyBlurFilterUseCase (fragment fallback)
-const mockExecuteFragmentBlur = vi.fn();
-vi.mock("../FilterApplyBlurFilterUseCase", () => ({
- "execute": (...args: any[]) => mockExecuteFragmentBlur(...args)
-}));
-
-describe("FilterApplyBlurComputeUseCase", () =>
-{
- const createMockAttachment = (width: number = 100, height: number = 100): IAttachmentObject =>
- {
- return {
- "id": 1,
- "width": width,
- "height": height,
- "clipLevel": 0,
- "texture": {
- "resource": { "label": "mockTexture" } as unknown as GPUTexture,
- "view": { "label": "mockTextureView" } as unknown as GPUTextureView
- }
- } as IAttachmentObject;
- };
-
- const createMockConfig = (): IFilterConfig =>
- {
- const mockPassEncoder = {
- "setPipeline": vi.fn(),
- "setBindGroup": vi.fn(),
- "setViewport": vi.fn(),
- "setScissorRect": vi.fn(),
- "draw": vi.fn(),
- "end": vi.fn()
- };
-
- return {
- "device": {
- "createBuffer": vi.fn(() => ({ "label": "mockBuffer" })),
- "queue": { "writeBuffer": vi.fn() },
- "createBindGroup": vi.fn(() => ({ "label": "mockBindGroup" }))
- } as unknown as GPUDevice,
- "commandEncoder": {
- "beginRenderPass": vi.fn(() => mockPassEncoder)
- } as unknown as GPUCommandEncoder,
- "frameBufferManager": {
- "createTemporaryAttachment": vi.fn((w: number, h: number) => createMockAttachment(w, h)),
- "releaseTemporaryAttachment": vi.fn(),
- "createRenderPassDescriptor": vi.fn(() => ({
- "colorAttachments": [{ "view": {}, "loadOp": "clear", "storeOp": "store" }]
- }))
- },
- "pipelineManager": {
- "getPipeline": vi.fn(() => ({ "label": "mockPipeline" })),
- "getBindGroupLayout": vi.fn(() => ({ "label": "mockLayout" }))
- },
- "textureManager": {
- "createSampler": vi.fn(() => ({ "label": "mockSampler" }))
- }
- } as unknown as IFilterConfig;
- };
-
- const createMockComputePipelineManager = (): ComputePipelineManager =>
- {
- return {
- "getPipeline": vi.fn(() => ({ "label": "mockComputePipeline" })),
- "getBindGroupLayout": vi.fn(() => ({ "label": "mockComputeLayout" }))
- } as unknown as ComputePipelineManager;
- };
-
- beforeEach(() =>
- {
- vi.clearAllMocks();
- $offset.x = 0;
- $offset.y = 0;
- vi.spyOn(console, "error").mockImplementation(() => {});
-
- // Default mock implementations
- mockCalculateBlurParams.mockReturnValue({
- "baseBlurX": 16,
- "baseBlurY": 16,
- "offsetX": 32,
- "offsetY": 32,
- "bufferScaleX": 1,
- "bufferScaleY": 1
- });
- });
-
- describe("compute shader decision", () =>
- {
- it("should use fragment shader when compute is not appropriate", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(false);
- const expectedResult = createMockAttachment(200, 200);
- mockExecuteFragmentBlur.mockReturnValue(expectedResult);
-
- const sourceAttachment = createMockAttachment();
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- const result = execute(
- sourceAttachment,
- matrix,
- 4, // small blurX
- 4, // small blurY
- 1,
- 1,
- config,
- computePipelineManager
- );
-
- expect(mockShouldUseComputeShader).toHaveBeenCalled();
- expect(mockExecuteFragmentBlur).toHaveBeenCalled();
- expect(result).toBe(expectedResult);
- });
-
- it("should use compute shader when appropriate", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
- mockCalculateBlurParams.mockReturnValue({
- "baseBlurX": 32,
- "baseBlurY": 32,
- "offsetX": 64,
- "offsetY": 64,
- "bufferScaleX": 1,
- "bufferScaleY": 1
- });
-
- const sourceAttachment = createMockAttachment();
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- execute(
- sourceAttachment,
- matrix,
- 32,
- 32,
- 1,
- 1,
- config,
- computePipelineManager
- );
-
- expect(mockShouldUseComputeShader).toHaveBeenCalled();
- expect(mockExecuteFragmentBlur).not.toHaveBeenCalled();
- expect(mockBlurComputeService).toHaveBeenCalled();
- });
- });
-
- describe("blur parameters", () =>
- {
- it("should calculate blur parameters correctly", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
-
- const sourceAttachment = createMockAttachment(100, 100);
- const matrix = new Float32Array([2, 0, 0, 0, 2, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- execute(
- sourceAttachment,
- matrix,
- 16,
- 16,
- 3,
- 2,
- config,
- computePipelineManager
- );
-
- expect(mockCalculateBlurParams).toHaveBeenCalledWith(
- matrix,
- 16,
- 16,
- 3,
- 2
- );
- });
-
- it("should update offset based on blur parameters", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
- mockCalculateBlurParams.mockReturnValue({
- "baseBlurX": 16,
- "baseBlurY": 16,
- "offsetX": 20,
- "offsetY": 25,
- "bufferScaleX": 1,
- "bufferScaleY": 1
- });
-
- const sourceAttachment = createMockAttachment();
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- execute(
- sourceAttachment,
- matrix,
- 16,
- 16,
- 1,
- 1,
- config,
- computePipelineManager
- );
-
- expect($offset.x).toBe(20);
- expect($offset.y).toBe(25);
- });
- });
-
- describe("multi-pass blur", () =>
- {
- it("should perform horizontal and vertical passes for each quality level", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
-
- const sourceAttachment = createMockAttachment();
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- execute(
- sourceAttachment,
- matrix,
- 16,
- 16,
- 3, // quality = 3
- 1,
- config,
- computePipelineManager
- );
-
- // 3 quality passes * 2 directions (horizontal + vertical) = 6 calls
- expect(mockBlurComputeService).toHaveBeenCalledTimes(6);
- });
-
- it("should skip horizontal pass when blurX is 0", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
- mockCalculateBlurParams.mockReturnValue({
- "baseBlurX": 0,
- "baseBlurY": 16,
- "offsetX": 0,
- "offsetY": 32,
- "bufferScaleX": 1,
- "bufferScaleY": 1
- });
-
- const sourceAttachment = createMockAttachment();
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- execute(
- sourceAttachment,
- matrix,
- 0,
- 16,
- 1,
- 1,
- config,
- computePipelineManager
- );
-
- // Only vertical passes
- expect(mockBlurComputeService).toHaveBeenCalledTimes(1);
- // Should be called with horizontal=false
- expect(mockBlurComputeService).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- false, // horizontal = false (vertical pass)
- expect.any(Number)
- );
- });
-
- it("should skip vertical pass when blurY is 0", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
- mockCalculateBlurParams.mockReturnValue({
- "baseBlurX": 16,
- "baseBlurY": 0,
- "offsetX": 32,
- "offsetY": 0,
- "bufferScaleX": 1,
- "bufferScaleY": 1
- });
-
- const sourceAttachment = createMockAttachment();
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- execute(
- sourceAttachment,
- matrix,
- 16,
- 0,
- 1,
- 1,
- config,
- computePipelineManager
- );
-
- // Only horizontal passes
- expect(mockBlurComputeService).toHaveBeenCalledTimes(1);
- // Should be called with horizontal=true
- expect(mockBlurComputeService).toHaveBeenCalledWith(
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- expect.anything(),
- true, // horizontal = true
- expect.any(Number)
- );
- });
- });
-
- describe("buffer management", () =>
- {
- it("should create temporary attachments for ping-pong buffer", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
-
- const sourceAttachment = createMockAttachment(100, 100);
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- execute(
- sourceAttachment,
- matrix,
- 16,
- 16,
- 1,
- 1,
- config,
- computePipelineManager
- );
-
- // Should create 2 temporary attachments for ping-pong
- expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalledTimes(2);
- });
-
- it("should release unused buffer after processing", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
-
- const sourceAttachment = createMockAttachment(100, 100);
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- execute(
- sourceAttachment,
- matrix,
- 16,
- 16,
- 1,
- 1,
- config,
- computePipelineManager
- );
-
- // Should release the unused ping-pong buffer
- expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled();
- });
- });
-
- describe("return value", () =>
- {
- it("should return result attachment after compute blur", () =>
- {
- mockShouldUseComputeShader.mockReturnValue(true);
-
- const sourceAttachment = createMockAttachment(100, 100);
- const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
- const config = createMockConfig();
- const computePipelineManager = createMockComputePipelineManager();
-
- const result = execute(
- sourceAttachment,
- matrix,
- 16,
- 16,
- 1,
- 1,
- config,
- computePipelineManager
- );
-
- expect(result).toBeDefined();
- expect(result.texture).toBeDefined();
- });
- });
-});
diff --git a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts b/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts
deleted file mode 100644
index 9639a3a8..00000000
--- a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts
+++ /dev/null
@@ -1,292 +0,0 @@
-import type { IAttachmentObject } from "../../../interface/IAttachmentObject";
-import type { IFilterConfig } from "../../../interface/IFilterConfig";
-import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager";
-import { $offset } from "../../FilterOffset";
-import { calculateBlurParams } from "../../BlurFilterUseCase";
-import {
- execute as blurComputeService,
- shouldUseComputeShader
-} from "../service/BlurFilterComputeShaderService";
-import { execute as executeFragmentBlur } from "../FilterApplyBlurFilterUseCase";
-
-/**
- * @description プリアロケートされたFloat32Array (サイズ4)
- */
-const $uniform4 = new Float32Array(4);
-
-/**
- * @description プリアロケートされたBindGroupEntry配列 (バインディング3つ)
- */
-const $entries3: GPUBindGroupEntry[] = [
- { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } },
- { "binding": 1, "resource": null as unknown as GPUSampler },
- { "binding": 2, "resource": null as unknown as GPUTextureView }
-];
-
-/**
- * @description Compute Shaderを使用したブラーフィルター
- * Apply blur filter using Compute Shader
- *
- * Fragment Shaderベースの従来実装と比較して:
- * - 大きなブラー半径で20-35%高速化
- * - 並列処理による効率的なテクスチャサンプリング
- * - 共有メモリを活用したメモリアクセス最適化
- *
- * 小さなブラー半径(8未満)では従来のFragment Shaderを使用。
- *
- * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ
- * @param {Float32Array} matrix - 変換行列
- * @param {number} blurX - X方向のブラー量
- * @param {number} blurY - Y方向のブラー量
- * @param {number} quality - クオリティ (1-15)
- * @param {number} devicePixelRatio - デバイスピクセル比
- * @param {IFilterConfig} config - WebGPUリソース設定
- * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager
- * @return {IAttachmentObject} - フィルター適用後のアタッチメント
- */
-export const execute = (
- sourceAttachment: IAttachmentObject,
- matrix: Float32Array,
- blurX: number,
- blurY: number,
- quality: number,
- devicePixelRatio: number,
- config: IFilterConfig,
- computePipelineManager: ComputePipelineManager
-): IAttachmentObject => {
-
- const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config;
-
- // ブラーパラメータを計算
- const blurParams = calculateBlurParams(matrix, blurX, blurY, quality, devicePixelRatio);
- const { baseBlurX, baseBlurY, offsetX, offsetY, bufferScaleX, bufferScaleY } = blurParams;
-
- // オフセットを更新
- $offset.x += offsetX;
- $offset.y += offsetY;
-
- // ブラー用バッファサイズを計算
- const width = sourceAttachment.width + offsetX * 2;
- const height = sourceAttachment.height + offsetY * 2;
- const bufferWidth = Math.ceil(width * bufferScaleX);
- const bufferHeight = Math.ceil(height * bufferScaleY);
-
- // Compute Shaderを使用すべきか判定
- const useCompute = shouldUseComputeShader(baseBlurX, baseBlurY, bufferWidth, bufferHeight);
-
- if (!useCompute) {
- // 小さなブラーは従来のFragment Shaderを使用
- // FilterApplyBlurFilterUseCaseにフォールバック
- return executeFragmentBlur(
- sourceAttachment,
- matrix,
- blurX,
- blurY,
- quality,
- devicePixelRatio,
- config
- );
- }
-
- // ピンポンバッファ用の一時アタッチメントを作成
- const attachment0 = frameBufferManager.createTemporaryAttachment(bufferWidth, bufferHeight);
- const attachment1 = frameBufferManager.createTemporaryAttachment(bufferWidth, bufferHeight);
-
- // サンプラーを作成(線形補間)
- const sampler = textureManager.createSampler("blur_compute_sampler", true);
-
- // ソーステクスチャをattachment0にコピー
- copyTextureToAttachment(
- device, commandEncoder, frameBufferManager, pipelineManager,
- sourceAttachment, attachment0, sampler,
- bufferScaleX, bufferScaleY,
- offsetX * bufferScaleX, offsetY * bufferScaleY,
- config.bufferManager
- );
-
- // バッファスケールを考慮したブラー値
- const bufferBlurX = baseBlurX * bufferScaleX;
- const bufferBlurY = baseBlurY * bufferScaleY;
-
- // Compute Shaderでブラーパスを実行
- const attachments = [attachment0, attachment1];
- let attachmentIndex = 0;
-
- for (let q = 0; q < quality; ++q) {
- // 水平ブラー
- if (blurX > 0) {
- const srcIndex = attachmentIndex;
- attachmentIndex = (attachmentIndex + 1) % 2;
-
- blurComputeService(
- device, commandEncoder, computePipelineManager, config,
- attachments[srcIndex], attachments[attachmentIndex],
- true, bufferBlurX
- );
- }
-
- // 垂直ブラー
- if (blurY > 0) {
- const srcIndex = attachmentIndex;
- attachmentIndex = (attachmentIndex + 1) % 2;
-
- blurComputeService(
- device, commandEncoder, computePipelineManager, config,
- attachments[srcIndex], attachments[attachmentIndex],
- false, bufferBlurY
- );
- }
- }
-
- // 結果のアタッチメント
- let resultAttachment = attachments[attachmentIndex];
-
- // バッファスケールが1でない場合は元のサイズにアップスケール
- if (bufferScaleX !== 1 || bufferScaleY !== 1) {
- const finalAttachment = frameBufferManager.createTemporaryAttachment(width, height);
-
- upscaleTexture(
- device, commandEncoder, frameBufferManager, pipelineManager,
- resultAttachment, finalAttachment, sampler,
- config.bufferManager
- );
-
- // ピンポンバッファを解放
- frameBufferManager.releaseTemporaryAttachment(attachment0);
- frameBufferManager.releaseTemporaryAttachment(attachment1);
-
- resultAttachment = finalAttachment;
- } else {
- // 使わなかったバッファを解放
- const unusedIndex = (attachmentIndex + 1) % 2;
- frameBufferManager.releaseTemporaryAttachment(attachments[unusedIndex]);
- }
-
- return resultAttachment;
-};
-
-/**
- * @description テクスチャをアタッチメントにコピー
- */
-const copyTextureToAttachment = (
- device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- frameBufferManager: IFilterConfig["frameBufferManager"],
- pipelineManager: IFilterConfig["pipelineManager"],
- source: IAttachmentObject,
- dest: IAttachmentObject,
- sampler: GPUSampler,
- bufferScaleX: number,
- bufferScaleY: number,
- pixelOffsetX: number,
- pixelOffsetY: number,
- bufferManager?: IFilterConfig["bufferManager"]
-): void => {
- const pipeline = pipelineManager.getPipeline("texture_copy_rgba8");
- const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy");
-
- if (!pipeline || !bindGroupLayout) {
- console.error("[WebGPU BlurCompute] texture_copy_rgba8 pipeline not found");
- return;
- }
-
- const scaledSourceWidth = source.width * bufferScaleX;
- const scaledSourceHeight = source.height * bufferScaleY;
-
- $uniform4[0] = 1;
- $uniform4[1] = 1;
- $uniform4[2] = 0;
- $uniform4[3] = 0;
- const uniformBuffer = bufferManager
- ? bufferManager.acquireAndWriteUniformBuffer($uniform4)
- : device.createBuffer({
- "size": $uniform4.byteLength,
- "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
- });
- if (!bufferManager) {
- device.queue.writeBuffer(uniformBuffer, 0, $uniform4);
- }
-
- ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
- $entries3[1].resource = sampler;
- $entries3[2].resource = source.texture!.view;
- const bindGroup = device.createBindGroup({
- "layout": bindGroupLayout,
- "entries": $entries3
- });
-
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
- dest.texture!.view, 0, 0, 0, 0, "clear"
- );
-
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
- passEncoder.setPipeline(pipeline);
- passEncoder.setBindGroup(0, bindGroup);
-
- passEncoder.setViewport(
- pixelOffsetX, pixelOffsetY,
- scaledSourceWidth, scaledSourceHeight,
- 0, 1
- );
- passEncoder.setScissorRect(
- Math.floor(pixelOffsetX), Math.floor(pixelOffsetY),
- Math.ceil(scaledSourceWidth), Math.ceil(scaledSourceHeight)
- );
-
- passEncoder.draw(6, 1, 0, 0);
- passEncoder.end();
-};
-
-/**
- * @description テクスチャをアップスケール
- */
-const upscaleTexture = (
- device: GPUDevice,
- commandEncoder: GPUCommandEncoder,
- frameBufferManager: IFilterConfig["frameBufferManager"],
- pipelineManager: IFilterConfig["pipelineManager"],
- source: IAttachmentObject,
- dest: IAttachmentObject,
- sampler: GPUSampler,
- bufferManager?: IFilterConfig["bufferManager"]
-): void => {
- const pipeline = pipelineManager.getPipeline("texture_copy_rgba8");
- const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy");
-
- if (!pipeline || !bindGroupLayout) {
- console.error("[WebGPU BlurCompute] texture_copy_rgba8 pipeline not found");
- return;
- }
-
- $uniform4[0] = 1;
- $uniform4[1] = 1;
- $uniform4[2] = 0;
- $uniform4[3] = 0;
- const uniformBuffer = bufferManager
- ? bufferManager.acquireAndWriteUniformBuffer($uniform4)
- : device.createBuffer({
- "size": $uniform4.byteLength,
- "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
- });
- if (!bufferManager) {
- device.queue.writeBuffer(uniformBuffer, 0, $uniform4);
- }
-
- ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
- $entries3[1].resource = sampler;
- $entries3[2].resource = source.texture!.view;
- const bindGroup = device.createBindGroup({
- "layout": bindGroupLayout,
- "entries": $entries3
- });
-
- const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor(
- dest.texture!.view, 0, 0, 0, 0, "clear"
- );
-
- const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
- passEncoder.setPipeline(pipeline);
- passEncoder.setBindGroup(0, bindGroup);
- passEncoder.draw(6, 1, 0, 0);
- passEncoder.end();
-};
diff --git a/packages/webgpu/src/Filter/BlurFilterShader.test.ts b/packages/webgpu/src/Filter/BlurFilterShader.test.ts
deleted file mode 100644
index 09e1a83c..00000000
--- a/packages/webgpu/src/Filter/BlurFilterShader.test.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { describe, it, expect } from "vitest";
-import { BlurFilterShader } from "./BlurFilterShader";
-
-describe("BlurFilterShader", () =>
-{
- describe("getVertexShader", () =>
- {
- it("should return a valid WGSL vertex shader string", () =>
- {
- const shader = BlurFilterShader.getVertexShader();
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should contain @vertex attribute", () =>
- {
- const shader = BlurFilterShader.getVertexShader();
-
- expect(shader).toContain("@vertex");
- });
-
- it("should contain main function", () =>
- {
- const shader = BlurFilterShader.getVertexShader();
-
- expect(shader).toContain("fn main");
- });
-
- it("should define VertexInput struct", () =>
- {
- const shader = BlurFilterShader.getVertexShader();
-
- expect(shader).toContain("struct VertexInput");
- });
-
- it("should define VertexOutput struct", () =>
- {
- const shader = BlurFilterShader.getVertexShader();
-
- expect(shader).toContain("struct VertexOutput");
- });
-
- it("should include position and texCoord attributes", () =>
- {
- const shader = BlurFilterShader.getVertexShader();
-
- expect(shader).toContain("@location(0) position");
- expect(shader).toContain("@location(1) texCoord");
- });
- });
-
- describe("getHorizontalBlurShader", () =>
- {
- it("should return a valid WGSL fragment shader string", () =>
- {
- const shader = BlurFilterShader.getHorizontalBlurShader();
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should contain @fragment attribute", () =>
- {
- const shader = BlurFilterShader.getHorizontalBlurShader();
-
- expect(shader).toContain("@fragment");
- });
-
- it("should define BlurUniforms struct", () =>
- {
- const shader = BlurFilterShader.getHorizontalBlurShader();
-
- expect(shader).toContain("struct BlurUniforms");
- });
-
- it("should include blur parameters", () =>
- {
- const shader = BlurFilterShader.getHorizontalBlurShader();
-
- expect(shader).toContain("blurSize");
- expect(shader).toContain("textureWidth");
- });
-
- it("should use textureWidth for horizontal sampling", () =>
- {
- const shader = BlurFilterShader.getHorizontalBlurShader();
-
- expect(shader).toContain("1.0 / uniforms.textureWidth");
- });
-
- it("should include texture sampling", () =>
- {
- const shader = BlurFilterShader.getHorizontalBlurShader();
-
- expect(shader).toContain("textureSample");
- });
- });
-
- describe("getVerticalBlurShader", () =>
- {
- it("should return a valid WGSL fragment shader string", () =>
- {
- const shader = BlurFilterShader.getVerticalBlurShader();
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should contain @fragment attribute", () =>
- {
- const shader = BlurFilterShader.getVerticalBlurShader();
-
- expect(shader).toContain("@fragment");
- });
-
- it("should define BlurUniforms struct", () =>
- {
- const shader = BlurFilterShader.getVerticalBlurShader();
-
- expect(shader).toContain("struct BlurUniforms");
- });
-
- it("should use textureHeight for vertical sampling", () =>
- {
- const shader = BlurFilterShader.getVerticalBlurShader();
-
- expect(shader).toContain("1.0 / uniforms.textureHeight");
- });
-
- it("should include y-axis offset calculation", () =>
- {
- const shader = BlurFilterShader.getVerticalBlurShader();
-
- expect(shader).toContain("input.texCoord.y + offset");
- });
- });
-});
diff --git a/packages/webgpu/src/Filter/BlurFilterShader.ts b/packages/webgpu/src/Filter/BlurFilterShader.ts
deleted file mode 100644
index c61ba3aa..00000000
--- a/packages/webgpu/src/Filter/BlurFilterShader.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-export class BlurFilterShader
-{
- static getVertexShader(): string
- {
- return /* wgsl */`
- struct VertexInput {
- @location(0) position: vec2,
- @location(1) texCoord: vec2,
- }
-
- struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
- }
-
- @vertex
- fn main(input: VertexInput) -> VertexOutput {
- var output: VertexOutput;
- output.position = vec4(input.position, 0.0, 1.0);
- output.texCoord = input.texCoord;
- return output;
- }
- `;
- }
-
- static getHorizontalBlurShader(): string
- {
- return /* wgsl */`
- struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
- }
-
- struct BlurUniforms {
- blurSize: f32,
- textureWidth: f32,
- textureHeight: f32,
- _padding: f32,
- }
-
- @group(0) @binding(0) var uniforms: BlurUniforms;
- @group(0) @binding(1) var textureSampler: sampler;
- @group(0) @binding(2) var textureData: texture_2d;
-
- @fragment
- fn main(input: VertexOutput) -> @location(0) vec4 {
- let texelSize = 1.0 / uniforms.textureWidth;
- var color = vec4(0.0);
- let blurRadius = i32(uniforms.blurSize);
-
- var totalWeight = 0.0;
-
- for (var i = -blurRadius; i <= blurRadius; i++) {
- let offset = f32(i) * texelSize;
- let weight = 1.0 - abs(f32(i)) / f32(blurRadius + 1);
-
- let sampleCoord = vec2(
- input.texCoord.x + offset,
- input.texCoord.y
- );
-
- color += textureSample(textureData, textureSampler, sampleCoord) * weight;
- totalWeight += weight;
- }
-
- return color / totalWeight;
- }
- `;
- }
-
- static getVerticalBlurShader(): string
- {
- return /* wgsl */`
- struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
- }
-
- struct BlurUniforms {
- blurSize: f32,
- textureWidth: f32,
- textureHeight: f32,
- _padding: f32,
- }
-
- @group(0) @binding(0) var uniforms: BlurUniforms;
- @group(0) @binding(1) var textureSampler: sampler;
- @group(0) @binding(2) var textureData: texture_2d;
-
- @fragment
- fn main(input: VertexOutput) -> @location(0) vec4 {
- let texelSize = 1.0 / uniforms.textureHeight;
- var color = vec4(0.0);
- let blurRadius = i32(uniforms.blurSize);
-
- var totalWeight = 0.0;
-
- for (var i = -blurRadius; i <= blurRadius; i++) {
- let offset = f32(i) * texelSize;
- let weight = 1.0 - abs(f32(i)) / f32(blurRadius + 1);
-
- let sampleCoord = vec2(
- input.texCoord.x,
- input.texCoord.y + offset
- );
-
- color += textureSample(textureData, textureSampler, sampleCoord) * weight;
- totalWeight += weight;
- }
-
- return color / totalWeight;
- }
- `;
- }
-}
diff --git a/packages/webgpu/src/Filter/BlurFilterUseCase.ts b/packages/webgpu/src/Filter/BlurFilterUseCase.ts
index 2954d3d1..5735d330 100644
--- a/packages/webgpu/src/Filter/BlurFilterUseCase.ts
+++ b/packages/webgpu/src/Filter/BlurFilterUseCase.ts
@@ -6,24 +6,27 @@
/**
* @description ブラー計算用のステップ値
* Step values for blur calculation
+ * @type {number[]}
*/
-const BLUR_STEP: number[] = [0.5, 1.05, 1.4, 1.55, 1.75, 1.9, 2, 2.15, 2.2, 2.3, 2.5, 3, 3, 3.5, 3.5];
+const $BLUR_STEP: number[] = [0.5, 1.05, 1.4, 1.55, 1.75, 1.9, 2, 2.15, 2.2, 2.3, 2.5, 3, 3, 3.5, 3.5];
/**
* @description ブラーフィルターパラメータを計算
- * @param {Float32Array} matrix - 変換行列
- * @param {number} blurX - X方向のブラー量
- * @param {number} blurY - Y方向のブラー量
- * @param {number} quality - クオリティ (1-15)
- * @param {number} devicePixelRatio - デバイスピクセル比
+ * Calculate blur filter parameters
+ *
+ * @param {Float32Array} matrix - 変換行列
+ * @param {number} blur_x - X方向のブラー量
+ * @param {number} blur_y - Y方向のブラー量
+ * @param {number} quality - クオリティ (1-15)
+ * @param {number} device_pixel_ratio - デバイスピクセル比
* @return {object}
*/
export const calculateBlurParams = (
matrix: Float32Array,
- blurX: number,
- blurY: number,
+ blur_x: number,
+ blur_y: number,
quality: number,
- devicePixelRatio: number
+ device_pixel_ratio: number
): {
baseBlurX: number;
baseBlurY: number;
@@ -35,10 +38,10 @@ export const calculateBlurParams = (
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 baseBlurX = blurX * (xScale / devicePixelRatio);
- const baseBlurY = blurY * (yScale / devicePixelRatio);
+ const baseBlurX = blur_x * (xScale / device_pixel_ratio);
+ const baseBlurY = blur_y * (yScale / device_pixel_ratio);
- const step = BLUR_STEP[Math.min(quality - 1, BLUR_STEP.length - 1)];
+ const step = $BLUR_STEP[Math.min(quality - 1, $BLUR_STEP.length - 1)];
const offsetX = Math.round(baseBlurX * step);
const offsetY = Math.round(baseBlurY * step);
@@ -78,17 +81,19 @@ export const calculateBlurParams = (
/**
* @description 方向ブラーのパラメータを計算
- * @param {boolean} isHorizontal - 水平方向かどうか
- * @param {number} blur - ブラー量
- * @param {number} textureWidth - テクスチャ幅
- * @param {number} textureHeight - テクスチャ高さ
+ * Calculate directional blur parameters
+ *
+ * @param {boolean} is_horizontal - 水平方向かどうか
+ * @param {number} blur - ブラー量
+ * @param {number} texture_width - テクスチャ幅
+ * @param {number} texture_height - テクスチャ高さ
* @return {object}
*/
export const calculateDirectionalBlurParams = (
- isHorizontal: boolean,
+ is_horizontal: boolean,
blur: number,
- textureWidth: number,
- textureHeight: number
+ texture_width: number,
+ texture_height: number
): {
offsetX: number;
offsetY: number;
@@ -101,8 +106,8 @@ export const calculateDirectionalBlurParams = (
const samples = 1 + blur;
// テクセルオフセットを計算
- const offsetX = isHorizontal ? 1 / textureWidth : 0;
- const offsetY = isHorizontal ? 0 : 1 / textureHeight;
+ const offsetX = is_horizontal ? 1 / texture_width : 0;
+ const offsetY = is_horizontal ? 0 : 1 / texture_height;
return {
offsetX,
diff --git a/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts b/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts
index b9ef9282..3ae6c859 100644
--- a/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts
+++ b/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts
@@ -19,13 +19,13 @@ const $entries3: GPUBindGroupEntry[] = [
* @description カラーマトリックスフィルターを適用
* Apply color matrix filter
*
- * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ(アタッチメント)
+ * @param {IAttachmentObject} source_attachment - 入力テクスチャ(アタッチメント)
* @param {Float32Array} matrix - 4x5カラーマトリックス (20 floats)
* @param {IFilterConfig} config - WebGPUリソース設定
* @return {IAttachmentObject} - フィルター適用後のアタッチメント
*/
export const execute = (
- sourceAttachment: IAttachmentObject,
+ source_attachment: IAttachmentObject,
matrix: Float32Array,
config: IFilterConfig
): IAttachmentObject => {
@@ -34,8 +34,8 @@ export const execute = (
// 出力アタッチメントを作成
const destAttachment = frameBufferManager.createTemporaryAttachment(
- sourceAttachment.width,
- sourceAttachment.height
+ source_attachment.width,
+ source_attachment.height
);
const pipeline = pipelineManager.getPipeline("color_matrix_filter");
@@ -43,7 +43,7 @@ export const execute = (
if (!pipeline || !bindGroupLayout) {
console.error("[WebGPU ColorMatrixFilter] Pipeline not found");
- return sourceAttachment;
+ return source_attachment;
}
// サンプラーを作成
@@ -93,7 +93,7 @@ export const execute = (
// バインドグループを作成
($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries3[1].resource = sampler;
- $entries3[2].resource = sourceAttachment.texture!.view;
+ $entries3[2].resource = source_attachment.texture!.view;
const bindGroup = device.createBindGroup({
"layout": bindGroupLayout,
"entries": $entries3
diff --git a/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts b/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts
deleted file mode 100644
index 672e57af..00000000
--- a/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { describe, it, expect } from "vitest";
-import { ColorMatrixFilterShader } from "./ColorMatrixFilterShader";
-
-describe("ColorMatrixFilterShader", () =>
-{
- describe("getVertexShader", () =>
- {
- it("should return a valid WGSL vertex shader string", () =>
- {
- const shader = ColorMatrixFilterShader.getVertexShader();
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should contain @vertex attribute", () =>
- {
- const shader = ColorMatrixFilterShader.getVertexShader();
-
- expect(shader).toContain("@vertex");
- });
-
- it("should define VertexInput struct", () =>
- {
- const shader = ColorMatrixFilterShader.getVertexShader();
-
- expect(shader).toContain("struct VertexInput");
- });
-
- it("should define VertexOutput struct", () =>
- {
- const shader = ColorMatrixFilterShader.getVertexShader();
-
- expect(shader).toContain("struct VertexOutput");
- });
- });
-
- describe("getFragmentShader", () =>
- {
- it("should return a valid WGSL fragment shader string", () =>
- {
- const shader = ColorMatrixFilterShader.getFragmentShader();
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should contain @fragment attribute", () =>
- {
- const shader = ColorMatrixFilterShader.getFragmentShader();
-
- expect(shader).toContain("@fragment");
- });
-
- it("should define ColorMatrixUniforms struct", () =>
- {
- const shader = ColorMatrixFilterShader.getFragmentShader();
-
- expect(shader).toContain("struct ColorMatrixUniforms");
- });
-
- it("should include matrix uniform", () =>
- {
- const shader = ColorMatrixFilterShader.getFragmentShader();
-
- expect(shader).toContain("matrix: mat4x4");
- });
-
- it("should include offset uniform", () =>
- {
- const shader = ColorMatrixFilterShader.getFragmentShader();
-
- expect(shader).toContain("offset: vec4");
- });
-
- it("should apply matrix transformation", () =>
- {
- const shader = ColorMatrixFilterShader.getFragmentShader();
-
- expect(shader).toContain("uniforms.matrix * color");
- });
-
- it("should apply offset", () =>
- {
- const shader = ColorMatrixFilterShader.getFragmentShader();
-
- expect(shader).toContain("+ uniforms.offset");
- });
-
- it("should clamp result to 0-1 range", () =>
- {
- const shader = ColorMatrixFilterShader.getFragmentShader();
-
- expect(shader).toContain("clamp");
- });
- });
-});
diff --git a/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts b/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts
deleted file mode 100644
index ed0108c2..00000000
--- a/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-export class ColorMatrixFilterShader
-{
- static getFragmentShader(): string
- {
- return /* wgsl */`
- struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
- }
-
- struct ColorMatrixUniforms {
- matrix: mat4x4,
- offset: vec4,
- }
-
- @group(0) @binding(0) var uniforms: ColorMatrixUniforms;
- @group(0) @binding(1) var textureSampler: sampler;
- @group(0) @binding(2) var textureData: texture_2d;
-
- @fragment
- fn main(input: VertexOutput) -> @location(0) vec4 {
- var color = textureSample(textureData, textureSampler, input.texCoord);
-
- var result = uniforms.matrix * color + uniforms.offset;
-
- result = clamp(result, vec4(0.0), vec4(1.0));
-
- return result;
- }
- `;
- }
-
- static getVertexShader(): string
- {
- return /* wgsl */`
- struct VertexInput {
- @location(0) position: vec2,
- @location(1) texCoord: vec2,
- }
-
- struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
- }
-
- @vertex
- fn main(input: VertexInput) -> VertexOutput {
- var output: VertexOutput;
- output.position = vec4(input.position, 0.0, 1.0);
- output.texCoord = input.texCoord;
- return output;
- }
- `;
- }
-}
diff --git a/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts b/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts
index 57bee8ff..f5d8bb0e 100644
--- a/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts
+++ b/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts
@@ -1,6 +1,7 @@
import type { IAttachmentObject } from "../../interface/IAttachmentObject";
import type { IFilterConfig } from "../../interface/IFilterConfig";
import { ShaderSource } from "../../Shader/ShaderSource";
+import { intToStraightRGBA } from "../FilterUtil";
/**
* @description プリアロケートされたBindGroupEntry配列 (バインディング3つ)
@@ -11,16 +12,6 @@ const $entries3: GPUBindGroupEntry[] = [
{ "binding": 2, "resource": null as unknown as GPUTextureView }
];
-/**
- * @description 32bit整数からRGB値を抽出
- */
-const intToRGBA = (color: number, alpha: number): [number, number, number, number] => {
- const r = (color >> 16 & 0xFF) / 255;
- const g = (color >> 8 & 0xFF) / 255;
- const b = (color & 0xFF) / 255;
- return [r, g, b, alpha];
-};
-
/**
* @description パイプラインキャッシュ(キー: matrixX,matrixY,preserveAlpha,clamp)
*/
@@ -31,15 +22,29 @@ const $pipelineCache = new Map
-{
- describe("getConvolutionFilterFragmentShader", () =>
- {
- it("should return a valid WGSL shader string", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should contain @vertex attribute", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("@vertex");
- });
-
- it("should contain @fragment attribute", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("@fragment");
- });
-
- it("should define ConvolutionUniforms struct", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("struct ConvolutionUniforms");
- });
-
- it("should include rcpSize uniform", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("rcpSize: vec2");
- });
-
- it("should include rcpDivisor uniform", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("rcpDivisor: f32");
- });
-
- it("should include bias uniform", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("bias: f32");
- });
-
- it("should include substituteColor uniform", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("substituteColor: vec4");
- });
-
- it("should include matrix array", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("matrix: array");
- });
-
- it("should generate correct matrix size for 3x3", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
- // 3x3 = 9 elements, ceil(9/4) = 3
- expect(shader).toContain("array, 3>");
- });
-
- it("should generate correct matrix size for 5x5", () =>
- {
- const shader = getConvolutionFilterFragmentShader(5, 5, true, true);
- // 5x5 = 25 elements, ceil(25/4) = 7
- expect(shader).toContain("array, 7>");
- });
-
- it("should include isInside helper function", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("fn isInside");
- });
-
- it("should include getMatrixWeight helper function", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("fn getMatrixWeight");
- });
-
- it("should include getWeightedColor helper function", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("fn getWeightedColor");
- });
-
- it("should preserve alpha when preserveAlpha is true", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a");
- });
-
- it("should not preserve alpha when preserveAlpha is false", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, false, true);
-
- expect(shader).not.toContain("result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a");
- });
-
- it("should include substituteColor handling when clamp is false", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, false);
-
- expect(shader).toContain("substituteColor");
- expect(shader).toContain("mix(substituteColor, color, isInside(uv))");
- });
-
- it("should not include substituteColor handling when clamp is true", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
- // Should still have substituteColor in uniforms but not the mix statement
- expect(shader).not.toContain("mix(substituteColor, color, isInside(uv))");
- });
-
- it("should clamp result values", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("clamp(result * rcpDivisor + bias");
- });
-
- it("should premultiply result", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("result.rgb * result.a");
- });
-
- it("should unpremultiply color for processing", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- expect(shader).toContain("color.rgb / max(0.0001, color.a)");
- });
-
- it("should generate 9 getWeightedColor calls for 3x3 matrix", () =>
- {
- const shader = getConvolutionFilterFragmentShader(3, 3, true, true);
-
- let count = 0;
- for (let i = 0; i < 9; i++) {
- if (shader.includes(`getWeightedColor(${i}`)) {
- count++;
- }
- }
-
- expect(count).toBe(9);
- });
-
- it("should handle asymmetric matrix sizes", () =>
- {
- const shader = getConvolutionFilterFragmentShader(5, 3, true, true);
- // 5x3 = 15 elements, ceil(15/4) = 4
- expect(shader).toContain("array, 4>");
- });
- });
-
- describe("getConvolutionFilterShaderKey", () =>
- {
- it("should generate unique key for 3x3 with preserveAlpha and clamp", () =>
- {
- const key = getConvolutionFilterShaderKey(3, 3, true, true);
-
- expect(key).toBe("convolution_3x3_pa_c");
- });
-
- it("should generate unique key for 5x5 without preserveAlpha and without clamp", () =>
- {
- const key = getConvolutionFilterShaderKey(5, 5, false, false);
-
- expect(key).toBe("convolution_5x5_npa_nc");
- });
-
- it("should include matrix dimensions in key", () =>
- {
- const key = getConvolutionFilterShaderKey(7, 3, true, true);
-
- expect(key).toContain("7x3");
- });
-
- it("should include preserveAlpha flag in key", () =>
- {
- const keyWithPA = getConvolutionFilterShaderKey(3, 3, true, true);
- const keyWithoutPA = getConvolutionFilterShaderKey(3, 3, false, true);
-
- expect(keyWithPA).toContain("_pa_");
- expect(keyWithoutPA).toContain("_npa_");
- });
-
- it("should include clamp flag in key", () =>
- {
- const keyWithClamp = getConvolutionFilterShaderKey(3, 3, true, true);
- const keyWithoutClamp = getConvolutionFilterShaderKey(3, 3, true, false);
-
- expect(keyWithClamp).toContain("_c");
- expect(keyWithoutClamp).toContain("_nc");
- });
-
- it("should generate different keys for different configurations", () =>
- {
- const key1 = getConvolutionFilterShaderKey(3, 3, true, true);
- const key2 = getConvolutionFilterShaderKey(3, 3, false, true);
- const key3 = getConvolutionFilterShaderKey(3, 3, true, false);
- const key4 = getConvolutionFilterShaderKey(5, 5, true, true);
-
- expect(key1).not.toBe(key2);
- expect(key1).not.toBe(key3);
- expect(key1).not.toBe(key4);
- });
- });
-});
diff --git a/packages/webgpu/src/Filter/ConvolutionFilterShader.ts b/packages/webgpu/src/Filter/ConvolutionFilterShader.ts
deleted file mode 100644
index e112bb40..00000000
--- a/packages/webgpu/src/Filter/ConvolutionFilterShader.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-export const getConvolutionFilterFragmentShader = (
- matrixX: number,
- matrixY: number,
- preserveAlpha: boolean,
- clamp: boolean
-): string => {
- const halfX = Math.floor(matrixX * 0.5);
- const halfY = Math.floor(matrixY * 0.5);
- const size = matrixX * matrixY;
-
- let matrixStatement = "";
- for (let idx = 0; idx < size; idx++) {
- matrixStatement += `
- result = result + getWeightedColor(${idx}, getMatrixWeight(${idx}));`;
- }
-
- const preserveAlphaStatement = preserveAlpha
- ? "result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a;"
- : "";
-
- const clampStatement = clamp
- ? ""
- : `
- let substituteColor = uniforms.substituteColor;
- color = mix(substituteColor, color, isInside(uv));`;
-
- return `
-struct ConvolutionUniforms {
- rcpSize: vec2,
- rcpDivisor: f32,
- bias: f32,
- substituteColor: vec4,
- matrix: array, ${Math.ceil(size / 4)}>,
-}
-
-@group(0) @binding(0) var uniforms: ConvolutionUniforms;
-@group(0) @binding(1) var sourceSampler: sampler;
-@group(0) @binding(2) var sourceTexture: texture_2d;
-
-struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
-}
-
-fn isInside(uv: vec2) -> f32 {
- let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0));
- return inside.x * inside.y;
-}
-
-fn getMatrixWeight(index: i32) -> f32 {
- let vecIndex = index / 4;
- let component = index % 4;
- let vec = uniforms.matrix[vecIndex];
-
- if (component == 0) { return vec.x; }
- else if (component == 1) { return vec.y; }
- else if (component == 2) { return vec.z; }
- else { return vec.w; }
-}
-
-fn getWeightedColor(i: i32, weight: f32) -> vec4 {
- let rcpSize = uniforms.rcpSize;
-
- let iDivX = i / ${matrixX};
- let iModX = i - ${matrixX} * iDivX;
- let offset = vec2(f32(iModX - ${halfX}), f32(${halfY} - iDivX));
- var uv = input.texCoord + offset * rcpSize;
-
- var color = textureSample(sourceTexture, sourceSampler, uv);
- color = vec4(color.rgb / max(0.0001, color.a), color.a);
- ${clampStatement}
-
- return color * weight;
-}
-
-var input: VertexOutput;
-
-@vertex
-fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
- var positions = array, 6>(
- vec2(-1.0, -1.0),
- vec2(1.0, -1.0),
- vec2(-1.0, 1.0),
- vec2(-1.0, 1.0),
- vec2(1.0, -1.0),
- vec2(1.0, 1.0)
- );
-
- var texCoords = array, 6>(
- vec2(0.0, 1.0),
- vec2(1.0, 1.0),
- vec2(0.0, 0.0),
- vec2(0.0, 0.0),
- vec2(1.0, 1.0),
- vec2(1.0, 0.0)
- );
-
- var output: VertexOutput;
- output.position = vec4(positions[vertexIndex], 0.0, 1.0);
- output.texCoord = texCoords[vertexIndex];
- return output;
-}
-
-@fragment
-fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 {
- input = fragInput;
-
- let rcpDivisor = uniforms.rcpDivisor;
- let bias = uniforms.bias;
-
- var result = vec4(0.0);
- ${matrixStatement}
-
- result = clamp(result * rcpDivisor + bias, vec4(0.0), vec4(1.0));
- ${preserveAlphaStatement}
-
- result = vec4(result.rgb * result.a, result.a);
- return result;
-}
-`;
-};
-
-export const getConvolutionFilterShaderKey = (
- matrixX: number,
- matrixY: number,
- preserveAlpha: boolean,
- clamp: boolean
-): string => {
- return `convolution_${matrixX}x${matrixY}_${preserveAlpha ? "pa" : "npa"}_${clamp ? "c" : "nc"}`;
-};
diff --git a/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts b/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts
index 2f44d640..2b89d782 100644
--- a/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts
+++ b/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts
@@ -1,6 +1,7 @@
import type { IAttachmentObject } from "../../interface/IAttachmentObject";
import type { IFilterConfig } from "../../interface/IFilterConfig";
import { ShaderSource } from "../../Shader/ShaderSource";
+import { intToPremultipliedRGBA } from "../FilterUtil";
/**
* @description プリアロケートされたFloat32Array (サイズ12: 最大48バイト)
@@ -17,16 +18,6 @@ const $entries4: GPUBindGroupEntry[] = [
{ "binding": 3, "resource": null as unknown as GPUTextureView }
];
-/**
- * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ)
- */
-const intToRGBA = (color: number, alpha: number): [number, number, number, number] => {
- const r = (color >> 16 & 0xFF) / 255 * alpha;
- const g = (color >> 8 & 0xFF) / 255 * alpha;
- const b = (color & 0xFF) / 255 * alpha;
- return [r, g, b, alpha];
-};
-
/**
* @description パイプラインキャッシュ(キー: componentX,componentY,mode)
*/
@@ -37,57 +28,76 @@ const $pipelineCache = new Map {
const { device, commandEncoder, frameBufferManager, textureManager } = config;
- const width = sourceAttachment.width;
- const height = sourceAttachment.height;
+ const width = source_attachment.width;
+ const height = source_attachment.height;
// WebGL版と同じ: baseWidth/baseHeightはビットマップサイズを使用
- const baseWidth = bitmapWidth;
- const baseHeight = bitmapHeight;
+ const baseWidth = bitmap_width;
+ const baseHeight = bitmap_height;
// 出力アタッチメントを作成
const destAttachment = frameBufferManager.createTemporaryAttachment(width, height);
// マップテクスチャを作成
const mapTexture = device.createTexture({
- "size": { "width": bitmapWidth, "height": bitmapHeight },
+ "size": { "width": bitmap_width, "height": bitmap_height },
"format": "rgba8unorm",
"usage": GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
});
device.queue.writeTexture(
{ "texture": mapTexture },
- bitmapBuffer.buffer,
- { "bytesPerRow": bitmapWidth * 4, "offset": bitmapBuffer.byteOffset },
- { "width": bitmapWidth, "height": bitmapHeight }
+ bitmap_buffer.buffer,
+ { "bytesPerRow": bitmap_width * 4, "offset": bitmap_buffer.byteOffset },
+ { "width": bitmap_width, "height": bitmap_height }
);
// パイプラインをキャッシュから取得または作成
- const cacheKey = `${componentX},${componentY},${mode}`;
+ const cacheKey = `${component_x},${component_y},${mode}`;
let cached = $pipelineCache.get(cacheKey);
if (!cached) {
const fragmentShaderCode = ShaderSource.getDisplacementMapFilterFragmentShader(
- componentX, componentY, mode
+ component_x, component_y, mode
);
const vertexShaderModule = device.createShaderModule({
@@ -171,16 +181,16 @@ export const execute = (
const uniformSize = needsSubstituteColor ? 48 : 32;
// uvToStScale
- $uniform12[0] = baseWidth / bitmapWidth;
- $uniform12[1] = baseHeight / bitmapHeight;
+ $uniform12[0] = baseWidth / bitmap_width;
+ $uniform12[1] = baseHeight / bitmap_height;
// uvToStOffset
- $uniform12[2] = mapPointX / bitmapWidth;
- $uniform12[3] = (baseHeight - bitmapHeight - mapPointY) / bitmapHeight;
+ $uniform12[2] = map_point_x / bitmap_width;
+ $uniform12[3] = (baseHeight - bitmap_height - map_point_y) / bitmap_height;
// scale
- $uniform12[4] = scaleX / baseWidth;
- $uniform12[5] = scaleY / baseHeight;
+ $uniform12[4] = scale_x / baseWidth;
+ $uniform12[5] = scale_y / baseHeight;
// padding
$uniform12[6] = 0;
@@ -188,7 +198,7 @@ export const execute = (
// substituteColor (mode === 1 の場合)
if (needsSubstituteColor) {
- const [r, g, b, a] = intToRGBA(color, alpha);
+ const [r, g, b, a] = intToPremultipliedRGBA(color, alpha);
$uniform12[8] = r;
$uniform12[9] = g;
$uniform12[10] = b;
@@ -208,7 +218,7 @@ export const execute = (
// バインドグループを作成
($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer;
$entries4[1].resource = sampler;
- $entries4[2].resource = sourceAttachment.texture!.view;
+ $entries4[2].resource = source_attachment.texture!.view;
$entries4[3].resource = mapTexture.createView();
const bindGroup = device.createBindGroup({
"layout": cached.bindGroupLayout,
diff --git a/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts b/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts
deleted file mode 100644
index 0400ad10..00000000
--- a/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-import { describe, it, expect } from "vitest";
-import { getDisplacementMapFilterFragmentShader, getDisplacementMapFilterShaderKey } from "./DisplacementMapFilterShader";
-
-describe("DisplacementMapFilterShader", () =>
-{
- describe("getDisplacementMapFilterFragmentShader", () =>
- {
- it("should return a valid WGSL shader string", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(typeof shader).toBe("string");
- expect(shader.length).toBeGreaterThan(0);
- });
-
- it("should contain @vertex attribute", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("@vertex");
- });
-
- it("should contain @fragment attribute", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("@fragment");
- });
-
- it("should define DisplacementMapUniforms struct", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("struct DisplacementMapUniforms");
- });
-
- it("should include uvToStScale uniform", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("uvToStScale: vec2");
- });
-
- it("should include uvToStOffset uniform", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("uvToStOffset: vec2");
- });
-
- it("should include scale uniform", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("scale: vec2");
- });
-
- it("should include mapTexture binding", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("var mapTexture: texture_2d");
- });
-
- it("should include sourceTexture binding", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("var sourceTexture: texture_2d");
- });
-
- it("should include isInside helper function", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("fn isInside");
- });
-
- // Component channel tests
- it("should use mapColor.r for componentX = 1 (RED)", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("mapColor.r");
- });
-
- it("should use mapColor.g for componentX = 2 (GREEN)", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(2, 1, 0);
-
- expect(shader).toContain("vec2(mapColor.g, mapColor.r)");
- });
-
- it("should use mapColor.b for componentX = 4 (BLUE)", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(4, 1, 0);
-
- expect(shader).toContain("vec2(mapColor.b, mapColor.r)");
- });
-
- it("should use mapColor.a for componentX = 8 (ALPHA)", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(8, 1, 0);
-
- expect(shader).toContain("vec2(mapColor.a, mapColor.r)");
- });
-
- it("should use 0.5 for unknown component value", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(99, 99, 0);
-
- expect(shader).toContain("vec2(0.5, 0.5)");
- });
-
- // Mode tests
- it("should handle mode 0 (direct sampling)", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("let sourceColor = textureSample(sourceTexture, sourceSampler, uv)");
- expect(shader).not.toContain("substituteColor");
- });
-
- it("should include substituteColor for mode 1", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 1);
-
- expect(shader).toContain("substituteColor: vec4");
- expect(shader).toContain("mix(substituteColor");
- });
-
- it("should handle mode 2 (wrap/repeat)", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 2);
-
- expect(shader).toContain("fract(uv)");
- });
-
- it("should handle mode 3 (axis fallback)", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 3);
-
- expect(shader).toContain("fallbackUv");
- });
-
- it("should calculate offset from map color", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("let offset = vec2");
- expect(shader).toContain("- 0.5");
- });
-
- it("should calculate displaced UV", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("let uv = input.texCoord + offset * scale");
- });
-
- it("should mix original and displaced color based on map bounds", () =>
- {
- const shader = getDisplacementMapFilterFragmentShader(1, 2, 0);
-
- expect(shader).toContain("mix(originalColor, sourceColor, isInside(st))");
- });
- });
-
- describe("getDisplacementMapFilterShaderKey", () =>
- {
- it("should generate unique key for component combination", () =>
- {
- const key = getDisplacementMapFilterShaderKey(1, 2, 0);
-
- expect(key).toBe("displacement_1_2_0");
- });
-
- it("should include all component and mode values", () =>
- {
- const key = getDisplacementMapFilterShaderKey(4, 8, 2);
-
- expect(key).toBe("displacement_4_8_2");
- });
-
- it("should generate different keys for different configurations", () =>
- {
- const key1 = getDisplacementMapFilterShaderKey(1, 2, 0);
- const key2 = getDisplacementMapFilterShaderKey(2, 1, 0);
- const key3 = getDisplacementMapFilterShaderKey(1, 2, 1);
- const key4 = getDisplacementMapFilterShaderKey(4, 8, 3);
-
- expect(key1).not.toBe(key2);
- expect(key1).not.toBe(key3);
- expect(key1).not.toBe(key4);
- });
-
- it("should include componentX in key", () =>
- {
- const key = getDisplacementMapFilterShaderKey(4, 2, 0);
-
- expect(key).toContain("_4_");
- });
-
- it("should include componentY in key", () =>
- {
- const key = getDisplacementMapFilterShaderKey(1, 8, 0);
-
- expect(key).toContain("_8_");
- });
-
- it("should include mode in key", () =>
- {
- const key = getDisplacementMapFilterShaderKey(1, 2, 3);
-
- expect(key).toContain("_3");
- });
- });
-});
diff --git a/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts b/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts
deleted file mode 100644
index 3a3f51d3..00000000
--- a/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-const getComponentExpression = (component: number): string => {
- switch (component) {
- case 1:
- return "mapColor.r";
- case 2:
- return "mapColor.g";
- case 4:
- return "mapColor.b";
- case 8:
- return "mapColor.a";
- default:
- return "0.5";
- }
-};
-
-const getModeStatement = (mode: number): string => {
- switch (mode) {
- case 0:
- return `
- let sourceColor = textureSample(sourceTexture, sourceSampler, uv);`;
-
- case 1:
- return `
- let substituteColor = uniforms.substituteColor;
- let sourceColor = mix(substituteColor, textureSample(sourceTexture, sourceSampler, uv), isInside(uv));`;
-
- case 3:
- return `
- let fallbackUv = mix(input.texCoord, uv, step(abs(uv - vec2(0.5)), vec2(0.5)));
- let sourceColor = textureSample(sourceTexture, sourceSampler, fallbackUv);`;
-
- case 2:
- default:
- return `
- let sourceColor = textureSample(sourceTexture, sourceSampler, fract(uv));`;
- }
-};
-
-export const getDisplacementMapFilterFragmentShader = (
- componentX: number,
- componentY: number,
- mode: number
-): string => {
- const cx = getComponentExpression(componentX);
- const cy = getComponentExpression(componentY);
- const modeStatement = getModeStatement(mode);
-
- const hasSubstituteColor = mode === 1;
-
- return `
-struct DisplacementMapUniforms {
- uvToStScale: vec2,
- uvToStOffset: vec2,
- scale: vec2,
- _pad: vec2,
-${hasSubstituteColor ? " substituteColor: vec4," : ""}
-}
-
-@group(0) @binding(0) var uniforms: DisplacementMapUniforms;
-@group(0) @binding(1) var sourceSampler: sampler;
-@group(0) @binding(2) var sourceTexture: texture_2d;
-@group(0) @binding(3) var mapTexture: texture_2d;
-
-struct VertexOutput {
- @builtin(position) position: vec4,
- @location(0) texCoord: vec2,
-}
-
-fn isInside(uv: vec2) -> f32 {
- let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0));
- return inside.x * inside.y;
-}
-
-var input: VertexOutput;
-
-@vertex
-fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
- var positions = array