@flighthq/render-wgpu 0.3.0 → 0.3.1-next.1041.35b950c

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 (38) hide show
  1. package/dist/explainWgpuTextureResolution.d.ts.map +1 -1
  2. package/dist/explainWgpuTextureResolution.js +2 -1
  3. package/dist/explainWgpuTextureResolution.js.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/wgpuBackground.d.ts +2 -0
  9. package/dist/wgpuBackground.d.ts.map +1 -1
  10. package/dist/wgpuBackground.js +28 -0
  11. package/dist/wgpuBackground.js.map +1 -1
  12. package/dist/wgpuCompressedTexture.d.ts.map +1 -1
  13. package/dist/wgpuCompressedTexture.js +13 -2
  14. package/dist/wgpuCompressedTexture.js.map +1 -1
  15. package/dist/wgpuDraw.d.ts.map +1 -1
  16. package/dist/wgpuDraw.js +12 -3
  17. package/dist/wgpuDraw.js.map +1 -1
  18. package/dist/wgpuMaterialRegistry.d.ts.map +1 -1
  19. package/dist/wgpuMaterialRegistry.js +13 -9
  20. package/dist/wgpuMaterialRegistry.js.map +1 -1
  21. package/dist/wgpuRenderState.d.ts +4 -1
  22. package/dist/wgpuRenderState.d.ts.map +1 -1
  23. package/dist/wgpuRenderState.js +66 -33
  24. package/dist/wgpuRenderState.js.map +1 -1
  25. package/dist/wgpuTestHelper.d.ts.map +1 -1
  26. package/dist/wgpuTestHelper.js +110 -7
  27. package/dist/wgpuTestHelper.js.map +1 -1
  28. package/dist/wgpuTextureResolver.d.ts.map +1 -1
  29. package/dist/wgpuTextureResolver.js +12 -11
  30. package/dist/wgpuTextureResolver.js.map +1 -1
  31. package/package.json +15 -11
  32. package/src/wgpuBackground.test.ts +34 -1
  33. package/src/wgpuCompressedTexture.test.ts +19 -7
  34. package/src/wgpuDraw.test.ts +16 -0
  35. package/src/wgpuMaterialRegistry.test.ts +16 -1
  36. package/src/wgpuRenderState.test.ts +340 -22
  37. package/src/wgpuTestHelper.test.ts +64 -0
  38. package/src/wgpuTextureResolver.test.ts +10 -7
@@ -111,6 +111,22 @@ describe('bindWgpuBitmapTexture', () => {
111
111
  bindWgpuBitmapTexture(state, image);
112
112
  expect(createTexture).toHaveBeenCalled();
113
113
  });
114
+
115
+ it('RETIRES the outgoing texture on a version bump rather than destroying it', async () => {
116
+ const state = await createWgpuRenderStateForTest();
117
+ const image = bitmap(2, 1);
118
+ const outgoing = bindWgpuBitmapTexture(state, image).texture;
119
+
120
+ image.version = 2;
121
+ bindWgpuBitmapTexture(state, image);
122
+
123
+ // A version bump rewrites the cached entry IN PLACE, so a bind group recorded earlier in the frame
124
+ // still points at `outgoing`. The frame's submit is deferred; destroying a texture a recorded command
125
+ // buffer references fails that submit and blanks the whole frame, so it has to wait for post-submit.
126
+ // This guards a hazard with no reproduction of its own — without the test there is nothing to stop
127
+ // the deferral being read as pointless indirection and reverted.
128
+ expect(getWgpuRenderStateRuntime(state).retiredTextures).toContain(outgoing);
129
+ });
114
130
  });
115
131
 
116
132
  describe('bindWgpuCompressedImageTexture', () => {
@@ -8,7 +8,7 @@ import {
8
8
  registerWgpuMaterialRenderer,
9
9
  resolveWgpuMaterialRenderer,
10
10
  } from './wgpuMaterialRegistry';
11
- import { createWgpuRenderStateRuntime } from './wgpuRenderState';
11
+ import { createWgpuRenderStateRuntime, getWgpuRenderStateRuntime } from './wgpuRenderState';
12
12
 
13
13
  const TestKind = 'TestMaterial';
14
14
  const testRenderer: WgpuMaterialRenderer = { instanceFloatCount: 0, getShaderModule: () => ({}) as GPUShaderModule };
@@ -33,8 +33,23 @@ describe('getWgpuMaterialRenderer', () => {
33
33
  describe('registerWgpuMaterialRenderer', () => {
34
34
  it('registers a renderer retrievable by kind', () => {
35
35
  const state = makeState();
36
+ const before = getWgpuRenderStateRuntime(state).registries.materialRenderers;
36
37
  registerWgpuMaterialRenderer(state, TestKind, testRenderer);
37
38
  expect(getWgpuMaterialRenderer(state, TestKind)).toBe(testRenderer);
39
+ expect(getWgpuRenderStateRuntime(state).registries.materialRenderers).not.toBe(before);
40
+ expect(before.entries.size).toBe(0);
41
+ });
42
+
43
+ it('is last-write-wins without mutating the earlier snapshot', () => {
44
+ const state = makeState();
45
+ const replacement: WgpuMaterialRenderer = { instanceFloatCount: 0, getShaderModule: () => ({}) as GPUShaderModule };
46
+ registerWgpuMaterialRenderer(state, TestKind, testRenderer);
47
+ const before = getWgpuRenderStateRuntime(state).registries.materialRenderers;
48
+
49
+ registerWgpuMaterialRenderer(state, TestKind, replacement);
50
+
51
+ expect(getWgpuMaterialRenderer(state, TestKind)).toBe(replacement);
52
+ expect(before.entries.get(TestKind)).toEqual({ state: 'bound', value: testRenderer });
38
53
  });
39
54
  });
40
55
 
@@ -1,22 +1,43 @@
1
1
  import { createEntity } from '@flighthq/entity/contract';
2
+ import {
3
+ createKeyedTable,
4
+ getRegistryTableEntry,
5
+ hasRegistryTableEntry,
6
+ withRegistryTableEntry,
7
+ } from '@flighthq/registry/contract';
2
8
  import {
3
9
  copyAllRenderersFromRenderState,
10
+ enableColorAdjustmentGuards,
11
+ enableColorAdjustments,
12
+ getColorAdjustmentUnsupportedGuard,
4
13
  getRenderStateRuntime,
5
14
  prepareScene2DRender,
6
15
  registerRenderer,
7
16
  } from '@flighthq/render/contract';
8
17
  import { createDisplayObject } from '@flighthq/scene2d/contract';
18
+ import type {
19
+ RenderEffectPaddingResolver,
20
+ RenderRootGuard,
21
+ RenderState,
22
+ WgpuColorAdjustmentMaterialFeature,
23
+ WgpuColorAdjustmentMaterialFeatureGuard,
24
+ } from '@flighthq/types/contract';
25
+ import { RegistryEntryState } from '@flighthq/types/contract';
9
26
 
10
27
  import { beginWgpuFrame } from './wgpuBackground';
28
+ import { registerWgpuCompressedTextureDecoder, registerWgpuCompressedTextureUpload } from './wgpuCompressedTexture';
11
29
  import { registerWgpuMaterialRenderer } from './wgpuMaterialRegistry';
12
30
  import {
13
31
  copyWgpuRenderStateRegistrations,
14
32
  createWgpuOffscreenRenderState,
15
33
  createWgpuRenderStateRuntime,
16
34
  destroyWgpuRenderState,
35
+ getWgpuColorAdjustmentMaterialFeature,
36
+ getWgpuColorAdjustmentMaterialFeatureGuard,
17
37
  getWgpuRenderStateRuntime,
18
38
  getWgpuSampler,
19
39
  isWgpuSupported,
40
+ resolveWgpuApplyBlendMode,
20
41
  } from './wgpuRenderState';
21
42
  import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
22
43
  import { registerWgpuTextureResolver } from './wgpuTextureResolver';
@@ -25,20 +46,113 @@ beforeAll(() => {
25
46
  installWgpuMock();
26
47
  });
27
48
 
49
+ function getPaddingResolver(state: RenderState, kind: string): RenderEffectPaddingResolver | null {
50
+ const table = getRenderStateRuntime(state).registries.effectPaddingResolvers;
51
+ return table === undefined ? null : getRegistryTableEntry(table, kind);
52
+ }
53
+
54
+ function registerPaddingResolver(state: RenderState, kind: string, resolver: RenderEffectPaddingResolver): void {
55
+ const runtime = getRenderStateRuntime(state);
56
+ runtime.registries.effectPaddingResolvers = withRegistryTableEntry(
57
+ runtime.registries.effectPaddingResolvers ??
58
+ createKeyedTable<RenderEffectPaddingResolver>('RenderEffectPaddingResolver', 'Zero'),
59
+ kind,
60
+ resolver,
61
+ );
62
+ }
63
+
28
64
  describe('copyWgpuRenderStateRegistrations', () => {
29
65
  it('copies late Wgpu registrations only when explicitly requested', async () => {
30
66
  const screen = await createWgpuRenderStateForTest();
31
67
  const offscreen = createWgpuOffscreenRenderState(screen);
68
+ const materialRenderer = { instanceFloatCount: 0, getShaderModule: vi.fn() } as never;
69
+ const offscreenMaterialRenderer = { instanceFloatCount: 0, getShaderModule: vi.fn() } as never;
70
+ const decoder = vi.fn(() => new Uint8ClampedArray(4));
32
71
  const resolver = vi.fn(() => null);
72
+ registerWgpuCompressedTextureDecoder(screen, decoder);
73
+ registerWgpuCompressedTextureUpload(screen);
74
+ registerWgpuMaterialRenderer(screen, 'acme.LateMaterial', materialRenderer);
33
75
  registerWgpuTextureResolver(screen, 'acme.LateTexture', resolver);
34
76
 
35
- expect(getWgpuRenderStateRuntime(offscreen).wgpuTextureResolverRegistry?.has('acme.LateTexture')).not.toBe(true);
77
+ expect(
78
+ hasRegistryTableEntry(getWgpuRenderStateRuntime(offscreen).registries.materialRenderers, 'acme.LateMaterial'),
79
+ ).toBe(false);
80
+ expect(
81
+ hasRegistryTableEntry(getWgpuRenderStateRuntime(offscreen).registries.textureResolvers, 'acme.LateTexture'),
82
+ ).toBe(false);
83
+ expect(getWgpuRenderStateRuntime(offscreen).registries.compressedTextureDecoder.entry).toBeNull();
84
+ expect(getWgpuRenderStateRuntime(offscreen).registries.compressedTextureUpload.entry).toBeNull();
36
85
  copyWgpuRenderStateRegistrations(offscreen, screen);
37
- expect(getWgpuRenderStateRuntime(offscreen).wgpuTextureResolverRegistry?.get('acme.LateTexture')).toBe(resolver);
86
+ expect(
87
+ getRegistryTableEntry(getWgpuRenderStateRuntime(offscreen).registries.materialRenderers, 'acme.LateMaterial'),
88
+ ).toBe(materialRenderer);
89
+ expect(
90
+ getRegistryTableEntry(getWgpuRenderStateRuntime(offscreen).registries.textureResolvers, 'acme.LateTexture'),
91
+ ).toBe(resolver);
92
+ expect(getWgpuRenderStateRuntime(offscreen).registries.compressedTextureDecoder).toBe(
93
+ getWgpuRenderStateRuntime(screen).registries.compressedTextureDecoder,
94
+ );
95
+ expect(getWgpuRenderStateRuntime(offscreen).registries.compressedTextureDecoder.entry).toEqual({
96
+ state: RegistryEntryState.Bound,
97
+ value: decoder,
98
+ });
99
+ expect(getWgpuRenderStateRuntime(offscreen).registries.compressedTextureUpload).toBe(
100
+ getWgpuRenderStateRuntime(screen).registries.compressedTextureUpload,
101
+ );
102
+ registerWgpuMaterialRenderer(offscreen, 'acme.LateMaterial', offscreenMaterialRenderer);
103
+ registerWgpuCompressedTextureDecoder(offscreen, null);
104
+ registerWgpuCompressedTextureUpload(offscreen, null);
105
+ registerWgpuTextureResolver(offscreen, 'acme.LateTexture', null);
106
+ expect(
107
+ getRegistryTableEntry(getWgpuRenderStateRuntime(offscreen).registries.materialRenderers, 'acme.LateMaterial'),
108
+ ).toBe(offscreenMaterialRenderer);
109
+ expect(
110
+ getRegistryTableEntry(getWgpuRenderStateRuntime(screen).registries.materialRenderers, 'acme.LateMaterial'),
111
+ ).toBe(materialRenderer);
112
+ expect(
113
+ hasRegistryTableEntry(getWgpuRenderStateRuntime(offscreen).registries.textureResolvers, 'acme.LateTexture'),
114
+ ).toBe(false);
115
+ expect(
116
+ getRegistryTableEntry(getWgpuRenderStateRuntime(screen).registries.textureResolvers, 'acme.LateTexture'),
117
+ ).toBe(resolver);
118
+ expect(getWgpuRenderStateRuntime(offscreen).registries.compressedTextureDecoder.entry).toBeNull();
119
+ expect(getWgpuRenderStateRuntime(screen).registries.compressedTextureDecoder.entry?.state).toBe(
120
+ RegistryEntryState.Bound,
121
+ );
122
+ expect(getWgpuRenderStateRuntime(offscreen).registries.compressedTextureUpload.entry).toBeNull();
123
+ expect(getWgpuRenderStateRuntime(screen).registries.compressedTextureUpload.entry?.state).toBe(
124
+ RegistryEntryState.Bound,
125
+ );
38
126
  });
39
127
  });
40
128
 
41
129
  describe('createWgpuOffscreenRenderState', () => {
130
+ it('resolves late screen blend-mode wiring explicitly until locally overridden', async () => {
131
+ const screen = await createWgpuRenderStateForTest();
132
+ const offscreen = createWgpuOffscreenRenderState(screen);
133
+ const nested = createWgpuOffscreenRenderState(offscreen);
134
+ const screenHook = vi.fn();
135
+ const laterScreenHook = vi.fn();
136
+ const offscreenHook = vi.fn();
137
+
138
+ expect(offscreen.applyBlendMode).toBeNull();
139
+ expect(Object.getOwnPropertyDescriptor(offscreen, 'applyBlendMode')?.get).toBeUndefined();
140
+ expect(resolveWgpuApplyBlendMode(offscreen)).toBeNull();
141
+ screen.applyBlendMode = screenHook;
142
+ expect(offscreen.applyBlendMode).toBeNull();
143
+ expect(resolveWgpuApplyBlendMode(offscreen)).toBe(screenHook);
144
+ expect(resolveWgpuApplyBlendMode(nested)).toBe(screenHook);
145
+
146
+ offscreen.applyBlendMode = offscreenHook;
147
+ screen.applyBlendMode = laterScreenHook;
148
+ expect(resolveWgpuApplyBlendMode(offscreen)).toBe(offscreenHook);
149
+ expect(resolveWgpuApplyBlendMode(nested)).toBe(offscreenHook);
150
+
151
+ copyWgpuRenderStateRegistrations(offscreen, screen);
152
+ expect(offscreen.applyBlendMode).toBeNull();
153
+ expect(resolveWgpuApplyBlendMode(offscreen)).toBe(laterScreenHook);
154
+ });
155
+
42
156
  it('shares device resources while snapshotting independent registration policy', async () => {
43
157
  const screen = await createWgpuRenderStateForTest();
44
158
  const renderer = { createData: () => null, submit: vi.fn() };
@@ -46,11 +160,43 @@ describe('createWgpuOffscreenRenderState', () => {
46
160
  const paddingResolver = vi.fn(() => ({ bottom: 1, left: 1, right: 1, top: 1 }));
47
161
  const textureResolver = vi.fn(() => null);
48
162
  const effectRunner = vi.fn();
163
+ const colorAdjustmentFeature: WgpuColorAdjustmentMaterialFeature = {
164
+ fragmentShaderChunk: '',
165
+ matrixFragmentShaderChunk: '',
166
+ record: vi.fn(),
167
+ resolveFlush: vi.fn(() => null),
168
+ };
169
+ const colorAdjustmentFeatureGuard: WgpuColorAdjustmentMaterialFeatureGuard = vi.fn();
170
+ const renderRootGuard: RenderRootGuard = vi.fn();
49
171
  registerRenderer(screen, 'acme.Node', renderer);
50
172
  registerWgpuMaterialRenderer(screen, 'acme.Material', materialRenderer);
51
173
  registerWgpuTextureResolver(screen, 'acme.Texture', textureResolver);
52
- getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.Effect', paddingResolver]]);
53
- getWgpuRenderStateRuntime(screen).wgpuRenderEffectRegistry = new Map([['acme.Effect', effectRunner]]);
174
+ enableColorAdjustments(screen);
175
+ enableColorAdjustmentGuards(screen);
176
+ registerPaddingResolver(screen, 'acme.Effect', paddingResolver);
177
+ getWgpuRenderStateRuntime(screen).registries.colorAdjustmentFeature = {
178
+ entry: { state: RegistryEntryState.Bound, value: colorAdjustmentFeature },
179
+ onMiss: 'Disabled',
180
+ registry: 'WgpuColorAdjustmentFeature',
181
+ shape: 'slot',
182
+ };
183
+ getWgpuRenderStateRuntime(screen).registries.colorAdjustmentFeatureGuard = {
184
+ entry: { state: RegistryEntryState.Bound, value: colorAdjustmentFeatureGuard },
185
+ onMiss: 'Disabled',
186
+ registry: 'WgpuColorAdjustmentFeatureGuard',
187
+ shape: 'slot',
188
+ };
189
+ getWgpuRenderStateRuntime(screen).registries.renderRootGuard = {
190
+ entry: { state: RegistryEntryState.Bound, value: renderRootGuard },
191
+ onMiss: 'Disabled',
192
+ registry: 'RenderRootGuard',
193
+ shape: 'slot',
194
+ };
195
+ getWgpuRenderStateRuntime(screen).registries.renderEffects = withRegistryTableEntry(
196
+ getWgpuRenderStateRuntime(screen).registries.renderEffects,
197
+ 'acme.Effect',
198
+ effectRunner as never,
199
+ );
54
200
  getWgpuRenderStateRuntime(screen).wgpuRenderTextureCache = new WeakMap();
55
201
 
56
202
  const offscreen = createWgpuOffscreenRenderState(screen);
@@ -64,18 +210,71 @@ describe('createWgpuOffscreenRenderState', () => {
64
210
  expect(offscreenRuntime.wgpuRenderTextureCache).toBe(screenRuntime.wgpuRenderTextureCache);
65
211
  expect(offscreenRuntime.uniformBindGroupLayout).toBe(screenRuntime.uniformBindGroupLayout);
66
212
  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,
213
+ expect(offscreenRuntime.registries.renderers).toBe(screenRuntime.registries.renderers);
214
+ expect(offscreenRuntime.registries).not.toBe(screenRuntime.registries);
215
+ expect(offscreenRuntime.registries.colorAdjustmentFeature).toBe(screenRuntime.registries.colorAdjustmentFeature);
216
+ expect(getWgpuColorAdjustmentMaterialFeature(offscreen)).toBe(colorAdjustmentFeature);
217
+ expect(offscreenRuntime.registries.colorAdjustmentFeatureGuard).toBe(
218
+ screenRuntime.registries.colorAdjustmentFeatureGuard,
219
+ );
220
+ expect(getWgpuColorAdjustmentMaterialFeatureGuard(offscreen)).toBe(colorAdjustmentFeatureGuard);
221
+ const sharedColorFeatureGuard = offscreenRuntime.registries.colorAdjustmentFeatureGuard;
222
+ screenRuntime.registries.colorAdjustmentFeatureGuard = undefined;
223
+ expect(getWgpuColorAdjustmentMaterialFeatureGuard(screen)).toBeNull();
224
+ expect(offscreenRuntime.registries.colorAdjustmentFeatureGuard).toBe(sharedColorFeatureGuard);
225
+ expect(getWgpuColorAdjustmentMaterialFeatureGuard(offscreen)).toBe(colorAdjustmentFeatureGuard);
226
+ const sharedColorFeature = offscreenRuntime.registries.colorAdjustmentFeature;
227
+ screenRuntime.registries.colorAdjustmentFeature = undefined;
228
+ expect(getWgpuColorAdjustmentMaterialFeature(screen)).toBeNull();
229
+ expect(offscreenRuntime.registries.colorAdjustmentFeature).toBe(sharedColorFeature);
230
+ expect(getWgpuColorAdjustmentMaterialFeature(offscreen)).toBe(colorAdjustmentFeature);
231
+ expect(offscreenRuntime.registries.colorAdjustments).toBe(screenRuntime.registries.colorAdjustments);
232
+ expect(offscreenRuntime.registries.colorAdjustmentUnsupportedGuard).toBe(
233
+ screenRuntime.registries.colorAdjustmentUnsupportedGuard,
234
+ );
235
+ expect(getColorAdjustmentUnsupportedGuard(offscreen)).not.toBeNull();
236
+ const sharedUnsupportedGuard = offscreenRuntime.registries.colorAdjustmentUnsupportedGuard;
237
+ screenRuntime.registries.colorAdjustmentUnsupportedGuard = undefined;
238
+ expect(getColorAdjustmentUnsupportedGuard(screen)).toBeNull();
239
+ expect(offscreenRuntime.registries.colorAdjustmentUnsupportedGuard).toBe(sharedUnsupportedGuard);
240
+ expect(getColorAdjustmentUnsupportedGuard(offscreen)).not.toBeNull();
241
+ expect(offscreenRuntime.registries.renderRootGuard).toBe(screenRuntime.registries.renderRootGuard);
242
+ expect(offscreenRuntime.registries.renderRootGuard?.entry).toEqual({
243
+ state: RegistryEntryState.Bound,
244
+ value: renderRootGuard,
245
+ });
246
+ const sharedRenderRootGuard = offscreenRuntime.registries.renderRootGuard;
247
+ screenRuntime.registries.renderRootGuard = undefined;
248
+ expect(screenRuntime.registries.renderRootGuard).toBeUndefined();
249
+ expect(offscreenRuntime.registries.renderRootGuard).toBe(sharedRenderRootGuard);
250
+ expect(
251
+ getRegistryTableEntry(
252
+ offscreenRuntime.registries.renderRootGuard!,
253
+ offscreenRuntime.registries.renderRootGuard!.registry,
254
+ ),
255
+ ).toBe(renderRootGuard);
256
+ expect(offscreenRuntime.registries.compressedTextureDecoder).toBe(
257
+ screenRuntime.registries.compressedTextureDecoder,
73
258
  );
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);
259
+ expect(offscreenRuntime.registries.compressedTextureUpload).toBe(screenRuntime.registries.compressedTextureUpload);
260
+ expect(offscreenRuntime.registries.customMaterialShaders).toBe(screenRuntime.registries.customMaterialShaders);
261
+ expect(offscreenRuntime.registries.materialRenderers).toBe(screenRuntime.registries.materialRenderers);
262
+ expect(offscreenRuntime.registries.meshMaterialRenderers).toBe(screenRuntime.registries.meshMaterialRenderers);
263
+ expect(offscreenRuntime.registries.modifierSnippets).toBe(screenRuntime.registries.modifierSnippets);
264
+ expect(offscreenRuntime.registries.modifierSnippetRevision).toBe(screenRuntime.registries.modifierSnippetRevision);
265
+ expect(offscreenRuntime.registries.renderEffects).toBe(screenRuntime.registries.renderEffects);
266
+ expect(offscreenRuntime.registries.shapeRasterizer).toBe(screenRuntime.registries.shapeRasterizer);
267
+ expect(offscreenRuntime.registries.strokeTessellator).toBe(screenRuntime.registries.strokeTessellator);
268
+ expect(offscreenRuntime.registries.textureResolvers).toBe(screenRuntime.registries.textureResolvers);
269
+ expect(offscreenRuntime.registries.velocityWriters).toBe(screenRuntime.registries.velocityWriters);
270
+ expect(offscreenRuntime.registries.effectPaddingResolvers).toBe(screenRuntime.registries.effectPaddingResolvers);
271
+ expect(getRegistryTableEntry(offscreenRuntime.registries.renderers, 'acme.Node')).toBe(renderer);
272
+ expect(getRegistryTableEntry(offscreenRuntime.registries.materialRenderers, 'acme.Material')).toBe(
273
+ materialRenderer,
274
+ );
275
+ expect(getRegistryTableEntry(offscreenRuntime.registries.textureResolvers, 'acme.Texture')).toBe(textureResolver);
276
+ expect(getRegistryTableEntry(offscreenRuntime.registries.renderEffects, 'acme.Effect')).toBe(effectRunner);
277
+ expect(getPaddingResolver(offscreen, 'acme.Effect')).toBe(paddingResolver);
79
278
  });
80
279
 
81
280
  it('owns an independent encoder and proxy tree', async () => {
@@ -120,17 +319,17 @@ describe('createWgpuOffscreenRenderState', () => {
120
319
  const renderer = { createData: () => null, submit: vi.fn() };
121
320
  const paddingResolver = vi.fn(() => ({ bottom: 2, left: 2, right: 2, top: 2 }));
122
321
  registerRenderer(screen, 'acme.LateNode', renderer);
123
- getRenderStateRuntime(screen).renderEffectPaddingResolverRegistry = new Map([['acme.LateEffect', paddingResolver]]);
322
+ registerPaddingResolver(screen, 'acme.LateEffect', paddingResolver);
124
323
 
125
- expect(getRenderStateRuntime(offscreen).rendererMap.has('acme.LateNode')).toBe(false);
126
- expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.has('acme.LateEffect')).not.toBe(true);
324
+ expect(hasRegistryTableEntry(getRenderStateRuntime(offscreen).registries.renderers, 'acme.LateNode')).toBe(false);
325
+ expect(getPaddingResolver(offscreen, 'acme.LateEffect')).toBeNull();
127
326
 
128
327
  copyAllRenderersFromRenderState(offscreen, screen);
129
328
  copyWgpuRenderStateRegistrations(offscreen, screen);
130
- expect(getRenderStateRuntime(offscreen).rendererMap.get('acme.LateNode')).toBe(renderer);
131
- expect(getRenderStateRuntime(offscreen).renderEffectPaddingResolverRegistry?.get('acme.LateEffect')).toBe(
132
- paddingResolver,
329
+ expect(getRegistryTableEntry(getRenderStateRuntime(offscreen).registries.renderers, 'acme.LateNode')).toBe(
330
+ renderer,
133
331
  );
332
+ expect(getPaddingResolver(offscreen, 'acme.LateEffect')).toBe(paddingResolver);
134
333
  });
135
334
  });
136
335
 
@@ -168,9 +367,70 @@ describe('createWgpuRenderState', () => {
168
367
  });
169
368
 
170
369
  describe('createWgpuRenderStateRuntime', () => {
171
- it('returns a runtime carrying the base entity-runtime binding slot', () => {
370
+ it('returns a runtime with the base binding slot and empty named registration tables', () => {
172
371
  const runtime = createWgpuRenderStateRuntime();
173
372
  expect(runtime.binding).toBeNull();
373
+ expect(runtime.registries.customMaterialShaders).toMatchObject({
374
+ onMiss: 'Unregistered',
375
+ registry: 'WgpuCustomMaterialShader',
376
+ shape: 'keyed',
377
+ });
378
+ expect(runtime.registries.customMaterialShaders.entries.size).toBe(0);
379
+ expect(runtime.registries.materialRenderers).toMatchObject({
380
+ onMiss: 'StandardMaterial',
381
+ registry: 'WgpuMaterialRenderer',
382
+ shape: 'keyed',
383
+ });
384
+ expect(runtime.registries.materialRenderers.entries.size).toBe(0);
385
+ expect(runtime.registries.meshMaterialRenderers).toMatchObject({
386
+ onMiss: 'StandardMaterial',
387
+ registry: 'WgpuMeshMaterialRenderer',
388
+ shape: 'keyed',
389
+ });
390
+ expect(runtime.registries.meshMaterialRenderers.entries.size).toBe(0);
391
+ expect(runtime.registries.modifierSnippets).toMatchObject({
392
+ onMiss: 'Unregistered',
393
+ registry: 'WgpuModifierSnippet',
394
+ shape: 'keyed',
395
+ });
396
+ expect(runtime.registries.modifierSnippets.entries.size).toBe(0);
397
+ expect(runtime.registries.modifierSnippetRevision).toBe(0);
398
+ expect(runtime.registries.renderEffects).toMatchObject({
399
+ onMiss: 'Unregistered',
400
+ registry: 'WgpuRenderEffect',
401
+ shape: 'keyed',
402
+ });
403
+ expect(runtime.registries.compressedTextureDecoder).toEqual({
404
+ entry: null,
405
+ onMiss: 'Unregistered',
406
+ registry: 'WgpuCompressedTextureDecoder',
407
+ shape: 'slot',
408
+ });
409
+ expect(runtime.registries.colorAdjustments).toBeUndefined();
410
+ expect(runtime.registries.compressedTextureUpload).toEqual({
411
+ entry: null,
412
+ onMiss: 'Unregistered',
413
+ registry: 'WgpuCompressedTextureUpload',
414
+ shape: 'slot',
415
+ });
416
+ expect(runtime.registries.shapeRasterizer).toEqual({
417
+ entry: null,
418
+ onMiss: 'Unregistered',
419
+ registry: 'WgpuShapeRasterizer',
420
+ shape: 'slot',
421
+ });
422
+ expect(runtime.registries.strokeTessellator).toEqual({
423
+ entry: null,
424
+ onMiss: 'Rasterize',
425
+ registry: 'StrokeTessellator',
426
+ shape: 'slot',
427
+ });
428
+ expect(runtime.registries.velocityWriters).toMatchObject({
429
+ onMiss: 'Unregistered',
430
+ registry: 'WgpuVelocityWriter',
431
+ shape: 'keyed',
432
+ });
433
+ expect(runtime.registries.velocityWriters.entries.size).toBe(0);
174
434
  });
175
435
  });
176
436
 
@@ -191,6 +451,55 @@ describe('destroyWgpuRenderState', () => {
191
451
  });
192
452
  });
193
453
 
454
+ describe('getWgpuColorAdjustmentMaterialFeature', () => {
455
+ it('resolves only a bound feature entry', async () => {
456
+ const state = await createWgpuRenderStateForTest();
457
+ const feature: WgpuColorAdjustmentMaterialFeature = {
458
+ fragmentShaderChunk: '',
459
+ matrixFragmentShaderChunk: '',
460
+ record: vi.fn(),
461
+ resolveFlush: vi.fn(() => null),
462
+ };
463
+ const runtime = getWgpuRenderStateRuntime(state);
464
+
465
+ expect(getWgpuColorAdjustmentMaterialFeature(state)).toBeNull();
466
+ runtime.registries.colorAdjustmentFeature = {
467
+ entry: { state: RegistryEntryState.Bound, value: feature },
468
+ onMiss: 'Disabled',
469
+ registry: 'WgpuColorAdjustmentFeature',
470
+ shape: 'slot',
471
+ };
472
+ expect(getWgpuColorAdjustmentMaterialFeature(state)).toBe(feature);
473
+ runtime.registries.colorAdjustmentFeature = {
474
+ ...runtime.registries.colorAdjustmentFeature,
475
+ entry: { state: RegistryEntryState.Tombstoned },
476
+ };
477
+ expect(getWgpuColorAdjustmentMaterialFeature(state)).toBeNull();
478
+ });
479
+ });
480
+
481
+ describe('getWgpuColorAdjustmentMaterialFeatureGuard', () => {
482
+ it('resolves only a bound guard entry', async () => {
483
+ const state = await createWgpuRenderStateForTest();
484
+ const guard: WgpuColorAdjustmentMaterialFeatureGuard = vi.fn();
485
+ const runtime = getWgpuRenderStateRuntime(state);
486
+
487
+ expect(getWgpuColorAdjustmentMaterialFeatureGuard(state)).toBeNull();
488
+ runtime.registries.colorAdjustmentFeatureGuard = {
489
+ entry: { state: RegistryEntryState.Bound, value: guard },
490
+ onMiss: 'Disabled',
491
+ registry: 'WgpuColorAdjustmentFeatureGuard',
492
+ shape: 'slot',
493
+ };
494
+ expect(getWgpuColorAdjustmentMaterialFeatureGuard(state)).toBe(guard);
495
+ runtime.registries.colorAdjustmentFeatureGuard = {
496
+ ...runtime.registries.colorAdjustmentFeatureGuard,
497
+ entry: { state: RegistryEntryState.Tombstoned },
498
+ };
499
+ expect(getWgpuColorAdjustmentMaterialFeatureGuard(state)).toBeNull();
500
+ });
501
+ });
502
+
194
503
  describe('getWgpuRenderStateRuntime', () => {
195
504
  it('returns the runtime attached by createWgpuRenderState', async () => {
196
505
  const state = await createWgpuRenderStateForTest();
@@ -265,3 +574,12 @@ describe('isWgpuSupported', () => {
265
574
  expect(isWgpuSupported()).toBe(true);
266
575
  });
267
576
  });
577
+
578
+ describe('resolveWgpuApplyBlendMode', () => {
579
+ it('returns a hook installed directly on the state', async () => {
580
+ const state = await createWgpuRenderStateForTest();
581
+ const hook = vi.fn();
582
+ state.applyBlendMode = hook;
583
+ expect(resolveWgpuApplyBlendMode(state)).toBe(hook);
584
+ });
585
+ });
@@ -0,0 +1,64 @@
1
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
2
+
3
+ beforeAll(() => {
4
+ installWgpuMock();
5
+ });
6
+
7
+ describe('installWgpuMock', () => {
8
+ it('accepts aligned writeBuffer data using typed-array element units', async () => {
9
+ const state = await createWgpuRenderStateForTest();
10
+ const buffer = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.COPY_DST });
11
+
12
+ expect(() => state.device.queue.writeBuffer(buffer, 0, new Uint16Array([0, 1, 2]), 0, 2)).not.toThrow();
13
+ });
14
+
15
+ it('rejects writeBuffer content whose byte length is not a multiple of four', async () => {
16
+ const state = await createWgpuRenderStateForTest();
17
+ const buffer = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.COPY_DST });
18
+
19
+ expect(() => state.device.queue.writeBuffer(buffer, 0, new Uint16Array([0, 1, 2]))).toThrow(
20
+ expect.objectContaining({ name: 'OperationError' }),
21
+ );
22
+ });
23
+
24
+ it('rejects writeBuffer source ranges and destination offsets outside the API contract', async () => {
25
+ const state = await createWgpuRenderStateForTest();
26
+ const buffer = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.COPY_DST });
27
+
28
+ expect(() => state.device.queue.writeBuffer(buffer, 0, new Uint32Array([0]), 2)).toThrow(
29
+ /data range is out of bounds/,
30
+ );
31
+ expect(() => state.device.queue.writeBuffer(buffer, 2, new Uint32Array([0]))).toThrow(
32
+ /bufferOffset must be a non-negative multiple of 4/,
33
+ );
34
+ });
35
+
36
+ it('rejects writeBuffer destinations without enough COPY_DST storage', async () => {
37
+ const state = await createWgpuRenderStateForTest();
38
+ const tooSmall = state.device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST });
39
+ const wrongUsage = state.device.createBuffer({ size: 8, usage: GPUBufferUsage.VERTEX });
40
+
41
+ expect(() => state.device.queue.writeBuffer(tooSmall, 0, new Uint32Array([0, 1]))).toThrow(
42
+ /destination range exceeds the buffer size/,
43
+ );
44
+ expect(() => state.device.queue.writeBuffer(wrongUsage, 0, new Uint32Array([0]))).toThrow(
45
+ /must have COPY_DST usage/,
46
+ );
47
+ });
48
+
49
+ it('rejects writeTexture data smaller than an rgba8 copy requires', async () => {
50
+ const state = await createWgpuRenderStateForTest();
51
+ const texture = state.device.createTexture({
52
+ size: [2, 2, 1],
53
+ format: 'rgba8unorm',
54
+ usage: GPUTextureUsage.COPY_DST,
55
+ });
56
+
57
+ expect(() =>
58
+ state.device.queue.writeTexture({ texture }, new Uint8Array(15), { bytesPerRow: 8, rowsPerImage: 2 }, [2, 2, 1]),
59
+ ).toThrow(expect.objectContaining({ name: 'OperationError' }));
60
+ expect(() =>
61
+ state.device.queue.writeTexture({ texture }, new Uint8Array(16), { bytesPerRow: 8, rowsPerImage: 2 }, [2, 2, 1]),
62
+ ).not.toThrow();
63
+ });
64
+ });
@@ -1,4 +1,5 @@
1
1
  import { createEntity } from '@flighthq/entity/contract';
2
+ import { getRegistryTableKeys } from '@flighthq/registry/contract';
2
3
  import type { Image, RenderTexture, Texture, TextureSource } from '@flighthq/types/contract';
3
4
  import {
4
5
  BitmapTextureSourceKind,
@@ -83,11 +84,17 @@ function renderTexture(): RenderTexture {
83
84
  return texture as RenderTexture;
84
85
  }
85
86
 
87
+ function registeredTextureSourceKinds(state: Parameters<typeof getWgpuRenderStateRuntime>[0]): string[] {
88
+ const kinds: string[] = [];
89
+ getRegistryTableKeys(kinds, getWgpuRenderStateRuntime(state).registries.textureResolvers);
90
+ return kinds;
91
+ }
92
+
86
93
  describe('registerStandardWgpuTextureResolvers', () => {
87
94
  it('registers bitmap, image, and render texture sources without compressed images', async () => {
88
95
  const state = await createWgpuRenderStateForTest();
89
96
  registerStandardWgpuTextureResolvers(state);
90
- expect([...getWgpuRenderStateRuntime(state).wgpuTextureResolverRegistry!.keys()]).toEqual([
97
+ expect(registeredTextureSourceKinds(state)).toEqual([
91
98
  BitmapTextureSourceKind,
92
99
  ImageTextureSourceKind,
93
100
  RenderTargetTextureSourceKind,
@@ -99,9 +106,7 @@ describe('registerWgpuBitmapTextureResolver', () => {
99
106
  it('registers only the Bitmap source key', async () => {
100
107
  const state = await createWgpuRenderStateForTest();
101
108
  registerWgpuBitmapTextureResolver(state);
102
- expect([...getWgpuRenderStateRuntime(state).wgpuTextureResolverRegistry!.keys()]).toEqual([
103
- BitmapTextureSourceKind,
104
- ]);
109
+ expect(registeredTextureSourceKinds(state)).toEqual([BitmapTextureSourceKind]);
105
110
  });
106
111
  });
107
112
 
@@ -109,9 +114,7 @@ describe('registerWgpuCompressedImageTextureResolver', () => {
109
114
  it('registers only the CompressedImage source key', async () => {
110
115
  const state = await createWgpuRenderStateForTest();
111
116
  registerWgpuCompressedImageTextureResolver(state);
112
- expect([...getWgpuRenderStateRuntime(state).wgpuTextureResolverRegistry!.keys()]).toEqual([
113
- CompressedImageTextureSourceKind,
114
- ]);
117
+ expect(registeredTextureSourceKinds(state)).toEqual([CompressedImageTextureSourceKind]);
115
118
  });
116
119
  });
117
120