@flighthq/render-wgpu 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/index.d.ts +17 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +16 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/wgpuAdapterCapabilities.d.ts +3 -0
  6. package/dist/wgpuAdapterCapabilities.d.ts.map +1 -0
  7. package/dist/wgpuAdapterCapabilities.js +28 -0
  8. package/dist/wgpuAdapterCapabilities.js.map +1 -0
  9. package/dist/wgpuBackground.d.ts +4 -0
  10. package/dist/wgpuBackground.d.ts.map +1 -0
  11. package/dist/wgpuBackground.js +110 -0
  12. package/dist/wgpuBackground.js.map +1 -0
  13. package/dist/wgpuDraw.d.ts +21 -0
  14. package/dist/wgpuDraw.d.ts.map +1 -0
  15. package/dist/wgpuDraw.js +153 -0
  16. package/dist/wgpuDraw.js.map +1 -0
  17. package/dist/wgpuElement.d.ts +2 -0
  18. package/dist/wgpuElement.d.ts.map +1 -0
  19. package/dist/wgpuElement.js +9 -0
  20. package/dist/wgpuElement.js.map +1 -0
  21. package/dist/wgpuFullscreenPass.d.ts +5 -0
  22. package/dist/wgpuFullscreenPass.d.ts.map +1 -0
  23. package/dist/wgpuFullscreenPass.js +106 -0
  24. package/dist/wgpuFullscreenPass.js.map +1 -0
  25. package/dist/wgpuMaterialRegistry.d.ts +5 -0
  26. package/dist/wgpuMaterialRegistry.d.ts.map +1 -0
  27. package/dist/wgpuMaterialRegistry.js +27 -0
  28. package/dist/wgpuMaterialRegistry.js.map +1 -0
  29. package/dist/wgpuRenderState.d.ts +7 -0
  30. package/dist/wgpuRenderState.d.ts.map +1 -0
  31. package/dist/wgpuRenderState.js +153 -0
  32. package/dist/wgpuRenderState.js.map +1 -0
  33. package/dist/wgpuRenderTarget.d.ts +19 -0
  34. package/dist/wgpuRenderTarget.d.ts.map +1 -0
  35. package/dist/wgpuRenderTarget.js +142 -0
  36. package/dist/wgpuRenderTarget.js.map +1 -0
  37. package/dist/wgpuRenderTargetPool.d.ts +10 -0
  38. package/dist/wgpuRenderTargetPool.d.ts.map +1 -0
  39. package/dist/wgpuRenderTargetPool.js +32 -0
  40. package/dist/wgpuRenderTargetPool.js.map +1 -0
  41. package/dist/wgpuScissor.d.ts +5 -0
  42. package/dist/wgpuScissor.d.ts.map +1 -0
  43. package/dist/wgpuScissor.js +37 -0
  44. package/dist/wgpuScissor.js.map +1 -0
  45. package/dist/wgpuShader.d.ts +43 -0
  46. package/dist/wgpuShader.d.ts.map +1 -0
  47. package/dist/wgpuShader.js +304 -0
  48. package/dist/wgpuShader.js.map +1 -0
  49. package/dist/wgpuShaderBinding.d.ts +5 -0
  50. package/dist/wgpuShaderBinding.d.ts.map +1 -0
  51. package/dist/wgpuShaderBinding.js +28 -0
  52. package/dist/wgpuShaderBinding.js.map +1 -0
  53. package/dist/wgpuShaderRegistry.d.ts +3 -0
  54. package/dist/wgpuShaderRegistry.d.ts.map +1 -0
  55. package/dist/wgpuShaderRegistry.js +8 -0
  56. package/dist/wgpuShaderRegistry.js.map +1 -0
  57. package/dist/wgpuSurface.d.ts +6 -0
  58. package/dist/wgpuSurface.d.ts.map +1 -0
  59. package/dist/wgpuSurface.js +107 -0
  60. package/dist/wgpuSurface.js.map +1 -0
  61. package/dist/wgpuTestHelper.d.ts +4 -0
  62. package/dist/wgpuTestHelper.d.ts.map +1 -0
  63. package/dist/wgpuTestHelper.js +137 -0
  64. package/dist/wgpuTestHelper.js.map +1 -0
  65. package/package.json +42 -0
  66. package/src/wgpuAdapterCapabilities.test.ts +49 -0
  67. package/src/wgpuBackground.test.ts +52 -0
  68. package/src/wgpuDraw.test.ts +174 -0
  69. package/src/wgpuElement.test.ts +21 -0
  70. package/src/wgpuFullscreenPass.test.ts +64 -0
  71. package/src/wgpuMaterialRegistry.test.ts +56 -0
  72. package/src/wgpuRenderState.test.ts +88 -0
  73. package/src/wgpuRenderTarget.test.ts +120 -0
  74. package/src/wgpuRenderTargetPool.test.ts +58 -0
  75. package/src/wgpuScissor.test.ts +97 -0
  76. package/src/wgpuShader.test.ts +140 -0
  77. package/src/wgpuShaderBinding.test.ts +62 -0
  78. package/src/wgpuShaderRegistry.test.ts +30 -0
  79. package/src/wgpuSurface.test.ts +49 -0
@@ -0,0 +1,88 @@
1
+ import {
2
+ createWgpuRenderStateRuntime,
3
+ destroyWgpuRenderState,
4
+ getWgpuRenderStateRuntime,
5
+ isWgpuSupported,
6
+ } from './wgpuRenderState';
7
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
8
+
9
+ beforeAll(() => {
10
+ installWgpuMock();
11
+ });
12
+
13
+ describe('createWgpuRenderState', () => {
14
+ it('returns a render state with device and context', async () => {
15
+ const state = await createWgpuRenderStateForTest();
16
+ expect(state.device).toBeDefined();
17
+ expect(state.context).toBeDefined();
18
+ });
19
+
20
+ it('sets allowSmoothing to true by default', async () => {
21
+ const state = await createWgpuRenderStateForTest();
22
+ expect(state.allowSmoothing).toBe(true);
23
+ });
24
+
25
+ it('initialises uniform ring buffer', async () => {
26
+ const state = await createWgpuRenderStateForTest();
27
+ const runtime = getWgpuRenderStateRuntime(state);
28
+ expect(runtime.uniformBuffer).toBeDefined();
29
+ expect(runtime.uniformData).toBeInstanceOf(Float32Array);
30
+ expect(runtime.uniformOffset).toBe(0);
31
+ });
32
+
33
+ it('stores the canvas', async () => {
34
+ const state = await createWgpuRenderStateForTest();
35
+ expect(state.canvas).toBeInstanceOf(HTMLCanvasElement);
36
+ });
37
+
38
+ it('starts with null renderPass and commandEncoder', async () => {
39
+ const state = await createWgpuRenderStateForTest();
40
+ const runtime = getWgpuRenderStateRuntime(state);
41
+ expect(runtime.renderPass).toBeNull();
42
+ expect(runtime.commandEncoder).toBeNull();
43
+ });
44
+ });
45
+
46
+ describe('createWgpuRenderStateRuntime', () => {
47
+ it('returns a runtime carrying the base entity-runtime binding slot', () => {
48
+ const runtime = createWgpuRenderStateRuntime();
49
+ expect(runtime.binding).toBeNull();
50
+ });
51
+ });
52
+
53
+ describe('destroyWgpuRenderState', () => {
54
+ it('destroys the state-owned uniform buffer', async () => {
55
+ const state = await createWgpuRenderStateForTest();
56
+ const runtime = getWgpuRenderStateRuntime(state);
57
+ const destroy = vi.spyOn(runtime.uniformBuffer, 'destroy');
58
+
59
+ destroyWgpuRenderState(state);
60
+
61
+ expect(destroy).toHaveBeenCalled();
62
+ });
63
+
64
+ it('does not throw on a fresh state with no lazily-created buffers', async () => {
65
+ const state = await createWgpuRenderStateForTest();
66
+ expect(() => destroyWgpuRenderState(state)).not.toThrow();
67
+ });
68
+ });
69
+
70
+ describe('getWgpuRenderStateRuntime', () => {
71
+ it('returns the runtime attached by createWgpuRenderState', async () => {
72
+ const state = await createWgpuRenderStateForTest();
73
+ const runtime = getWgpuRenderStateRuntime(state);
74
+ expect(runtime).toBeDefined();
75
+ expect(runtime.uniformBuffer).toBeDefined();
76
+ });
77
+
78
+ it('resolves the same runtime object on repeated calls', async () => {
79
+ const state = await createWgpuRenderStateForTest();
80
+ expect(getWgpuRenderStateRuntime(state)).toBe(getWgpuRenderStateRuntime(state));
81
+ });
82
+ });
83
+
84
+ describe('isWgpuSupported', () => {
85
+ it('returns true when navigator.gpu is present', () => {
86
+ expect(isWgpuSupported()).toBe(true);
87
+ });
88
+ });
@@ -0,0 +1,120 @@
1
+ import { createMatrix } from '@flighthq/geometry';
2
+
3
+ import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
4
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
5
+ import {
6
+ beginWgpuRenderTarget,
7
+ createWgpuRenderTarget,
8
+ destroyWgpuRenderTarget,
9
+ drawWgpuRenderTargetResult,
10
+ endWgpuRenderTarget,
11
+ resizeWgpuRenderTarget,
12
+ } from './wgpuRenderTarget';
13
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
14
+
15
+ beforeAll(() => {
16
+ installWgpuMock();
17
+ });
18
+
19
+ describe('beginWgpuRenderTarget', () => {
20
+ it('sets the render target viewport', async () => {
21
+ const state = await createWgpuRenderStateForTest();
22
+ renderWgpuBackground(state);
23
+ const target = createWgpuRenderTarget(state, 64, 64);
24
+ beginWgpuRenderTarget(state, target, createMatrix());
25
+ expect(getWgpuRenderStateRuntime(state).renderTargetViewport?.width).toBe(64);
26
+ endWgpuRenderTarget(state);
27
+ submitWgpuRenderPass(state);
28
+ });
29
+ });
30
+
31
+ describe('beginWgpuRenderTarget / endWgpuRenderTarget', () => {
32
+ it('switches the render target and restores it', async () => {
33
+ const state = await createWgpuRenderStateForTest();
34
+ renderWgpuBackground(state);
35
+
36
+ const target = createWgpuRenderTarget(state, 128, 128);
37
+ const transform = createMatrix();
38
+ beginWgpuRenderTarget(state, target, transform);
39
+
40
+ const runtime = getWgpuRenderStateRuntime(state);
41
+ expect(runtime.renderTargetViewport?.width).toBe(128);
42
+
43
+ endWgpuRenderTarget(state);
44
+ expect(runtime.renderTargetViewport).toBeNull();
45
+
46
+ submitWgpuRenderPass(state);
47
+ });
48
+ });
49
+
50
+ describe('createWgpuRenderTarget', () => {
51
+ it('returns a target with texture, view, bind group, and depth-stencil', async () => {
52
+ const state = await createWgpuRenderStateForTest();
53
+ const target = createWgpuRenderTarget(state, 256, 256);
54
+ expect(target.texture).toBeDefined();
55
+ expect(target.view).toBeDefined();
56
+ expect(target.bindGroup).toBeDefined();
57
+ expect(target.depthStencilTexture).toBeDefined();
58
+ expect(target.width).toBe(256);
59
+ expect(target.height).toBe(256);
60
+ });
61
+
62
+ it('clamps to minimum 1×1', async () => {
63
+ const state = await createWgpuRenderStateForTest();
64
+ const target = createWgpuRenderTarget(state, 0, 0);
65
+ expect(target.width).toBe(1);
66
+ expect(target.height).toBe(1);
67
+ });
68
+ });
69
+
70
+ describe('destroyWgpuRenderTarget', () => {
71
+ it('calls destroy on texture and depth-stencil', async () => {
72
+ const state = await createWgpuRenderStateForTest();
73
+ const target = createWgpuRenderTarget(state, 64, 64);
74
+ const destroyTexture = vi.spyOn(target.texture, 'destroy');
75
+ const destroyDepth = vi.spyOn(target.depthStencilTexture, 'destroy');
76
+ destroyWgpuRenderTarget(state, target);
77
+ expect(destroyTexture).toHaveBeenCalled();
78
+ expect(destroyDepth).toHaveBeenCalled();
79
+ });
80
+ });
81
+
82
+ describe('drawWgpuRenderTargetResult', () => {
83
+ it('does not throw with an open render pass', async () => {
84
+ const state = await createWgpuRenderStateForTest();
85
+ renderWgpuBackground(state);
86
+ const target = createWgpuRenderTarget(state, 64, 64);
87
+ const fakeNode = {
88
+ alpha: 1,
89
+ material: null,
90
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
91
+ };
92
+ expect(() => drawWgpuRenderTargetResult(state, fakeNode, target, createMatrix())).not.toThrow();
93
+ submitWgpuRenderPass(state);
94
+ });
95
+ });
96
+
97
+ describe('endWgpuRenderTarget', () => {
98
+ it('restores null renderTargetViewport', async () => {
99
+ const state = await createWgpuRenderStateForTest();
100
+ renderWgpuBackground(state);
101
+ const target = createWgpuRenderTarget(state, 32, 32);
102
+ beginWgpuRenderTarget(state, target, createMatrix());
103
+ endWgpuRenderTarget(state);
104
+ expect(getWgpuRenderStateRuntime(state).renderTargetViewport).toBeNull();
105
+ submitWgpuRenderPass(state);
106
+ });
107
+ });
108
+
109
+ describe('resizeWgpuRenderTarget', () => {
110
+ it('updates width, height, and bind group', async () => {
111
+ const state = await createWgpuRenderStateForTest();
112
+ const target = createWgpuRenderTarget(state, 64, 64);
113
+ const previousBindGroup = target.bindGroup;
114
+ resizeWgpuRenderTarget(state, target, 200, 150);
115
+ expect(target.width).toBe(200);
116
+ expect(target.height).toBe(150);
117
+ expect(target.bindGroup).toBeDefined();
118
+ expect(target.bindGroup).not.toBe(previousBindGroup);
119
+ });
120
+ });
@@ -0,0 +1,58 @@
1
+ import {
2
+ acquireWgpuRenderTarget,
3
+ createWgpuRenderTargetPool,
4
+ destroyWgpuRenderTargetPool,
5
+ releaseWgpuRenderTarget,
6
+ } from './wgpuRenderTargetPool';
7
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
8
+
9
+ beforeAll(() => {
10
+ installWgpuMock();
11
+ });
12
+
13
+ describe('acquireWgpuRenderTarget', () => {
14
+ it('reuses a released target matching width, height, and format', async () => {
15
+ const state = await createWgpuRenderStateForTest();
16
+ const pool = createWgpuRenderTargetPool();
17
+ const first = acquireWgpuRenderTarget(state, pool, { width: 64, height: 64 });
18
+ releaseWgpuRenderTarget(pool, first);
19
+ const second = acquireWgpuRenderTarget(state, pool, { width: 64, height: 64 });
20
+ expect(second).toBe(first);
21
+ });
22
+
23
+ it('allocates a distinct target when no free target matches the format', async () => {
24
+ const state = await createWgpuRenderStateForTest();
25
+ const pool = createWgpuRenderTargetPool();
26
+ const eight = acquireWgpuRenderTarget(state, pool, { width: 64, height: 64 });
27
+ releaseWgpuRenderTarget(pool, eight);
28
+ const hdr = acquireWgpuRenderTarget(state, pool, { width: 64, height: 64, format: 'rgba16float' });
29
+ expect(hdr).not.toBe(eight);
30
+ expect(hdr.format).toBe('rgba16float');
31
+ });
32
+ });
33
+
34
+ describe('createWgpuRenderTargetPool', () => {
35
+ it('starts with an empty free list', () => {
36
+ expect(createWgpuRenderTargetPool().free).toEqual([]);
37
+ });
38
+ });
39
+
40
+ describe('destroyWgpuRenderTargetPool', () => {
41
+ it('clears the free list', async () => {
42
+ const state = await createWgpuRenderStateForTest();
43
+ const pool = createWgpuRenderTargetPool();
44
+ releaseWgpuRenderTarget(pool, acquireWgpuRenderTarget(state, pool, { width: 32, height: 32 }));
45
+ destroyWgpuRenderTargetPool(state, pool);
46
+ expect(pool.free.length).toBe(0);
47
+ });
48
+ });
49
+
50
+ describe('releaseWgpuRenderTarget', () => {
51
+ it('returns the target to the free list', async () => {
52
+ const state = await createWgpuRenderStateForTest();
53
+ const pool = createWgpuRenderTargetPool();
54
+ const target = acquireWgpuRenderTarget(state, pool, { width: 16, height: 16 });
55
+ releaseWgpuRenderTarget(pool, target);
56
+ expect(pool.free).toContain(target);
57
+ });
58
+ });
@@ -0,0 +1,97 @@
1
+ import { renderWgpuBackground } from './wgpuBackground';
2
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
3
+ import { applyWgpuScissorRect, popWgpuScissorRect, pushWgpuScissorRect } from './wgpuScissor';
4
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
5
+
6
+ beforeAll(() => {
7
+ installWgpuMock();
8
+ });
9
+
10
+ describe('applyWgpuScissorRect', () => {
11
+ it('calls setScissorRect on the pass when a rect is active', async () => {
12
+ const state = await createWgpuRenderStateForTest();
13
+ renderWgpuBackground(state);
14
+ const runtime = getWgpuRenderStateRuntime(state);
15
+ runtime.currentScissorRect = { x: 10, y: 20, width: 100, height: 50 };
16
+ const calls: number[][] = [];
17
+ const fakePass = { setScissorRect: (...args: number[]) => calls.push(args) } as unknown as GPURenderPassEncoder;
18
+ applyWgpuScissorRect(state, fakePass);
19
+ expect(calls.length).toBe(1);
20
+ expect(calls[0]).toEqual([10, 20, 100, 50]);
21
+ });
22
+
23
+ it('is a no-op when currentScissorRect is null', async () => {
24
+ const state = await createWgpuRenderStateForTest();
25
+ renderWgpuBackground(state);
26
+ const calls: number[][] = [];
27
+ const fakePass = { setScissorRect: (...args: number[]) => calls.push(args) } as unknown as GPURenderPassEncoder;
28
+ applyWgpuScissorRect(state, fakePass);
29
+ expect(calls.length).toBe(0);
30
+ });
31
+
32
+ it('clamps dimensions to at least 1×1', async () => {
33
+ const state = await createWgpuRenderStateForTest();
34
+ renderWgpuBackground(state);
35
+ const runtime = getWgpuRenderStateRuntime(state);
36
+ runtime.currentScissorRect = { x: 0, y: 0, width: 0, height: 0 };
37
+ const calls: number[][] = [];
38
+ const fakePass = { setScissorRect: (...args: number[]) => calls.push(args) } as unknown as GPURenderPassEncoder;
39
+ applyWgpuScissorRect(state, fakePass);
40
+ expect(calls[0]).toEqual([0, 0, 1, 1]);
41
+ });
42
+ });
43
+
44
+ describe('popWgpuScissorRect', () => {
45
+ it('is a no-op when the stack is empty', async () => {
46
+ const state = await createWgpuRenderStateForTest();
47
+ renderWgpuBackground(state);
48
+ expect(() => popWgpuScissorRect(state)).not.toThrow();
49
+ expect(getWgpuRenderStateRuntime(state).currentScissorRect).toBeNull();
50
+ });
51
+
52
+ it('restores the previous rect after a push/pop pair', async () => {
53
+ const state = await createWgpuRenderStateForTest();
54
+ renderWgpuBackground(state);
55
+ const r1 = { x: 0, y: 0, width: 100, height: 100 };
56
+ const r2 = { x: 10, y: 10, width: 50, height: 50 };
57
+ pushWgpuScissorRect(state, r1);
58
+ pushWgpuScissorRect(state, r2);
59
+ popWgpuScissorRect(state);
60
+ const runtime = getWgpuRenderStateRuntime(state);
61
+ expect(runtime.currentScissorRect).toEqual(r1);
62
+ popWgpuScissorRect(state);
63
+ expect(runtime.currentScissorRect).toBeNull();
64
+ });
65
+ });
66
+
67
+ describe('pushWgpuScissorRect', () => {
68
+ it('sets currentScissorRect on first push', async () => {
69
+ const state = await createWgpuRenderStateForTest();
70
+ renderWgpuBackground(state);
71
+ const rect = { x: 5, y: 10, width: 80, height: 40 };
72
+ pushWgpuScissorRect(state, rect);
73
+ expect(getWgpuRenderStateRuntime(state).currentScissorRect).toEqual(rect);
74
+ });
75
+
76
+ it('stacks previous rect when pushing a second rect', async () => {
77
+ const state = await createWgpuRenderStateForTest();
78
+ renderWgpuBackground(state);
79
+ const r1 = { x: 0, y: 0, width: 100, height: 100 };
80
+ const r2 = { x: 20, y: 20, width: 60, height: 60 };
81
+ pushWgpuScissorRect(state, r1);
82
+ pushWgpuScissorRect(state, r2);
83
+ const runtime = getWgpuRenderStateRuntime(state);
84
+ expect(runtime.currentScissorRect).toEqual(r2);
85
+ expect(runtime.scissorStack.length).toBe(1);
86
+ expect(runtime.scissorStack[0]).toEqual(r1);
87
+ });
88
+
89
+ it('copies the rect so mutation of the original does not affect the pushed value', async () => {
90
+ const state = await createWgpuRenderStateForTest();
91
+ renderWgpuBackground(state);
92
+ const rect = { x: 0, y: 0, width: 50, height: 50 };
93
+ pushWgpuScissorRect(state, rect);
94
+ rect.width = 999;
95
+ expect(getWgpuRenderStateRuntime(state).currentScissorRect?.width).toBe(50);
96
+ });
97
+ });
@@ -0,0 +1,140 @@
1
+ import { renderWgpuBackground } from './wgpuBackground';
2
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
3
+ import {
4
+ createWgpuBindGroupLayouts,
5
+ createWgpuPipelineLayout,
6
+ getActiveWgpuPipeline,
7
+ getWgpuPipeline,
8
+ setWgpuMatrixFromTransform,
9
+ UNIFORM_BYTE_SIZE,
10
+ writeWgpuMatrixOnlyUniforms,
11
+ writeWgpuQuadUniforms,
12
+ } from './wgpuShader';
13
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
14
+
15
+ beforeAll(() => {
16
+ installWgpuMock();
17
+ });
18
+
19
+ describe('createWgpuBindGroupLayouts', () => {
20
+ it('returns uniformBindGroupLayout and textureBindGroupLayout', async () => {
21
+ const state = await createWgpuRenderStateForTest();
22
+ const layouts = createWgpuBindGroupLayouts(state.device);
23
+ expect(layouts.uniformBindGroupLayout).toBeDefined();
24
+ expect(layouts.textureBindGroupLayout).toBeDefined();
25
+ });
26
+ });
27
+
28
+ describe('createWgpuPipelineLayout', () => {
29
+ it('returns a pipeline layout', async () => {
30
+ const state = await createWgpuRenderStateForTest();
31
+ const runtime = getWgpuRenderStateRuntime(state);
32
+ const layout = createWgpuPipelineLayout(
33
+ state.device,
34
+ runtime.uniformBindGroupLayout,
35
+ runtime.textureBindGroupLayout,
36
+ );
37
+ expect(layout).toBeDefined();
38
+ });
39
+ });
40
+
41
+ describe('getActiveWgpuPipeline', () => {
42
+ it('returns the normal pipeline when no mask is active', async () => {
43
+ const state = await createWgpuRenderStateForTest();
44
+ const runtime = getWgpuRenderStateRuntime(state);
45
+ runtime.currentMaskDepth = 0;
46
+ runtime.maskWriteMode = false;
47
+ const pipeline = getActiveWgpuPipeline(state);
48
+ expect(pipeline).toBeDefined();
49
+ });
50
+ });
51
+
52
+ describe('getWgpuPipeline', () => {
53
+ it('returns a cached pipeline on second call', async () => {
54
+ const state = await createWgpuRenderStateForTest();
55
+ const p1 = getWgpuPipeline(state, null, 'normal');
56
+ const p2 = getWgpuPipeline(state, null, 'normal');
57
+ expect(p1).toBe(p2);
58
+ });
59
+
60
+ it('returns different pipelines for different stencil modes', async () => {
61
+ const state = await createWgpuRenderStateForTest();
62
+ const normal = getWgpuPipeline(state, null, 'normal');
63
+ const maskwrite = getWgpuPipeline(state, null, 'maskwrite');
64
+ expect(normal).not.toBe(maskwrite);
65
+ });
66
+ });
67
+
68
+ describe('setWgpuMatrixFromTransform', () => {
69
+ it('builds a column-major clip-space matrix from an identity transform', () => {
70
+ const m = new Float32Array(9);
71
+ setWgpuMatrixFromTransform(m, { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, { width: 2, height: 2 });
72
+ // Identity scale on a 2×2 viewport: a*iw=1, -d*ih=-1, tx*iw-1=-1, -ty*ih+1=1
73
+ expect(m[0]).toBeCloseTo(1);
74
+ expect(m[4]).toBeCloseTo(-1);
75
+ expect(m[6]).toBeCloseTo(-1);
76
+ expect(m[7]).toBeCloseTo(1);
77
+ });
78
+
79
+ it('builds a distinct out from aliased inputs (out === input matrix)', () => {
80
+ const m = new Float32Array(9);
81
+ setWgpuMatrixFromTransform(m, { a: 2, b: 0, c: 0, d: 2, tx: 100, ty: 50 }, { width: 400, height: 300 });
82
+ expect(m[0]).toBeCloseTo(2 * (2 / 400));
83
+ });
84
+ });
85
+
86
+ describe('UNIFORM_BYTE_SIZE', () => {
87
+ it('is 128 bytes', () => {
88
+ expect(UNIFORM_BYTE_SIZE).toBe(128);
89
+ });
90
+ });
91
+
92
+ describe('writeWgpuMatrixOnlyUniforms', () => {
93
+ it('advances uniformOffset', async () => {
94
+ const state = await createWgpuRenderStateForTest();
95
+ const runtime = getWgpuRenderStateRuntime(state);
96
+ renderWgpuBackground(state);
97
+ const before = runtime.uniformOffset;
98
+ const fakeNode = { alpha: 1, useColorTransform: false, colorTransform: null };
99
+ const t = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
100
+ writeWgpuMatrixOnlyUniforms(state, fakeNode as never, t, 0, 0, 10, 10, 0, 0, 1, 1);
101
+ expect(runtime.uniformOffset).toBe(before + runtime.uniformStride);
102
+ });
103
+ });
104
+
105
+ describe('writeWgpuQuadUniforms', () => {
106
+ it('advances uniformOffset by uniformStride', async () => {
107
+ const state = await createWgpuRenderStateForTest();
108
+ const runtime = getWgpuRenderStateRuntime(state);
109
+ renderWgpuBackground(state);
110
+ const before = runtime.uniformOffset;
111
+ const fakeNode = {
112
+ alpha: 1,
113
+ useColorTransform: false,
114
+ colorTransform: null,
115
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
116
+ };
117
+ writeWgpuQuadUniforms(state, fakeNode, null, 0, 0, 100, 100, 0, 0, 1, 1);
118
+ expect(runtime.uniformOffset).toBe(before + runtime.uniformStride);
119
+ });
120
+
121
+ it('writes quad coordinates into uniform data at float offset 24', async () => {
122
+ const state = await createWgpuRenderStateForTest();
123
+ const runtime = getWgpuRenderStateRuntime(state);
124
+ renderWgpuBackground(state);
125
+ const fakeNode = {
126
+ alpha: 0.5,
127
+ useColorTransform: false,
128
+ colorTransform: null,
129
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
130
+ };
131
+ const offset = writeWgpuQuadUniforms(state, fakeNode, null, 10, 20, 30, 40, 0.1, 0.2, 0.9, 0.8);
132
+ const floatBase = offset >> 2;
133
+ expect(runtime.uniformData[floatBase + 24]).toBeCloseTo(10);
134
+ expect(runtime.uniformData[floatBase + 25]).toBeCloseTo(20);
135
+ expect(runtime.uniformData[floatBase + 26]).toBeCloseTo(30);
136
+ expect(runtime.uniformData[floatBase + 27]).toBeCloseTo(40);
137
+ expect(runtime.uniformData[floatBase + 28]).toBeCloseTo(0.1);
138
+ expect(runtime.uniformData[floatBase + 12]).toBeCloseTo(0.5); // alpha
139
+ });
140
+ });
@@ -0,0 +1,62 @@
1
+ import { createBitmap } from '@flighthq/displayobject';
2
+ import { getOrCreateRenderProxy2D } from '@flighthq/render';
3
+ import type { WgpuBitmapShader } from '@flighthq/types';
4
+
5
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
6
+ import { getWgpuShader, resolveWgpuShader, setWgpuShader } from './wgpuShaderBinding';
7
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
8
+
9
+ beforeAll(() => {
10
+ installWgpuMock();
11
+ });
12
+
13
+ describe('getWgpuShader', () => {
14
+ it('returns undefined when no shader is bound', async () => {
15
+ const state = await createWgpuRenderStateForTest();
16
+ const bitmap = createBitmap();
17
+ const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
18
+ expect(getWgpuShader(renderProxy)).toBeUndefined();
19
+ });
20
+ });
21
+
22
+ describe('resolveWgpuShader', () => {
23
+ it('returns null when no shader feature is enabled', async () => {
24
+ const state = await createWgpuRenderStateForTest();
25
+ const bitmap = createBitmap();
26
+ const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
27
+ expect(resolveWgpuShader(state, renderProxy)).toBeNull();
28
+ });
29
+ });
30
+
31
+ describe('setWgpuShader', () => {
32
+ it('binds a shader to a display object', async () => {
33
+ const state = await createWgpuRenderStateForTest();
34
+ const bitmap = createBitmap();
35
+ const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
36
+
37
+ setWgpuShader(state, bitmap, fakeShader);
38
+
39
+ const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
40
+ expect(getWgpuShader(renderProxy)).toBe(fakeShader);
41
+ });
42
+
43
+ it('installs the per-node shader binding resolver', async () => {
44
+ const state = await createWgpuRenderStateForTest();
45
+ const runtime = getWgpuRenderStateRuntime(state);
46
+ const bitmap = createBitmap();
47
+ const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
48
+ expect(runtime.webgpuShaderBindingResolver).toBeUndefined();
49
+ setWgpuShader(state, bitmap, fakeShader);
50
+ expect(runtime.webgpuShaderBindingResolver).toBe(getWgpuShader);
51
+ });
52
+
53
+ it('removes the binding when shader is null', async () => {
54
+ const state = await createWgpuRenderStateForTest();
55
+ const bitmap = createBitmap();
56
+ const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
57
+ setWgpuShader(state, bitmap, fakeShader);
58
+ setWgpuShader(state, bitmap, null);
59
+ const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
60
+ expect(getWgpuShader(renderProxy)).toBeUndefined();
61
+ });
62
+ });
@@ -0,0 +1,30 @@
1
+ import type { WgpuBitmapShader } from '@flighthq/types';
2
+
3
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
4
+ import { registerWgpuBitmapShader } from './wgpuShaderRegistry';
5
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
6
+
7
+ beforeAll(() => {
8
+ installWgpuMock();
9
+ });
10
+
11
+ describe('registerWgpuBitmapShader', () => {
12
+ it('sets the shader as the state default', async () => {
13
+ const state = await createWgpuRenderStateForTest();
14
+ const fakeShader = {
15
+ pipeline: {},
16
+ bind: () => {},
17
+ } as unknown as WgpuBitmapShader;
18
+ registerWgpuBitmapShader(state, fakeShader);
19
+ expect(getWgpuRenderStateRuntime(state).defaultBitmapShader).toBe(fakeShader);
20
+ });
21
+
22
+ it('replaces a previously registered shader', async () => {
23
+ const state = await createWgpuRenderStateForTest();
24
+ const shader1 = { pipeline: {}, bind: () => {} } as unknown as WgpuBitmapShader;
25
+ const shader2 = { pipeline: {}, bind: () => {} } as unknown as WgpuBitmapShader;
26
+ registerWgpuBitmapShader(state, shader1);
27
+ registerWgpuBitmapShader(state, shader2);
28
+ expect(getWgpuRenderStateRuntime(state).defaultBitmapShader).toBe(shader2);
29
+ });
30
+ });
@@ -0,0 +1,49 @@
1
+ import { beforeAll, describe, expect, it } from 'vitest';
2
+
3
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
4
+ import {
5
+ acquireWgpuFrameCaptureTexture,
6
+ createSurfaceFromWgpuRenderState,
7
+ enableWgpuFrameCapture,
8
+ encodeWgpuFrameCapture,
9
+ } from './wgpuSurface';
10
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
11
+
12
+ beforeAll(() => {
13
+ installWgpuMock();
14
+ });
15
+
16
+ describe('acquireWgpuFrameCaptureTexture', () => {
17
+ it('returns null until capture is enabled, then an offscreen texture', async () => {
18
+ const state = await createWgpuRenderStateForTest();
19
+ expect(acquireWgpuFrameCaptureTexture(state)).toBeNull();
20
+ enableWgpuFrameCapture(state);
21
+ expect(acquireWgpuFrameCaptureTexture(state)).not.toBeNull();
22
+ });
23
+ });
24
+
25
+ describe('createSurfaceFromWgpuRenderState', () => {
26
+ it('throws when frame capture was never enabled (no buffer to read)', async () => {
27
+ const state = await createWgpuRenderStateForTest();
28
+ await expect(createSurfaceFromWgpuRenderState(state)).rejects.toThrow(/enableWgpuFrameCapture/);
29
+ });
30
+ });
31
+
32
+ describe('enableWgpuFrameCapture', () => {
33
+ it('sets the capture flag on the render state runtime', async () => {
34
+ const state = await createWgpuRenderStateForTest();
35
+ expect(getWgpuRenderStateRuntime(state).frameCaptureEnabled).toBeFalsy();
36
+ enableWgpuFrameCapture(state);
37
+ expect(getWgpuRenderStateRuntime(state).frameCaptureEnabled).toBe(true);
38
+ });
39
+ });
40
+
41
+ describe('encodeWgpuFrameCapture', () => {
42
+ it('is a no-op when capture is disabled', async () => {
43
+ const state = await createWgpuRenderStateForTest();
44
+ let copied = false;
45
+ const encoder = { copyTextureToBuffer: () => (copied = true) } as unknown as GPUCommandEncoder;
46
+ encodeWgpuFrameCapture(state, encoder);
47
+ expect(copied).toBe(false);
48
+ });
49
+ });