@flighthq/render-wgpu 0.3.0-next.906.07cea63 → 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 (95) 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 +15 -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.d.ts +2 -0
  18. package/dist/wgpuBackground.d.ts.map +1 -1
  19. package/dist/wgpuBackground.js +30 -2
  20. package/dist/wgpuBackground.js.map +1 -1
  21. package/dist/wgpuCompressedTexture.d.ts +2 -2
  22. package/dist/wgpuCompressedTexture.d.ts.map +1 -1
  23. package/dist/wgpuCompressedTexture.js +52 -13
  24. package/dist/wgpuCompressedTexture.js.map +1 -1
  25. package/dist/wgpuDraw.d.ts +6 -4
  26. package/dist/wgpuDraw.d.ts.map +1 -1
  27. package/dist/wgpuDraw.js +150 -69
  28. package/dist/wgpuDraw.js.map +1 -1
  29. package/dist/wgpuExternalImageSource.d.ts +3 -0
  30. package/dist/wgpuExternalImageSource.d.ts.map +1 -0
  31. package/dist/wgpuExternalImageSource.js +51 -0
  32. package/dist/wgpuExternalImageSource.js.map +1 -0
  33. package/dist/wgpuExternalTexture.d.ts +4 -0
  34. package/dist/wgpuExternalTexture.d.ts.map +1 -0
  35. package/dist/wgpuExternalTexture.js +62 -0
  36. package/dist/wgpuExternalTexture.js.map +1 -0
  37. package/dist/wgpuMaterialRegistry.d.ts.map +1 -1
  38. package/dist/wgpuMaterialRegistry.js +17 -12
  39. package/dist/wgpuMaterialRegistry.js.map +1 -1
  40. package/dist/wgpuMipmap.d.ts.map +1 -1
  41. package/dist/wgpuMipmap.js +11 -9
  42. package/dist/wgpuMipmap.js.map +1 -1
  43. package/dist/wgpuRenderState.d.ts +15 -3
  44. package/dist/wgpuRenderState.d.ts.map +1 -1
  45. package/dist/wgpuRenderState.js +253 -33
  46. package/dist/wgpuRenderState.js.map +1 -1
  47. package/dist/wgpuRenderTarget.d.ts.map +1 -1
  48. package/dist/wgpuRenderTarget.js +2 -0
  49. package/dist/wgpuRenderTarget.js.map +1 -1
  50. package/dist/wgpuRenderTexture.d.ts +15 -0
  51. package/dist/wgpuRenderTexture.d.ts.map +1 -0
  52. package/dist/wgpuRenderTexture.js +130 -0
  53. package/dist/wgpuRenderTexture.js.map +1 -0
  54. package/dist/wgpuRenderTexturePool.d.ts +7 -0
  55. package/dist/wgpuRenderTexturePool.d.ts.map +1 -0
  56. package/dist/wgpuRenderTexturePool.js +86 -0
  57. package/dist/wgpuRenderTexturePool.js.map +1 -0
  58. package/dist/wgpuShader.d.ts.map +1 -1
  59. package/dist/wgpuShader.js +5 -1
  60. package/dist/wgpuShader.js.map +1 -1
  61. package/dist/wgpuSurface.d.ts +2 -2
  62. package/dist/wgpuSurface.d.ts.map +1 -1
  63. package/dist/wgpuSurface.js +61 -13
  64. package/dist/wgpuSurface.js.map +1 -1
  65. package/dist/wgpuTestHelper.d.ts +1 -0
  66. package/dist/wgpuTestHelper.d.ts.map +1 -1
  67. package/dist/wgpuTestHelper.js +125 -8
  68. package/dist/wgpuTestHelper.js.map +1 -1
  69. package/dist/wgpuTextureResolver.d.ts +9 -0
  70. package/dist/wgpuTextureResolver.d.ts.map +1 -0
  71. package/dist/wgpuTextureResolver.js +68 -0
  72. package/dist/wgpuTextureResolver.js.map +1 -0
  73. package/dist/wgpuTextureUpload.d.ts +2 -2
  74. package/dist/wgpuTextureUpload.d.ts.map +1 -1
  75. package/dist/wgpuTextureUpload.js +5 -10
  76. package/dist/wgpuTextureUpload.js.map +1 -1
  77. package/package.json +17 -7
  78. package/src/enableWgpuTextureResolverGuards.test.ts +65 -0
  79. package/src/explainWgpuTextureResolution.test.ts +36 -0
  80. package/src/wgpuBackground.test.ts +34 -1
  81. package/src/wgpuCompressedTexture.test.ts +35 -40
  82. package/src/wgpuDraw.test.ts +180 -62
  83. package/src/wgpuExternalImageSource.test.ts +89 -0
  84. package/src/wgpuExternalTexture.test.ts +35 -0
  85. package/src/wgpuMaterialRegistry.test.ts +44 -3
  86. package/src/wgpuMipmap.test.ts +10 -2
  87. package/src/wgpuRenderState.test.ts +462 -12
  88. package/src/wgpuRenderTarget.test.ts +14 -0
  89. package/src/wgpuRenderTexture.test.ts +165 -0
  90. package/src/wgpuRenderTexturePool.test.ts +146 -0
  91. package/src/wgpuShaderBinding.test.ts +6 -6
  92. package/src/wgpuSurface.test.ts +95 -3
  93. package/src/wgpuTestHelper.test.ts +64 -0
  94. package/src/wgpuTextureResolver.test.ts +212 -0
  95. package/src/wgpuTextureUpload.test.ts +28 -13
@@ -1,18 +1,21 @@
1
+ import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
2
+ import { enableRenderRegistryGuards, explainRenderRegistryMisses } from '@flighthq/render/contract';
1
3
  import type { Material, WgpuMaterialRenderer, WgpuRenderState } from '@flighthq/types/contract';
2
- import { StandardMaterialKind, EntityRuntimeKey } from '@flighthq/types/contract';
4
+ import { StandardMaterialKind, EntityRuntimeKey, RenderRegistry } from '@flighthq/types/contract';
3
5
 
4
6
  import {
5
7
  getWgpuMaterialRenderer,
6
8
  registerWgpuMaterialRenderer,
7
9
  resolveWgpuMaterialRenderer,
8
10
  } from './wgpuMaterialRegistry';
9
- import { createWgpuRenderStateRuntime } from './wgpuRenderState';
11
+ import { createWgpuRenderStateRuntime, getWgpuRenderStateRuntime } from './wgpuRenderState';
10
12
 
11
13
  const TestKind = 'TestMaterial';
12
14
  const testRenderer: WgpuMaterialRenderer = { instanceFloatCount: 0, getShaderModule: () => ({}) as GPUShaderModule };
13
15
 
14
16
  function makeState(): WgpuRenderState {
15
- const state = {} as WgpuRenderState;
17
+ // `device` is what the guard's message table dispatches on to name the wgpu registrar.
18
+ const state = { device: {} } as WgpuRenderState;
16
19
  state[EntityRuntimeKey] = createWgpuRenderStateRuntime();
17
20
  return state;
18
21
  }
@@ -30,8 +33,23 @@ describe('getWgpuMaterialRenderer', () => {
30
33
  describe('registerWgpuMaterialRenderer', () => {
31
34
  it('registers a renderer retrievable by kind', () => {
32
35
  const state = makeState();
36
+ const before = getWgpuRenderStateRuntime(state).registries.materialRenderers;
33
37
  registerWgpuMaterialRenderer(state, TestKind, testRenderer);
34
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 });
35
53
  });
36
54
  });
37
55
 
@@ -53,4 +71,27 @@ describe('resolveWgpuMaterialRenderer', () => {
53
71
  expect(resolveWgpuMaterialRenderer(state, makeMaterial('Other'))).toBe(testRenderer);
54
72
  expect(resolveWgpuMaterialRenderer(state, null)).toBe(testRenderer);
55
73
  });
74
+
75
+ it('reports the missing kind against the wgpu registrar, not the gl one', () => {
76
+ const state = makeState();
77
+ enableRenderRegistryGuards(state);
78
+ const sink = createMemoryLogSink(4);
79
+ addLogSink(sink.sink);
80
+
81
+ try {
82
+ resolveWgpuMaterialRenderer(state, makeMaterial(TestKind));
83
+
84
+ expect(explainRenderRegistryMisses(state).misses).toEqual([
85
+ { kind: TestKind, registry: RenderRegistry.MaterialRenderer },
86
+ ]);
87
+ expect(getMemoryLogSinkEntries(sink)[0]?.data).toMatchObject({
88
+ kind: TestKind,
89
+ message:
90
+ 'resolveWgpuMaterialRenderer: material kind has no registered renderer, so nodes using it do not draw — call registerWgpuMaterialRenderer(state, kind, renderer)',
91
+ registry: RenderRegistry.MaterialRenderer,
92
+ });
93
+ } finally {
94
+ removeLogSink(sink.sink);
95
+ }
96
+ });
56
97
  });
@@ -53,10 +53,18 @@ describe('generateWgpuMipmaps', () => {
53
53
  const state = await createWgpuRenderStateForTest();
54
54
  spyEncoderPasses(state);
55
55
  generateWgpuMipmaps(state, makeTextureSpy(), 4, 4, 'rgba8unorm');
56
- const pipeline = getWgpuRenderStateRuntime(state).mipmapPipeline;
56
+ const pipeline = getWgpuRenderStateRuntime(state).mipmapPipelineCache.get('rgba8unorm')?.pipeline;
57
57
  expect(pipeline).toBeDefined();
58
58
  generateWgpuMipmaps(state, makeTextureSpy(), 8, 8, 'rgba8unorm');
59
- expect(getWgpuRenderStateRuntime(state).mipmapPipeline).toBe(pipeline);
59
+ expect(getWgpuRenderStateRuntime(state).mipmapPipelineCache.get('rgba8unorm')?.pipeline).toBe(pipeline);
60
+ });
61
+
62
+ it('keeps linear and sRGB downsample pipelines distinct', async () => {
63
+ const state = await createWgpuRenderStateForTest();
64
+ spyEncoderPasses(state);
65
+ generateWgpuMipmaps(state, makeTextureSpy(), 4, 4, 'rgba8unorm');
66
+ generateWgpuMipmaps(state, makeTextureSpy(), 4, 4, 'rgba8unorm-srgb');
67
+ expect(getWgpuRenderStateRuntime(state).mipmapPipelineCache.size).toBe(2);
60
68
  });
61
69
  });
62
70
 
@@ -1,16 +1,338 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
1
2
  import {
3
+ createKeyedTable,
4
+ getRegistryTableEntry,
5
+ hasRegistryTableEntry,
6
+ withRegistryTableEntry,
7
+ } from '@flighthq/registry/contract';
8
+ import {
9
+ copyAllRenderersFromRenderState,
10
+ enableColorAdjustmentGuards,
11
+ enableColorAdjustments,
12
+ getColorAdjustmentUnsupportedGuard,
13
+ getRenderStateRuntime,
14
+ prepareScene2DRender,
15
+ registerRenderer,
16
+ } from '@flighthq/render/contract';
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';
26
+
27
+ import { beginWgpuFrame } from './wgpuBackground';
28
+ import { registerWgpuCompressedTextureDecoder, registerWgpuCompressedTextureUpload } from './wgpuCompressedTexture';
29
+ import { registerWgpuMaterialRenderer } from './wgpuMaterialRegistry';
30
+ import {
31
+ copyWgpuRenderStateRegistrations,
32
+ createWgpuOffscreenRenderState,
2
33
  createWgpuRenderStateRuntime,
3
34
  destroyWgpuRenderState,
35
+ getWgpuColorAdjustmentMaterialFeature,
36
+ getWgpuColorAdjustmentMaterialFeatureGuard,
4
37
  getWgpuRenderStateRuntime,
5
38
  getWgpuSampler,
6
39
  isWgpuSupported,
40
+ resolveWgpuApplyBlendMode,
7
41
  } from './wgpuRenderState';
8
42
  import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
43
+ import { registerWgpuTextureResolver } from './wgpuTextureResolver';
9
44
 
10
45
  beforeAll(() => {
11
46
  installWgpuMock();
12
47
  });
13
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
+
64
+ describe('copyWgpuRenderStateRegistrations', () => {
65
+ it('copies late Wgpu registrations only when explicitly requested', async () => {
66
+ const screen = await createWgpuRenderStateForTest();
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));
71
+ const resolver = vi.fn(() => null);
72
+ registerWgpuCompressedTextureDecoder(screen, decoder);
73
+ registerWgpuCompressedTextureUpload(screen);
74
+ registerWgpuMaterialRenderer(screen, 'acme.LateMaterial', materialRenderer);
75
+ registerWgpuTextureResolver(screen, 'acme.LateTexture', resolver);
76
+
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();
85
+ copyWgpuRenderStateRegistrations(offscreen, screen);
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
+ );
126
+ });
127
+ });
128
+
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
+
156
+ it('shares device resources while snapshotting independent registration policy', async () => {
157
+ const screen = await createWgpuRenderStateForTest();
158
+ const renderer = { createData: () => null, submit: vi.fn() };
159
+ const materialRenderer = { instanceFloatCount: 0, getShaderModule: vi.fn() } as never;
160
+ const paddingResolver = vi.fn(() => ({ bottom: 1, left: 1, right: 1, top: 1 }));
161
+ const textureResolver = vi.fn(() => null);
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();
171
+ registerRenderer(screen, 'acme.Node', renderer);
172
+ registerWgpuMaterialRenderer(screen, 'acme.Material', materialRenderer);
173
+ registerWgpuTextureResolver(screen, 'acme.Texture', textureResolver);
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
+ );
200
+ getWgpuRenderStateRuntime(screen).wgpuRenderTextureCache = new WeakMap();
201
+
202
+ const offscreen = createWgpuOffscreenRenderState(screen);
203
+ const screenRuntime = getWgpuRenderStateRuntime(screen);
204
+ const offscreenRuntime = getWgpuRenderStateRuntime(offscreen);
205
+
206
+ expect(offscreen.device).toBe(screen.device);
207
+ expect(offscreen.canvas).toBe(screen.canvas);
208
+ expect(offscreenRuntime.pipelineCache).toBe(screenRuntime.pipelineCache);
209
+ expect(offscreenRuntime.textureCache).toBe(screenRuntime.textureCache);
210
+ expect(offscreenRuntime.wgpuRenderTextureCache).toBe(screenRuntime.wgpuRenderTextureCache);
211
+ expect(offscreenRuntime.uniformBindGroupLayout).toBe(screenRuntime.uniformBindGroupLayout);
212
+ expect(offscreenRuntime.uniformBuffer).not.toBe(screenRuntime.uniformBuffer);
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,
258
+ );
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);
278
+ });
279
+
280
+ it('owns an independent encoder and proxy tree', async () => {
281
+ const screen = await createWgpuRenderStateForTest();
282
+ const offscreen = createWgpuOffscreenRenderState(screen);
283
+ const root = createDisplayObject();
284
+ registerRenderer(offscreen, root.kind, { createData: () => createEntity({}), submit: vi.fn() });
285
+ prepareScene2DRender(offscreen, root);
286
+ beginWgpuFrame(screen);
287
+ beginWgpuFrame(offscreen);
288
+
289
+ expect(getRenderStateRuntime(offscreen).renderProxyMap).not.toBe(getRenderStateRuntime(screen).renderProxyMap);
290
+ expect(getWgpuRenderStateRuntime(offscreen).commandEncoder).not.toBe(
291
+ getWgpuRenderStateRuntime(screen).commandEncoder,
292
+ );
293
+ });
294
+
295
+ it('destroys derived renderer data and its own uniform ring without freeing the screen ring', async () => {
296
+ const screen = await createWgpuRenderStateForTest();
297
+ const root = createDisplayObject();
298
+ const destroyData = vi.fn();
299
+ registerRenderer(screen, root.kind, {
300
+ createData: () => createEntity({}),
301
+ destroyData,
302
+ submit: vi.fn(),
303
+ });
304
+ const offscreen = createWgpuOffscreenRenderState(screen);
305
+ prepareScene2DRender(offscreen, root);
306
+ const screenDestroy = vi.spyOn(getWgpuRenderStateRuntime(screen).uniformBuffer, 'destroy');
307
+ const offscreenDestroy = vi.spyOn(getWgpuRenderStateRuntime(offscreen).uniformBuffer, 'destroy');
308
+
309
+ destroyWgpuRenderState(offscreen);
310
+
311
+ expect(destroyData).toHaveBeenCalledOnce();
312
+ expect(offscreenDestroy).toHaveBeenCalledOnce();
313
+ expect(screenDestroy).not.toHaveBeenCalled();
314
+ });
315
+
316
+ it('requires explicit re-copy for renderers and padding registered after derivation', async () => {
317
+ const screen = await createWgpuRenderStateForTest();
318
+ const offscreen = createWgpuOffscreenRenderState(screen);
319
+ const renderer = { createData: () => null, submit: vi.fn() };
320
+ const paddingResolver = vi.fn(() => ({ bottom: 2, left: 2, right: 2, top: 2 }));
321
+ registerRenderer(screen, 'acme.LateNode', renderer);
322
+ registerPaddingResolver(screen, 'acme.LateEffect', paddingResolver);
323
+
324
+ expect(hasRegistryTableEntry(getRenderStateRuntime(offscreen).registries.renderers, 'acme.LateNode')).toBe(false);
325
+ expect(getPaddingResolver(offscreen, 'acme.LateEffect')).toBeNull();
326
+
327
+ copyAllRenderersFromRenderState(offscreen, screen);
328
+ copyWgpuRenderStateRegistrations(offscreen, screen);
329
+ expect(getRegistryTableEntry(getRenderStateRuntime(offscreen).registries.renderers, 'acme.LateNode')).toBe(
330
+ renderer,
331
+ );
332
+ expect(getPaddingResolver(offscreen, 'acme.LateEffect')).toBe(paddingResolver);
333
+ });
334
+ });
335
+
14
336
  describe('createWgpuRenderState', () => {
15
337
  it('returns a render state with device and context', async () => {
16
338
  const state = await createWgpuRenderStateForTest();
@@ -45,9 +367,70 @@ describe('createWgpuRenderState', () => {
45
367
  });
46
368
 
47
369
  describe('createWgpuRenderStateRuntime', () => {
48
- 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', () => {
49
371
  const runtime = createWgpuRenderStateRuntime();
50
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);
51
434
  });
52
435
  });
53
436
 
@@ -68,6 +451,55 @@ describe('destroyWgpuRenderState', () => {
68
451
  });
69
452
  });
70
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
+
71
503
  describe('getWgpuRenderStateRuntime', () => {
72
504
  it('returns the runtime attached by createWgpuRenderState', async () => {
73
505
  const state = await createWgpuRenderStateForTest();
@@ -85,8 +517,8 @@ describe('getWgpuRenderStateRuntime', () => {
85
517
  describe('getWgpuSampler', () => {
86
518
  it('caches a sampler per filter+wrap+mip+anisotropy config and reuses it (one createSampler)', async () => {
87
519
  const state = await createWgpuRenderStateForTest();
88
- const a = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
89
- const b = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
520
+ const a = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
521
+ const b = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
90
522
  expect(a).toBe(b);
91
523
  // The cache is keyed by a packed NUMBER (no per-call string allocation), and one config caches once.
92
524
  expect([...getWgpuRenderStateRuntime(state).samplerCache.keys()].every((k) => typeof k === 'number')).toBe(true);
@@ -95,17 +527,17 @@ describe('getWgpuSampler', () => {
95
527
 
96
528
  it('returns a distinct sampler for a different wrap or filter', async () => {
97
529
  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');
530
+ const repeat = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
531
+ const clamp = getWgpuSampler(state, 'linear', 'linear', 'clamp-to-edge', 'clamp-to-edge');
532
+ const nearest = getWgpuSampler(state, 'nearest', 'nearest', 'repeat', 'repeat');
101
533
  expect(repeat).not.toBe(clamp);
102
534
  expect(repeat).not.toBe(nearest);
103
535
  });
104
536
 
105
537
  it('keys the mip filter separately so a trilinear sampler differs from a non-mip one', async () => {
106
538
  const state = await createWgpuRenderStateForTest();
107
- const noMip = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
108
- const trilinear = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear');
539
+ const noMip = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat');
540
+ const trilinear = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear');
109
541
  expect(noMip).not.toBe(trilinear);
110
542
  expect(getWgpuRenderStateRuntime(state).samplerCache.size).toBe(2);
111
543
  });
@@ -114,18 +546,27 @@ describe('getWgpuSampler', () => {
114
546
  // WebGPU rejects maxAnisotropy > 1 unless min/mag/mip are all linear, so a nearest+aniso request
115
547
  // collapses to the SAME sampler as the explicit linear/trilinear anisotropic request.
116
548
  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);
549
+ const collapsed = getWgpuSampler(state, 'nearest', 'nearest', 'clamp-to-edge', 'clamp-to-edge', undefined, 8);
550
+ const explicit = getWgpuSampler(state, 'linear', 'linear', 'clamp-to-edge', 'clamp-to-edge', 'linear', 8);
119
551
  expect(collapsed).toBe(explicit);
120
552
  });
121
553
 
122
554
  it('floors and clamps the anisotropy level into the cache key', async () => {
123
555
  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);
556
+ const a = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear', 4.9);
557
+ const b = getWgpuSampler(state, 'linear', 'linear', 'repeat', 'repeat', 'linear', 4);
126
558
  expect(a).toBe(b);
127
559
  expect(getWgpuRenderStateRuntime(state).samplerCache.size).toBe(1);
128
560
  });
561
+
562
+ it('keeps independent minification and magnification filters', async () => {
563
+ const state = await createWgpuRenderStateForTest();
564
+ const createSampler = vi.spyOn(state.device, 'createSampler');
565
+ getWgpuSampler(state, 'nearest', 'linear', 'repeat', 'repeat');
566
+ expect(createSampler).toHaveBeenLastCalledWith(
567
+ expect.objectContaining({ minFilter: 'nearest', magFilter: 'linear' }),
568
+ );
569
+ });
129
570
  });
130
571
 
131
572
  describe('isWgpuSupported', () => {
@@ -133,3 +574,12 @@ describe('isWgpuSupported', () => {
133
574
  expect(isWgpuSupported()).toBe(true);
134
575
  });
135
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
+ });
@@ -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);