@flighthq/render-wgpu 0.3.0-next.906.07cea63 → 0.3.1-next.209.43ef1bc

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 (91) hide show
  1. package/dist/contract.d.ts +7 -1
  2. package/dist/contract.d.ts.map +1 -1
  3. package/dist/contract.js +7 -1
  4. package/dist/contract.js.map +1 -1
  5. package/dist/enableWgpuTextureResolverGuards.d.ts +4 -0
  6. package/dist/enableWgpuTextureResolverGuards.d.ts.map +1 -0
  7. package/dist/enableWgpuTextureResolverGuards.js +8 -0
  8. package/dist/enableWgpuTextureResolverGuards.js.map +1 -0
  9. package/dist/explainWgpuTextureResolution.d.ts +3 -0
  10. package/dist/explainWgpuTextureResolution.d.ts.map +1 -0
  11. package/dist/explainWgpuTextureResolution.js +14 -0
  12. package/dist/explainWgpuTextureResolution.js.map +1 -0
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/wgpuBackground.js +2 -2
  18. package/dist/wgpuBackground.js.map +1 -1
  19. package/dist/wgpuCompressedTexture.d.ts +2 -2
  20. package/dist/wgpuCompressedTexture.d.ts.map +1 -1
  21. package/dist/wgpuCompressedTexture.js +40 -12
  22. package/dist/wgpuCompressedTexture.js.map +1 -1
  23. package/dist/wgpuDraw.d.ts +6 -4
  24. package/dist/wgpuDraw.d.ts.map +1 -1
  25. package/dist/wgpuDraw.js +139 -67
  26. package/dist/wgpuDraw.js.map +1 -1
  27. package/dist/wgpuExternalImageSource.d.ts +3 -0
  28. package/dist/wgpuExternalImageSource.d.ts.map +1 -0
  29. package/dist/wgpuExternalImageSource.js +51 -0
  30. package/dist/wgpuExternalImageSource.js.map +1 -0
  31. package/dist/wgpuExternalTexture.d.ts +4 -0
  32. package/dist/wgpuExternalTexture.d.ts.map +1 -0
  33. package/dist/wgpuExternalTexture.js +62 -0
  34. package/dist/wgpuExternalTexture.js.map +1 -0
  35. package/dist/wgpuMaterialRegistry.d.ts.map +1 -1
  36. package/dist/wgpuMaterialRegistry.js +10 -9
  37. package/dist/wgpuMaterialRegistry.js.map +1 -1
  38. package/dist/wgpuMipmap.d.ts.map +1 -1
  39. package/dist/wgpuMipmap.js +11 -9
  40. package/dist/wgpuMipmap.js.map +1 -1
  41. package/dist/wgpuRenderState.d.ts +11 -2
  42. package/dist/wgpuRenderState.d.ts.map +1 -1
  43. package/dist/wgpuRenderState.js +217 -30
  44. package/dist/wgpuRenderState.js.map +1 -1
  45. package/dist/wgpuRenderTarget.d.ts.map +1 -1
  46. package/dist/wgpuRenderTarget.js +2 -0
  47. package/dist/wgpuRenderTarget.js.map +1 -1
  48. package/dist/wgpuRenderTexture.d.ts +15 -0
  49. package/dist/wgpuRenderTexture.d.ts.map +1 -0
  50. package/dist/wgpuRenderTexture.js +130 -0
  51. package/dist/wgpuRenderTexture.js.map +1 -0
  52. package/dist/wgpuRenderTexturePool.d.ts +7 -0
  53. package/dist/wgpuRenderTexturePool.d.ts.map +1 -0
  54. package/dist/wgpuRenderTexturePool.js +86 -0
  55. package/dist/wgpuRenderTexturePool.js.map +1 -0
  56. package/dist/wgpuShader.d.ts.map +1 -1
  57. package/dist/wgpuShader.js +5 -1
  58. package/dist/wgpuShader.js.map +1 -1
  59. package/dist/wgpuSurface.d.ts +2 -2
  60. package/dist/wgpuSurface.d.ts.map +1 -1
  61. package/dist/wgpuSurface.js +61 -13
  62. package/dist/wgpuSurface.js.map +1 -1
  63. package/dist/wgpuTestHelper.d.ts +1 -0
  64. package/dist/wgpuTestHelper.d.ts.map +1 -1
  65. package/dist/wgpuTestHelper.js +15 -1
  66. package/dist/wgpuTestHelper.js.map +1 -1
  67. package/dist/wgpuTextureResolver.d.ts +9 -0
  68. package/dist/wgpuTextureResolver.d.ts.map +1 -0
  69. package/dist/wgpuTextureResolver.js +67 -0
  70. package/dist/wgpuTextureResolver.js.map +1 -0
  71. package/dist/wgpuTextureUpload.d.ts +2 -2
  72. package/dist/wgpuTextureUpload.d.ts.map +1 -1
  73. package/dist/wgpuTextureUpload.js +5 -10
  74. package/dist/wgpuTextureUpload.js.map +1 -1
  75. package/package.json +13 -7
  76. package/src/enableWgpuTextureResolverGuards.test.ts +65 -0
  77. package/src/explainWgpuTextureResolution.test.ts +36 -0
  78. package/src/wgpuCompressedTexture.test.ts +18 -35
  79. package/src/wgpuDraw.test.ts +164 -62
  80. package/src/wgpuExternalImageSource.test.ts +89 -0
  81. package/src/wgpuExternalTexture.test.ts +35 -0
  82. package/src/wgpuMaterialRegistry.test.ts +28 -2
  83. package/src/wgpuMipmap.test.ts +10 -2
  84. package/src/wgpuRenderState.test.ts +143 -11
  85. package/src/wgpuRenderTarget.test.ts +14 -0
  86. package/src/wgpuRenderTexture.test.ts +165 -0
  87. package/src/wgpuRenderTexturePool.test.ts +146 -0
  88. package/src/wgpuShaderBinding.test.ts +6 -6
  89. package/src/wgpuSurface.test.ts +95 -3
  90. package/src/wgpuTextureResolver.test.ts +209 -0
  91. package/src/wgpuTextureUpload.test.ts +28 -13
@@ -1,4 +1,17 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
1
2
  import {
3
+ copyAllRenderersFromRenderState,
4
+ getRenderStateRuntime,
5
+ prepareScene2DRender,
6
+ registerRenderer,
7
+ } from '@flighthq/render/contract';
8
+ import { createDisplayObject } from '@flighthq/scene2d/contract';
9
+
10
+ import { beginWgpuFrame } from './wgpuBackground';
11
+ import { registerWgpuMaterialRenderer } from './wgpuMaterialRegistry';
12
+ import {
13
+ copyWgpuRenderStateRegistrations,
14
+ createWgpuOffscreenRenderState,
2
15
  createWgpuRenderStateRuntime,
3
16
  destroyWgpuRenderState,
4
17
  getWgpuRenderStateRuntime,
@@ -6,11 +19,121 @@ import {
6
19
  isWgpuSupported,
7
20
  } from './wgpuRenderState';
8
21
  import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
22
+ import { registerWgpuTextureResolver } from './wgpuTextureResolver';
9
23
 
10
24
  beforeAll(() => {
11
25
  installWgpuMock();
12
26
  });
13
27
 
28
+ describe('copyWgpuRenderStateRegistrations', () => {
29
+ it('copies late Wgpu registrations only when explicitly requested', async () => {
30
+ const screen = await createWgpuRenderStateForTest();
31
+ const offscreen = createWgpuOffscreenRenderState(screen);
32
+ const resolver = vi.fn(() => null);
33
+ registerWgpuTextureResolver(screen, 'acme.LateTexture', resolver);
34
+
35
+ expect(getWgpuRenderStateRuntime(offscreen).wgpuTextureResolverRegistry?.has('acme.LateTexture')).not.toBe(true);
36
+ copyWgpuRenderStateRegistrations(offscreen, screen);
37
+ expect(getWgpuRenderStateRuntime(offscreen).wgpuTextureResolverRegistry?.get('acme.LateTexture')).toBe(resolver);
38
+ });
39
+ });
40
+
41
+ describe('createWgpuOffscreenRenderState', () => {
42
+ it('shares device resources while snapshotting independent registration policy', async () => {
43
+ const screen = await createWgpuRenderStateForTest();
44
+ const renderer = { createData: () => null, submit: vi.fn() };
45
+ const materialRenderer = { instanceFloatCount: 0, getShaderModule: vi.fn() } as never;
46
+ const paddingResolver = vi.fn(() => ({ bottom: 1, left: 1, right: 1, top: 1 }));
47
+ const textureResolver = vi.fn(() => null);
48
+ const effectRunner = vi.fn();
49
+ registerRenderer(screen, 'acme.Node', renderer);
50
+ registerWgpuMaterialRenderer(screen, 'acme.Material', materialRenderer);
51
+ registerWgpuTextureResolver(screen, 'acme.Texture', textureResolver);
52
+ getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.Effect', paddingResolver]]);
53
+ getWgpuRenderStateRuntime(screen).wgpuRenderEffectRegistry = new Map([['acme.Effect', effectRunner]]);
54
+ getWgpuRenderStateRuntime(screen).wgpuRenderTextureCache = new WeakMap();
55
+
56
+ const offscreen = createWgpuOffscreenRenderState(screen);
57
+ const screenRuntime = getWgpuRenderStateRuntime(screen);
58
+ const offscreenRuntime = getWgpuRenderStateRuntime(offscreen);
59
+
60
+ expect(offscreen.device).toBe(screen.device);
61
+ expect(offscreen.canvas).toBe(screen.canvas);
62
+ expect(offscreenRuntime.pipelineCache).toBe(screenRuntime.pipelineCache);
63
+ expect(offscreenRuntime.textureCache).toBe(screenRuntime.textureCache);
64
+ expect(offscreenRuntime.wgpuRenderTextureCache).toBe(screenRuntime.wgpuRenderTextureCache);
65
+ expect(offscreenRuntime.uniformBindGroupLayout).toBe(screenRuntime.uniformBindGroupLayout);
66
+ expect(offscreenRuntime.uniformBuffer).not.toBe(screenRuntime.uniformBuffer);
67
+ expect(offscreenRuntime.rendererMap).not.toBe(screenRuntime.rendererMap);
68
+ expect(offscreenRuntime.materialRendererMap).not.toBe(screenRuntime.materialRendererMap);
69
+ expect(offscreenRuntime.wgpuTextureResolverRegistry).not.toBe(screenRuntime.wgpuTextureResolverRegistry);
70
+ expect(offscreenRuntime.wgpuRenderEffectRegistry).not.toBe(screenRuntime.wgpuRenderEffectRegistry);
71
+ expect(offscreenRuntime.renderEffectPaddingResolverRegistry).not.toBe(
72
+ screenRuntime.renderEffectPaddingResolverRegistry,
73
+ );
74
+ expect(offscreenRuntime.rendererMap.get('acme.Node')).toBe(renderer);
75
+ expect(offscreenRuntime.materialRendererMap?.get('acme.Material')).toBe(materialRenderer);
76
+ expect(offscreenRuntime.wgpuTextureResolverRegistry?.get('acme.Texture')).toBe(textureResolver);
77
+ expect(offscreenRuntime.wgpuRenderEffectRegistry?.get('acme.Effect')).toBe(effectRunner);
78
+ expect(offscreenRuntime.renderEffectPaddingResolverRegistry?.get('acme.Effect')).toBe(paddingResolver);
79
+ });
80
+
81
+ it('owns an independent encoder and proxy tree', async () => {
82
+ const screen = await createWgpuRenderStateForTest();
83
+ const offscreen = createWgpuOffscreenRenderState(screen);
84
+ const root = createDisplayObject();
85
+ registerRenderer(offscreen, root.kind, { createData: () => createEntity({}), submit: vi.fn() });
86
+ prepareScene2DRender(offscreen, root);
87
+ beginWgpuFrame(screen);
88
+ beginWgpuFrame(offscreen);
89
+
90
+ expect(getRenderStateRuntime(offscreen).renderProxyMap).not.toBe(getRenderStateRuntime(screen).renderProxyMap);
91
+ expect(getWgpuRenderStateRuntime(offscreen).commandEncoder).not.toBe(
92
+ getWgpuRenderStateRuntime(screen).commandEncoder,
93
+ );
94
+ });
95
+
96
+ it('destroys derived renderer data and its own uniform ring without freeing the screen ring', async () => {
97
+ const screen = await createWgpuRenderStateForTest();
98
+ const root = createDisplayObject();
99
+ const destroyData = vi.fn();
100
+ registerRenderer(screen, root.kind, {
101
+ createData: () => createEntity({}),
102
+ destroyData,
103
+ submit: vi.fn(),
104
+ });
105
+ const offscreen = createWgpuOffscreenRenderState(screen);
106
+ prepareScene2DRender(offscreen, root);
107
+ const screenDestroy = vi.spyOn(getWgpuRenderStateRuntime(screen).uniformBuffer, 'destroy');
108
+ const offscreenDestroy = vi.spyOn(getWgpuRenderStateRuntime(offscreen).uniformBuffer, 'destroy');
109
+
110
+ destroyWgpuRenderState(offscreen);
111
+
112
+ expect(destroyData).toHaveBeenCalledOnce();
113
+ expect(offscreenDestroy).toHaveBeenCalledOnce();
114
+ expect(screenDestroy).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('requires explicit re-copy for renderers and padding registered after derivation', async () => {
118
+ const screen = await createWgpuRenderStateForTest();
119
+ const offscreen = createWgpuOffscreenRenderState(screen);
120
+ const renderer = { createData: () => null, submit: vi.fn() };
121
+ const paddingResolver = vi.fn(() => ({ bottom: 2, left: 2, right: 2, top: 2 }));
122
+ registerRenderer(screen, 'acme.LateNode', renderer);
123
+ getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.LateEffect', paddingResolver]]);
124
+
125
+ expect(getRenderStateRuntime(offscreen).rendererMap.has('acme.LateNode')).toBe(false);
126
+ expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.has('acme.LateEffect')).not.toBe(true);
127
+
128
+ copyAllRenderersFromRenderState(offscreen, screen);
129
+ copyWgpuRenderStateRegistrations(offscreen, screen);
130
+ expect(getRenderStateRuntime(offscreen).rendererMap.get('acme.LateNode')).toBe(renderer);
131
+ expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.get('acme.LateEffect')).toBe(
132
+ paddingResolver,
133
+ );
134
+ });
135
+ });
136
+
14
137
  describe('createWgpuRenderState', () => {
15
138
  it('returns a render state with device and context', async () => {
16
139
  const state = await createWgpuRenderStateForTest();
@@ -85,8 +208,8 @@ describe('getWgpuRenderStateRuntime', () => {
85
208
  describe('getWgpuSampler', () => {
86
209
  it('caches a sampler per filter+wrap+mip+anisotropy config and reuses it (one createSampler)', async () => {
87
210
  const state = await createWgpuRenderStateForTest();
88
- const a = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
89
- const b = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
211
+ const a = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
212
+ const b = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
90
213
  expect(a).toBe(b);
91
214
  // The cache is keyed by a packed NUMBER (no per-call string allocation), and one config caches once.
92
215
  expect([...getWgpuRenderStateRuntime(state).samplerCache.keys()].every((k) => typeof k === 'number')).toBe(true);
@@ -95,17 +218,17 @@ describe('getWgpuSampler', () => {
95
218
 
96
219
  it('returns a distinct sampler for a different wrap or filter', async () => {
97
220
  const state = await createWgpuRenderStateForTest();
98
- const repeat = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
99
- const clamp = getWgpuSampler(state, 'linear', 'clamp-to-edge', 'clamp-to-edge');
100
- const nearest = getWgpuSampler(state, 'nearest', 'repeat', 'repeat');
221
+ const repeat = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
222
+ const clamp = getWgpuSampler(state, 'linear', 'linear', 'clamp-to-edge', 'clamp-to-edge');
223
+ const nearest = getWgpuSampler(state, 'nearest', 'nearest', 'repeat', 'repeat');
101
224
  expect(repeat).not.toBe(clamp);
102
225
  expect(repeat).not.toBe(nearest);
103
226
  });
104
227
 
105
228
  it('keys the mip filter separately so a trilinear sampler differs from a non-mip one', async () => {
106
229
  const state = await createWgpuRenderStateForTest();
107
- const noMip = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
108
- const trilinear = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear');
230
+ const noMip = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
231
+ const trilinear = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear');
109
232
  expect(noMip).not.toBe(trilinear);
110
233
  expect(getWgpuRenderStateRuntime(state).samplerCache.size).toBe(2);
111
234
  });
@@ -114,18 +237,27 @@ describe('getWgpuSampler', () => {
114
237
  // WebGPU rejects maxAnisotropy > 1 unless min/mag/mip are all linear, so a nearest+aniso request
115
238
  // collapses to the SAME sampler as the explicit linear/trilinear anisotropic request.
116
239
  const state = await createWgpuRenderStateForTest();
117
- const collapsed = getWgpuSampler(state, 'nearest', 'clamp-to-edge', 'clamp-to-edge', undefined, 8);
118
- const explicit = getWgpuSampler(state, 'linear', 'clamp-to-edge', 'clamp-to-edge', 'linear', 8);
240
+ const collapsed = getWgpuSampler(state, 'nearest', 'nearest', 'clamp-to-edge', 'clamp-to-edge', undefined, 8);
241
+ const explicit = getWgpuSampler(state, 'linear', 'linear', 'clamp-to-edge', 'clamp-to-edge', 'linear', 8);
119
242
  expect(collapsed).toBe(explicit);
120
243
  });
121
244
 
122
245
  it('floors and clamps the anisotropy level into the cache key', async () => {
123
246
  const state = await createWgpuRenderStateForTest();
124
- const a = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear', 4.9);
125
- const b = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear', 4);
247
+ const a = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear', 4.9);
248
+ const b = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear', 4);
126
249
  expect(a).toBe(b);
127
250
  expect(getWgpuRenderStateRuntime(state).samplerCache.size).toBe(1);
128
251
  });
252
+
253
+ it('keeps independent minification and magnification filters', async () => {
254
+ const state = await createWgpuRenderStateForTest();
255
+ const createSampler = vi.spyOn(state.device, 'createSampler');
256
+ getWgpuSampler(state, 'nearest', 'linear', 'repeat', 'repeat');
257
+ expect(createSampler).toHaveBeenLastCalledWith(
258
+ expect.objectContaining({ minFilter: 'nearest', magFilter: 'linear' }),
259
+ );
260
+ });
129
261
  });
130
262
 
131
263
  describe('isWgpuSupported', () => {
@@ -155,6 +155,20 @@ describe('endWgpuRenderPass', () => {
155
155
  });
156
156
 
157
157
  describe('resizeWgpuRenderTarget', () => {
158
+ it('preserves the allocation when dimensions are unchanged', async () => {
159
+ const state = await createWgpuRenderStateForTest();
160
+ const target = createWgpuRenderTarget(state, 64, 64);
161
+ const texture = target.texture;
162
+ const depthStencilTexture = target.depthStencilTexture;
163
+ const bindGroup = target.bindGroup;
164
+
165
+ resizeWgpuRenderTarget(state, target, 64, 64);
166
+
167
+ expect(target.texture).toBe(texture);
168
+ expect(target.depthStencilTexture).toBe(depthStencilTexture);
169
+ expect(target.bindGroup).toBe(bindGroup);
170
+ });
171
+
158
172
  it('updates width, height, and bind group', async () => {
159
173
  const state = await createWgpuRenderStateForTest();
160
174
  const target = createWgpuRenderTarget(state, 64, 64);
@@ -0,0 +1,165 @@
1
+ import type { RenderTexture } from '@flighthq/types/contract';
2
+ import { RenderTargetTextureSourceKind } from '@flighthq/types/contract';
3
+
4
+ import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
5
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
6
+ import {
7
+ bindWgpuRenderTexture,
8
+ destroyWgpuRenderTexture,
9
+ explainWgpuRenderTexture,
10
+ getWgpuRenderTextureTarget,
11
+ invalidateWgpuRenderTexture,
12
+ isWgpuRenderTextureReady,
13
+ renderIntoWgpuRenderTexture,
14
+ setWgpuRenderTextureGuard,
15
+ writeWgpuRenderTextureTarget,
16
+ } from './wgpuRenderTexture';
17
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
18
+
19
+ beforeAll(() => {
20
+ installWgpuMock();
21
+ });
22
+
23
+ function texture(): RenderTexture {
24
+ return {
25
+ colorSpace: 'linear',
26
+ flipX: false,
27
+ flipY: false,
28
+ sampler: {
29
+ anisotropy: 1,
30
+ magFilter: 'linear',
31
+ minFilter: 'linear',
32
+ mipmaps: false,
33
+ wrapU: 'clamp-to-edge',
34
+ wrapV: 'clamp-to-edge',
35
+ },
36
+ dimension: '2d',
37
+ source: { height: 8, kind: RenderTargetTextureSourceKind, version: 0, width: 8 },
38
+ uvOffset: { x: 0, y: 0 },
39
+ uvRotation: 0,
40
+ uvScale: { x: 1, y: 1 },
41
+ version: 0,
42
+ } as unknown as RenderTexture;
43
+ }
44
+
45
+ describe('bindWgpuRenderTexture', () => {
46
+ it('returns null before render and the state-owned target after render', async () => {
47
+ const state = await createWgpuRenderStateForTest();
48
+ const renderTexture = texture();
49
+ expect(bindWgpuRenderTexture(state, renderTexture)).toBeNull();
50
+
51
+ renderWgpuBackground(state);
52
+ renderIntoWgpuRenderTexture(state, renderTexture, () => {});
53
+ expect(bindWgpuRenderTexture(state, renderTexture)).not.toBeNull();
54
+ submitWgpuRenderPass(state);
55
+ });
56
+ });
57
+
58
+ describe('destroyWgpuRenderTexture', () => {
59
+ it('destroys and removes the hidden target', async () => {
60
+ const state = await createWgpuRenderStateForTest();
61
+ const renderTexture = texture();
62
+ renderWgpuBackground(state);
63
+ renderIntoWgpuRenderTexture(state, renderTexture, () => {});
64
+ const entry = getWgpuRenderStateRuntime(state).wgpuRenderTextureCache!.get(renderTexture)!;
65
+ const destroy = vi.spyOn(entry.target.texture, 'destroy');
66
+
67
+ destroyWgpuRenderTexture(state, renderTexture);
68
+ expect(destroy).toHaveBeenCalled();
69
+ expect(bindWgpuRenderTexture(state, renderTexture)).toBeNull();
70
+ submitWgpuRenderPass(state);
71
+ });
72
+ });
73
+
74
+ describe('explainWgpuRenderTexture', () => {
75
+ it('reports source dimensions and lifecycle status before allocation', async () => {
76
+ const state = await createWgpuRenderStateForTest();
77
+ expect(explainWgpuRenderTexture(state, texture())).toEqual({ height: 8, status: 'unrendered', width: 8 });
78
+ });
79
+ });
80
+
81
+ describe('getWgpuRenderTextureTarget', () => {
82
+ it('returns the hidden target only after a completed write', async () => {
83
+ const state = await createWgpuRenderStateForTest();
84
+ const renderTexture = texture();
85
+
86
+ expect(getWgpuRenderTextureTarget(state, renderTexture)).toBeNull();
87
+ writeWgpuRenderTextureTarget(state, renderTexture, () => {});
88
+ expect(getWgpuRenderTextureTarget(state, renderTexture)).not.toBeNull();
89
+ });
90
+ });
91
+
92
+ describe('invalidateWgpuRenderTexture', () => {
93
+ it('makes published content unavailable without destroying its target', async () => {
94
+ const state = await createWgpuRenderStateForTest();
95
+ const renderTexture = texture();
96
+ writeWgpuRenderTextureTarget(state, renderTexture, () => {});
97
+ const target = getWgpuRenderTextureTarget(state, renderTexture)!;
98
+ const destroy = vi.spyOn(target.texture, 'destroy');
99
+
100
+ invalidateWgpuRenderTexture(state, renderTexture);
101
+
102
+ expect(getWgpuRenderTextureTarget(state, renderTexture)).toBeNull();
103
+ expect(destroy).not.toHaveBeenCalled();
104
+ });
105
+ });
106
+
107
+ describe('isWgpuRenderTextureReady', () => {
108
+ it('tracks the successful rendered-content transition', async () => {
109
+ const state = await createWgpuRenderStateForTest();
110
+ const renderTexture = texture();
111
+ expect(isWgpuRenderTextureReady(state, renderTexture)).toBe(false);
112
+ renderWgpuBackground(state);
113
+ renderIntoWgpuRenderTexture(state, renderTexture, () => {});
114
+ expect(isWgpuRenderTextureReady(state, renderTexture)).toBe(true);
115
+ submitWgpuRenderPass(state);
116
+ });
117
+ });
118
+
119
+ describe('renderIntoWgpuRenderTexture', () => {
120
+ it('restores the enclosing pass and bumps version after success', async () => {
121
+ const state = await createWgpuRenderStateForTest();
122
+ const renderTexture = texture();
123
+ renderWgpuBackground(state);
124
+ const enclosingPass = getWgpuRenderStateRuntime(state).renderPass;
125
+
126
+ renderIntoWgpuRenderTexture(state, renderTexture, () => {});
127
+
128
+ expect(renderTexture.version).toBe(1);
129
+ expect(renderTexture.colorSpace).toBe('linear');
130
+ expect(getWgpuRenderStateRuntime(state).renderPass).not.toBeNull();
131
+ expect(getWgpuRenderStateRuntime(state).renderPass).not.toBe(enclosingPass);
132
+ submitWgpuRenderPass(state);
133
+ });
134
+ });
135
+
136
+ describe('setWgpuRenderTextureGuard', () => {
137
+ it('installs and removes the lifecycle diagnostic callback', async () => {
138
+ const state = await createWgpuRenderStateForTest();
139
+ const renderTexture = texture();
140
+ const guard = vi.fn();
141
+
142
+ setWgpuRenderTextureGuard(state, guard);
143
+ bindWgpuRenderTexture(state, renderTexture);
144
+ expect(guard).toHaveBeenCalledOnce();
145
+
146
+ setWgpuRenderTextureGuard(state, null);
147
+ bindWgpuRenderTexture(state, renderTexture);
148
+ expect(guard).toHaveBeenCalledOnce();
149
+ });
150
+ });
151
+
152
+ describe('writeWgpuRenderTextureTarget', () => {
153
+ it('passes the hidden target to the producer and publishes the completed write', async () => {
154
+ const state = await createWgpuRenderStateForTest();
155
+ const renderTexture = texture();
156
+ const producer = vi.fn();
157
+
158
+ writeWgpuRenderTextureTarget(state, renderTexture, producer);
159
+
160
+ expect(producer).toHaveBeenCalledOnce();
161
+ expect(producer.mock.calls[0][0]).toBe(getWgpuRenderTextureTarget(state, renderTexture));
162
+ expect(renderTexture.version).toBe(1);
163
+ expect(isWgpuRenderTextureReady(state, renderTexture)).toBe(true);
164
+ });
165
+ });
@@ -0,0 +1,146 @@
1
+ import {
2
+ explainWgpuRenderTexture,
3
+ getWgpuRenderTextureTarget,
4
+ isWgpuRenderTextureReady,
5
+ writeWgpuRenderTextureTarget,
6
+ } from './wgpuRenderTexture';
7
+ import {
8
+ acquireWgpuRenderTexture,
9
+ createWgpuRenderTexturePool,
10
+ destroyWgpuRenderTexturePool,
11
+ releaseWgpuRenderTexture,
12
+ withWgpuRenderTextures,
13
+ } from './wgpuRenderTexturePool';
14
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
15
+
16
+ beforeAll(() => installWgpuMock());
17
+
18
+ describe('acquireWgpuRenderTexture', () => {
19
+ it('reuses a released handle and reapplies dimensions before the next write', async () => {
20
+ const state = await createWgpuRenderStateForTest();
21
+ const pool = createWgpuRenderTexturePool();
22
+ const first = acquireWgpuRenderTexture(state, pool, { width: 32, height: 16 });
23
+ writeWgpuRenderTextureTarget(state, first, () => {});
24
+ releaseWgpuRenderTexture(state, pool, first);
25
+
26
+ expect(explainWgpuRenderTexture(state, first).status).toBe('released');
27
+ expect(isWgpuRenderTextureReady(state, first)).toBe(false);
28
+
29
+ const second = acquireWgpuRenderTexture(state, pool, { width: 80, height: 48 });
30
+ expect(second).toBe(first);
31
+ expect(second.source.width).toBe(80);
32
+ expect(second.source.height).toBe(48);
33
+ expect(explainWgpuRenderTexture(state, second).status).toBe('unrendered');
34
+
35
+ writeWgpuRenderTextureTarget(state, second, () => {});
36
+ expect(explainWgpuRenderTexture(state, second)).toEqual({ width: 80, height: 48, status: 'ready' });
37
+ });
38
+
39
+ it('resets a released slab view before exposing the next logical target', async () => {
40
+ const state = await createWgpuRenderStateForTest();
41
+ const pool = createWgpuRenderTexturePool();
42
+ const slabView = acquireWgpuRenderTexture(state, pool, { width: 720, height: 480 });
43
+ slabView.flipX = true;
44
+ slabView.flipY = true;
45
+ slabView.uvOffset.x = 140 / 720;
46
+ slabView.uvOffset.y = 160 / 480;
47
+ slabView.uvRotation = Math.PI / 2;
48
+ slabView.uvScale.x = 100 / 720;
49
+ slabView.uvScale.y = 80 / 480;
50
+ releaseWgpuRenderTexture(state, pool, slabView);
51
+
52
+ const logicalTarget = acquireWgpuRenderTexture(state, pool, { width: 100, height: 80 });
53
+
54
+ expect(logicalTarget).toBe(slabView);
55
+ expect(logicalTarget).toMatchObject({
56
+ flipX: false,
57
+ flipY: false,
58
+ uvOffset: { x: 0, y: 0 },
59
+ uvRotation: 0,
60
+ uvScale: { x: 1, y: 1 },
61
+ });
62
+ });
63
+
64
+ it('does not allow a pool to cross GPU devices', async () => {
65
+ const a = await createWgpuRenderStateForTest();
66
+ const b = await createWgpuRenderStateForTest();
67
+ const pool = createWgpuRenderTexturePool();
68
+ acquireWgpuRenderTexture(a, pool, { width: 8, height: 8 });
69
+
70
+ expect(() => acquireWgpuRenderTexture(b, pool, { width: 8, height: 8 })).toThrow('cannot cross GPU devices');
71
+ });
72
+ });
73
+
74
+ describe('createWgpuRenderTexturePool', () => {
75
+ it('creates an empty device-unbound pool', () => {
76
+ const pool = createWgpuRenderTexturePool();
77
+ expect(pool.device).toBeNull();
78
+ expect(pool.destroyed).toBe(false);
79
+ expect(pool.free).toEqual([]);
80
+ expect(pool.leased.size).toBe(0);
81
+ });
82
+ });
83
+
84
+ describe('destroyWgpuRenderTexturePool', () => {
85
+ it('destroys hidden targets for free and outstanding leases', async () => {
86
+ const state = await createWgpuRenderStateForTest();
87
+ const pool = createWgpuRenderTexturePool();
88
+ const free = acquireWgpuRenderTexture(state, pool, { width: 8, height: 8 });
89
+ const leased = acquireWgpuRenderTexture(state, pool, { width: 16, height: 16 });
90
+ writeWgpuRenderTextureTarget(state, free, () => {});
91
+ writeWgpuRenderTextureTarget(state, leased, () => {});
92
+ const destroyFree = vi.spyOn(getWgpuRenderTextureTarget(state, free)!.texture, 'destroy');
93
+ const destroyLeased = vi.spyOn(getWgpuRenderTextureTarget(state, leased)!.texture, 'destroy');
94
+ releaseWgpuRenderTexture(state, pool, free);
95
+
96
+ destroyWgpuRenderTexturePool(state, pool);
97
+
98
+ expect(destroyFree).toHaveBeenCalledOnce();
99
+ expect(destroyLeased).toHaveBeenCalledOnce();
100
+ expect(pool.free).toHaveLength(0);
101
+ expect(pool.leased.size).toBe(0);
102
+ });
103
+
104
+ it('rejects acquisition after destruction', async () => {
105
+ const state = await createWgpuRenderStateForTest();
106
+ const pool = createWgpuRenderTexturePool();
107
+ destroyWgpuRenderTexturePool(state, pool);
108
+ expect(() => acquireWgpuRenderTexture(state, pool, { width: 8, height: 8 })).toThrow('has been destroyed');
109
+ });
110
+ });
111
+
112
+ describe('releaseWgpuRenderTexture', () => {
113
+ it('rejects a double release', async () => {
114
+ const state = await createWgpuRenderStateForTest();
115
+ const pool = createWgpuRenderTexturePool();
116
+ const texture = acquireWgpuRenderTexture(state, pool, { width: 8, height: 8 });
117
+ releaseWgpuRenderTexture(state, pool, texture);
118
+
119
+ expect(() => releaseWgpuRenderTexture(state, pool, texture)).toThrow('texture is not leased');
120
+ });
121
+ });
122
+
123
+ describe('withWgpuRenderTextures', () => {
124
+ it('releases every lease when the bracket callback throws', async () => {
125
+ const state = await createWgpuRenderStateForTest();
126
+ const pool = createWgpuRenderTexturePool();
127
+
128
+ expect(() =>
129
+ withWgpuRenderTextures(
130
+ state,
131
+ pool,
132
+ [
133
+ { width: 8, height: 8 },
134
+ { width: 8, height: 8 },
135
+ { width: 8, height: 8 },
136
+ ],
137
+ () => {
138
+ throw new Error('capture failed');
139
+ },
140
+ ),
141
+ ).toThrow('capture failed');
142
+
143
+ expect(pool.leased.size).toBe(0);
144
+ expect(pool.free).toHaveLength(3);
145
+ });
146
+ });
@@ -1,5 +1,5 @@
1
1
  import { getOrCreateRenderProxy2D } from '@flighthq/render/contract';
2
- import { createBitmap } from '@flighthq/scene2d/contract';
2
+ import { createSprite } from '@flighthq/scene2d/contract';
3
3
  import type { WgpuBitmapShader } from '@flighthq/types/contract';
4
4
 
5
5
  import { getWgpuRenderStateRuntime } from './wgpuRenderState';
@@ -13,7 +13,7 @@ beforeAll(() => {
13
13
  describe('getWgpuShader', () => {
14
14
  it('returns undefined when no shader is bound', async () => {
15
15
  const state = await createWgpuRenderStateForTest();
16
- const bitmap = createBitmap();
16
+ const bitmap = createSprite();
17
17
  const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
18
18
  expect(getWgpuShader(renderProxy)).toBeUndefined();
19
19
  });
@@ -22,7 +22,7 @@ describe('getWgpuShader', () => {
22
22
  describe('resolveWgpuShader', () => {
23
23
  it('returns null when no shader feature is enabled', async () => {
24
24
  const state = await createWgpuRenderStateForTest();
25
- const bitmap = createBitmap();
25
+ const bitmap = createSprite();
26
26
  const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
27
27
  expect(resolveWgpuShader(state, renderProxy)).toBeNull();
28
28
  });
@@ -31,7 +31,7 @@ describe('resolveWgpuShader', () => {
31
31
  describe('setWgpuShader', () => {
32
32
  it('binds a shader to a display object', async () => {
33
33
  const state = await createWgpuRenderStateForTest();
34
- const bitmap = createBitmap();
34
+ const bitmap = createSprite();
35
35
  const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
36
36
 
37
37
  setWgpuShader(state, bitmap, fakeShader);
@@ -43,7 +43,7 @@ describe('setWgpuShader', () => {
43
43
  it('installs the per-node shader binding resolver', async () => {
44
44
  const state = await createWgpuRenderStateForTest();
45
45
  const runtime = getWgpuRenderStateRuntime(state);
46
- const bitmap = createBitmap();
46
+ const bitmap = createSprite();
47
47
  const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
48
48
  expect(runtime.webgpuShaderBindingResolver).toBeUndefined();
49
49
  setWgpuShader(state, bitmap, fakeShader);
@@ -52,7 +52,7 @@ describe('setWgpuShader', () => {
52
52
 
53
53
  it('removes the binding when shader is null', async () => {
54
54
  const state = await createWgpuRenderStateForTest();
55
- const bitmap = createBitmap();
55
+ const bitmap = createSprite();
56
56
  const fakeShader: WgpuBitmapShader = { pipeline: {} as never, bind: () => {} };
57
57
  setWgpuShader(state, bitmap, fakeShader);
58
58
  setWgpuShader(state, bitmap, null);