@flighthq/scene2d-wgpu 0.2.1-next.840.857425c

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 (144) hide show
  1. package/dist/enableWgpuColorAdjustmentGuards.d.ts +4 -0
  2. package/dist/enableWgpuColorAdjustmentGuards.d.ts.map +1 -0
  3. package/dist/enableWgpuColorAdjustmentGuards.js +21 -0
  4. package/dist/enableWgpuColorAdjustmentGuards.js.map +1 -0
  5. package/dist/index.d.ts +29 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +29 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/moduleMockTestHelper.d.ts +15 -0
  10. package/dist/moduleMockTestHelper.d.ts.map +1 -0
  11. package/dist/moduleMockTestHelper.js +25 -0
  12. package/dist/moduleMockTestHelper.js.map +1 -0
  13. package/dist/wgpuBitmap.d.ts +4 -0
  14. package/dist/wgpuBitmap.d.ts.map +1 -0
  15. package/dist/wgpuBitmap.js +84 -0
  16. package/dist/wgpuBitmap.js.map +1 -0
  17. package/dist/wgpuCache.d.ts +31 -0
  18. package/dist/wgpuCache.d.ts.map +1 -0
  19. package/dist/wgpuCache.js +244 -0
  20. package/dist/wgpuCache.js.map +1 -0
  21. package/dist/wgpuClip.d.ts +3 -0
  22. package/dist/wgpuClip.d.ts.map +1 -0
  23. package/dist/wgpuClip.js +44 -0
  24. package/dist/wgpuClip.js.map +1 -0
  25. package/dist/wgpuClipContours.d.ts +4 -0
  26. package/dist/wgpuClipContours.d.ts.map +1 -0
  27. package/dist/wgpuClipContours.js +174 -0
  28. package/dist/wgpuClipContours.js.map +1 -0
  29. package/dist/wgpuClipRectangle.d.ts +4 -0
  30. package/dist/wgpuClipRectangle.d.ts.map +1 -0
  31. package/dist/wgpuClipRectangle.js +80 -0
  32. package/dist/wgpuClipRectangle.js.map +1 -0
  33. package/dist/wgpuColorAdjustment.d.ts +3 -0
  34. package/dist/wgpuColorAdjustment.d.ts.map +1 -0
  35. package/dist/wgpuColorAdjustment.js +201 -0
  36. package/dist/wgpuColorAdjustment.js.map +1 -0
  37. package/dist/wgpuDefaultMaterial.d.ts +4 -0
  38. package/dist/wgpuDefaultMaterial.d.ts.map +1 -0
  39. package/dist/wgpuDefaultMaterial.js +49 -0
  40. package/dist/wgpuDefaultMaterial.js.map +1 -0
  41. package/dist/wgpuNode2D.d.ts +5 -0
  42. package/dist/wgpuNode2D.d.ts.map +1 -0
  43. package/dist/wgpuNode2D.js +41 -0
  44. package/dist/wgpuNode2D.js.map +1 -0
  45. package/dist/wgpuParticleEmitter2D.d.ts +4 -0
  46. package/dist/wgpuParticleEmitter2D.d.ts.map +1 -0
  47. package/dist/wgpuParticleEmitter2D.js +293 -0
  48. package/dist/wgpuParticleEmitter2D.js.map +1 -0
  49. package/dist/wgpuQuadBatch.d.ts +5 -0
  50. package/dist/wgpuQuadBatch.d.ts.map +1 -0
  51. package/dist/wgpuQuadBatch.js +90 -0
  52. package/dist/wgpuQuadBatch.js.map +1 -0
  53. package/dist/wgpuRegistration.d.ts +19 -0
  54. package/dist/wgpuRegistration.d.ts.map +1 -0
  55. package/dist/wgpuRegistration.js +53 -0
  56. package/dist/wgpuRegistration.js.map +1 -0
  57. package/dist/wgpuRenderStats.d.ts +20 -0
  58. package/dist/wgpuRenderStats.d.ts.map +1 -0
  59. package/dist/wgpuRenderStats.js +50 -0
  60. package/dist/wgpuRenderStats.js.map +1 -0
  61. package/dist/wgpuRendererData.d.ts +26 -0
  62. package/dist/wgpuRendererData.d.ts.map +1 -0
  63. package/dist/wgpuRendererData.js +29 -0
  64. package/dist/wgpuRendererData.js.map +1 -0
  65. package/dist/wgpuRichText.d.ts +8 -0
  66. package/dist/wgpuRichText.d.ts.map +1 -0
  67. package/dist/wgpuRichText.js +175 -0
  68. package/dist/wgpuRichText.js.map +1 -0
  69. package/dist/wgpuScale9Mapper.d.ts +3 -0
  70. package/dist/wgpuScale9Mapper.d.ts.map +1 -0
  71. package/dist/wgpuScale9Mapper.js +42 -0
  72. package/dist/wgpuScale9Mapper.js.map +1 -0
  73. package/dist/wgpuScale9Shape.d.ts +9 -0
  74. package/dist/wgpuScale9Shape.d.ts.map +1 -0
  75. package/dist/wgpuScale9Shape.js +123 -0
  76. package/dist/wgpuScale9Shape.js.map +1 -0
  77. package/dist/wgpuShape.d.ts +4 -0
  78. package/dist/wgpuShape.d.ts.map +1 -0
  79. package/dist/wgpuShape.js +164 -0
  80. package/dist/wgpuShape.js.map +1 -0
  81. package/dist/wgpuShapeMesh.d.ts +3 -0
  82. package/dist/wgpuShapeMesh.d.ts.map +1 -0
  83. package/dist/wgpuShapeMesh.js +224 -0
  84. package/dist/wgpuShapeMesh.js.map +1 -0
  85. package/dist/wgpuSprite.d.ts +3 -0
  86. package/dist/wgpuSprite.d.ts.map +1 -0
  87. package/dist/wgpuSprite.js +28 -0
  88. package/dist/wgpuSprite.js.map +1 -0
  89. package/dist/wgpuSpriteBatch.d.ts +12 -0
  90. package/dist/wgpuSpriteBatch.d.ts.map +1 -0
  91. package/dist/wgpuSpriteBatch.js +341 -0
  92. package/dist/wgpuSpriteBatch.js.map +1 -0
  93. package/dist/wgpuSpriteRenderer.d.ts +3 -0
  94. package/dist/wgpuSpriteRenderer.d.ts.map +1 -0
  95. package/dist/wgpuSpriteRenderer.js +55 -0
  96. package/dist/wgpuSpriteRenderer.js.map +1 -0
  97. package/dist/wgpuTextInput.d.ts +4 -0
  98. package/dist/wgpuTextInput.d.ts.map +1 -0
  99. package/dist/wgpuTextInput.js +44 -0
  100. package/dist/wgpuTextInput.js.map +1 -0
  101. package/dist/wgpuTextLabel.d.ts +4 -0
  102. package/dist/wgpuTextLabel.d.ts.map +1 -0
  103. package/dist/wgpuTextLabel.js +149 -0
  104. package/dist/wgpuTextLabel.js.map +1 -0
  105. package/dist/wgpuTilemap.d.ts +3 -0
  106. package/dist/wgpuTilemap.d.ts.map +1 -0
  107. package/dist/wgpuTilemap.js +81 -0
  108. package/dist/wgpuTilemap.js.map +1 -0
  109. package/dist/wgpuVelocity.d.ts +23 -0
  110. package/dist/wgpuVelocity.d.ts.map +1 -0
  111. package/dist/wgpuVelocity.js +365 -0
  112. package/dist/wgpuVelocity.js.map +1 -0
  113. package/dist/wgpuVideo.d.ts +6 -0
  114. package/dist/wgpuVideo.d.ts.map +1 -0
  115. package/dist/wgpuVideo.js +63 -0
  116. package/dist/wgpuVideo.js.map +1 -0
  117. package/package.json +60 -0
  118. package/src/enableWgpuColorAdjustmentGuards.test.ts +82 -0
  119. package/src/wgpuBitmap.test.ts +40 -0
  120. package/src/wgpuCache.test.ts +272 -0
  121. package/src/wgpuClip.test.ts +15 -0
  122. package/src/wgpuClipContours.test.ts +101 -0
  123. package/src/wgpuClipRectangle.test.ts +45 -0
  124. package/src/wgpuColorAdjustment.test.ts +120 -0
  125. package/src/wgpuDefaultMaterial.test.ts +21 -0
  126. package/src/wgpuNode2D.test.ts +36 -0
  127. package/src/wgpuParticleEmitter2D.test.ts +85 -0
  128. package/src/wgpuQuadBatch.test.ts +69 -0
  129. package/src/wgpuRegistration.test.ts +115 -0
  130. package/src/wgpuRenderStats.test.ts +89 -0
  131. package/src/wgpuRendererData.test.ts +29 -0
  132. package/src/wgpuRichText.test.ts +100 -0
  133. package/src/wgpuScale9Mapper.test.ts +20 -0
  134. package/src/wgpuScale9Shape.test.ts +97 -0
  135. package/src/wgpuShape.test.ts +96 -0
  136. package/src/wgpuShapeMesh.test.ts +164 -0
  137. package/src/wgpuSprite.test.ts +19 -0
  138. package/src/wgpuSpriteBatch.test.ts +272 -0
  139. package/src/wgpuSpriteRenderer.test.ts +63 -0
  140. package/src/wgpuTextInput.test.ts +37 -0
  141. package/src/wgpuTextLabel.test.ts +124 -0
  142. package/src/wgpuTilemap.test.ts +47 -0
  143. package/src/wgpuVelocity.test.ts +148 -0
  144. package/src/wgpuVideo.test.ts +59 -0
@@ -0,0 +1,272 @@
1
+ import { createMatrix } from '@flighthq/geometry';
2
+ import { createRenderCache, createRenderState, RenderCacheKind, useRenderCache } from '@flighthq/render';
3
+ import type * as WgpuRenderWgpuModule from '@flighthq/render-wgpu';
4
+ import { createDisplayObject } from '@flighthq/scene2d';
5
+ import type { WgpuRenderState, WgpuRenderTarget } from '@flighthq/types';
6
+ import { EntityRuntimeKey } from '@flighthq/types';
7
+
8
+ import { scopeModuleMocks } from './moduleMockTestHelper';
9
+ import type * as WgpuCacheModule from './wgpuCache';
10
+ import type * as WgpuNode2DModule from './wgpuNode2D';
11
+ import type * as WgpuSpriteBatchModule from './wgpuSpriteBatch';
12
+
13
+ // The GPU render-target lifecycle (@flighthq/render-wgpu) and the two local collaborators
14
+ // ./wgpuSpriteBatch and ./wgpuNode2D are stubbed so cache orchestration can be unit-tested
15
+ // without a real GPU pipeline: createWgpuRenderTarget returns a plain descriptor, and the composite,
16
+ // batch-flush, and subtree-render calls become spies for the call and ordering assertions below.
17
+ // The mocks are scoped to this file's dynamic import of ./wgpuCache and unmocked in afterAll, so
18
+ // under a shared (isolate:false) worker they never leak into the real render-wgpu / scene2d-
19
+ // wgpu consumers. The mocked functions are read back from the same dynamic imports so the handles
20
+ // the assertions observe are the exact vi.fn instances the cache module calls.
21
+ let createWgpuCacheState: typeof WgpuCacheModule.createWgpuCacheState;
22
+ let defaultWgpuRenderCacheRenderer: typeof WgpuCacheModule.defaultWgpuRenderCacheRenderer;
23
+ let enableWgpuRenderCache: typeof WgpuCacheModule.enableWgpuRenderCache;
24
+ let ensureWgpuRenderCacheTarget: typeof WgpuCacheModule.ensureWgpuRenderCacheTarget;
25
+ let getWgpuRenderCacheTarget: typeof WgpuCacheModule.getWgpuRenderCacheTarget;
26
+ let refreshWgpuRenderCache: typeof WgpuCacheModule.refreshWgpuRenderCache;
27
+ let releaseWgpuRenderCache: typeof WgpuCacheModule.releaseWgpuRenderCache;
28
+ let createWgpuRenderStateRuntime: typeof WgpuRenderWgpuModule.createWgpuRenderStateRuntime;
29
+ let getWgpuRenderStateRuntime: typeof WgpuRenderWgpuModule.getWgpuRenderStateRuntime;
30
+ let beginWgpuFrame: typeof WgpuRenderWgpuModule.beginWgpuFrame;
31
+ let destroyWgpuRenderTarget: typeof WgpuRenderWgpuModule.destroyWgpuRenderTarget;
32
+ let drawWgpuRenderTargetResult: typeof WgpuRenderWgpuModule.drawWgpuRenderTargetResult;
33
+ let submitWgpuRenderPass: typeof WgpuRenderWgpuModule.submitWgpuRenderPass;
34
+ let renderWgpuScene2D: typeof WgpuNode2DModule.renderWgpuScene2D;
35
+ let flushWgpuSpriteBatch: typeof WgpuSpriteBatchModule.flushWgpuSpriteBatch;
36
+
37
+ // EntityRuntimeKey (Symbol.for) and RenderCacheKind (a string) are identity-stable across the
38
+ // registry reset scopeModuleMocks performs, and cache adapters are stored on the state, not module-
39
+ // level, so the statically-imported @flighthq/render still interoperates with the re-evaluated
40
+ // subject even though the subject re-imports @flighthq/render under the reset.
41
+ scopeModuleMocks(['./wgpuSpriteBatch', '@flighthq/render-wgpu', './wgpuNode2D']);
42
+
43
+ beforeAll(async () => {
44
+ vi.doMock('./wgpuSpriteBatch', async (importOriginal) => {
45
+ const actual = await importOriginal<typeof WgpuSpriteBatchModule>();
46
+ return { ...actual, flushWgpuSpriteBatch: vi.fn() };
47
+ });
48
+ vi.doMock('@flighthq/render-wgpu', async (importOriginal) => {
49
+ const actual = await importOriginal<typeof WgpuRenderWgpuModule>();
50
+ return {
51
+ ...actual,
52
+ beginWgpuFrame: vi.fn((state: WgpuRenderState) => {
53
+ getWgpuRenderStateRuntime(state).commandEncoder = {} as GPUCommandEncoder;
54
+ }),
55
+ beginWgpuRenderPass: vi.fn(),
56
+ setWgpuRenderTransform2D: vi.fn(),
57
+ createWgpuRenderTarget: vi.fn(
58
+ (_state: unknown, width: number, height: number): WgpuRenderTarget => ({
59
+ bindGroup: {} as GPUBindGroup,
60
+ colorSpace: 'srgb',
61
+ depthStencilTexture: {} as GPUTexture,
62
+ depthStencilView: {} as GPUTextureView,
63
+ texture: {} as GPUTexture,
64
+ view: {} as GPUTextureView,
65
+ format: 'bgra8unorm',
66
+ clearColors: [],
67
+ clearDepth: 1,
68
+ width,
69
+ height,
70
+ }),
71
+ ),
72
+ destroyWgpuRenderTarget: vi.fn(),
73
+ drawWgpuRenderTargetResult: vi.fn(),
74
+ endWgpuRenderPass: vi.fn(),
75
+ resizeWgpuRenderTarget: vi.fn((_state: unknown, target: WgpuRenderTarget, width: number, height: number) => {
76
+ target.width = width;
77
+ target.height = height;
78
+ }),
79
+ submitWgpuRenderPass: vi.fn((state: WgpuRenderState) => {
80
+ getWgpuRenderStateRuntime(state).commandEncoder = null;
81
+ }),
82
+ };
83
+ });
84
+ vi.doMock('./wgpuNode2D', async (importOriginal) => {
85
+ const actual = await importOriginal<typeof WgpuNode2DModule>();
86
+ return { ...actual, renderWgpuScene2D: vi.fn() };
87
+ });
88
+
89
+ ({
90
+ beginWgpuFrame,
91
+ createWgpuRenderStateRuntime,
92
+ destroyWgpuRenderTarget,
93
+ drawWgpuRenderTargetResult,
94
+ getWgpuRenderStateRuntime,
95
+ submitWgpuRenderPass,
96
+ } = await import('@flighthq/render-wgpu'));
97
+ ({ flushWgpuSpriteBatch } = await import('./wgpuSpriteBatch'));
98
+ ({ renderWgpuScene2D } = await import('./wgpuNode2D'));
99
+ ({
100
+ createWgpuCacheState,
101
+ defaultWgpuRenderCacheRenderer,
102
+ enableWgpuRenderCache,
103
+ ensureWgpuRenderCacheTarget,
104
+ getWgpuRenderCacheTarget,
105
+ refreshWgpuRenderCache,
106
+ releaseWgpuRenderCache,
107
+ } = await import('./wgpuCache'));
108
+ });
109
+
110
+ function fakeScreen(options = {}): WgpuRenderState {
111
+ const state = createRenderState(options) as unknown as WgpuRenderState;
112
+ (state as any).device = {} as GPUDevice;
113
+ state[EntityRuntimeKey] = createWgpuRenderStateRuntime();
114
+ const runtime = getWgpuRenderStateRuntime(state);
115
+ runtime.commandEncoder = null;
116
+ runtime.currentBlendMode = null;
117
+ return state;
118
+ }
119
+
120
+ function makeCacheNode(source: unknown): any {
121
+ return { source, kind: RenderCacheKind, transform2D: createMatrix(), alpha: 1, blendMode: null };
122
+ }
123
+
124
+ describe('createWgpuCacheState', () => {
125
+ it('copies renderers and shares the GPU device but keeps its own node map', () => {
126
+ const screen = fakeScreen();
127
+ enableWgpuRenderCache(screen);
128
+ const cacheState = createWgpuCacheState(screen);
129
+ expect(getWgpuRenderStateRuntime(cacheState).rendererMap.get(RenderCacheKind)).toBe(defaultWgpuRenderCacheRenderer);
130
+ expect((cacheState as any).device).toBe((screen as any).device);
131
+ expect(getWgpuRenderStateRuntime(cacheState).renderProxyMap).not.toBe(
132
+ getWgpuRenderStateRuntime(screen).renderProxyMap,
133
+ );
134
+ });
135
+ });
136
+
137
+ describe('defaultWgpuRenderCacheRenderer', () => {
138
+ it('does nothing when no cache is attached to the source', () => {
139
+ const state = fakeScreen();
140
+ defaultWgpuRenderCacheRenderer.submit(state, makeCacheNode(createDisplayObject()));
141
+ expect(drawWgpuRenderTargetResult).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('composites the cache target attached to the source node', () => {
145
+ const state = fakeScreen();
146
+ const obj = createDisplayObject();
147
+ const cache = createRenderCache();
148
+ useRenderCache(state, obj, cache);
149
+ const target = ensureWgpuRenderCacheTarget(state, cache, 16, 16);
150
+ defaultWgpuRenderCacheRenderer.submit(state, makeCacheNode(obj));
151
+ expect(drawWgpuRenderTargetResult).toHaveBeenCalledWith(state, expect.anything(), target, expect.anything());
152
+ });
153
+
154
+ it('flushes pending batched geometry before the immediate composite', () => {
155
+ const state = fakeScreen();
156
+ const obj = createDisplayObject();
157
+ const cache = createRenderCache();
158
+ useRenderCache(state, obj, cache);
159
+ ensureWgpuRenderCacheTarget(state, cache, 16, 16);
160
+ defaultWgpuRenderCacheRenderer.submit(state, makeCacheNode(obj));
161
+ // The composite draws an immediate quad outside the sprite batch; geometry submitted earlier in
162
+ // the walk must be drained first, or the immediate quad interleaves with the un-flushed batch's
163
+ // instance buffer and bind-group state and corrupts it.
164
+ expect(flushWgpuSpriteBatch).toHaveBeenCalledWith(state);
165
+ expect((flushWgpuSpriteBatch as any).mock.invocationCallOrder[0]).toBeLessThan(
166
+ (drawWgpuRenderTargetResult as any).mock.invocationCallOrder[0],
167
+ );
168
+ });
169
+ });
170
+
171
+ describe('enableWgpuRenderCache', () => {
172
+ it('registers the renderer for the render cache kind', () => {
173
+ const state = fakeScreen();
174
+ enableWgpuRenderCache(state);
175
+ expect(getWgpuRenderStateRuntime(state).rendererMap.get(RenderCacheKind)).toBe(defaultWgpuRenderCacheRenderer);
176
+ });
177
+ });
178
+
179
+ describe('ensureWgpuRenderCacheTarget', () => {
180
+ it('creates a target sized to the request', () => {
181
+ const state = fakeScreen();
182
+ const target = ensureWgpuRenderCacheTarget(state, createRenderCache(), 64, 32);
183
+ expect(target.width).toBe(64);
184
+ expect(target.height).toBe(32);
185
+ });
186
+
187
+ it('reuses and resizes the same target on subsequent calls', () => {
188
+ const state = fakeScreen();
189
+ const cache = createRenderCache();
190
+ const first = ensureWgpuRenderCacheTarget(state, cache, 64, 32);
191
+ const second = ensureWgpuRenderCacheTarget(state, cache, 16, 16);
192
+ expect(second).toBe(first);
193
+ expect(second.width).toBe(16);
194
+ });
195
+
196
+ it('keeps targets isolated per state for the same handle', () => {
197
+ const stateA = fakeScreen();
198
+ const stateB = fakeScreen();
199
+ const cache = createRenderCache();
200
+ expect(ensureWgpuRenderCacheTarget(stateA, cache, 8, 8)).not.toBe(ensureWgpuRenderCacheTarget(stateB, cache, 8, 8));
201
+ });
202
+ });
203
+
204
+ describe('getWgpuRenderCacheTarget', () => {
205
+ it('returns null before a target is allocated', () => {
206
+ expect(getWgpuRenderCacheTarget(fakeScreen(), createRenderCache())).toBeNull();
207
+ });
208
+
209
+ it('returns the allocated target', () => {
210
+ const state = fakeScreen();
211
+ const cache = createRenderCache();
212
+ const target = ensureWgpuRenderCacheTarget(state, cache, 8, 8);
213
+ expect(getWgpuRenderCacheTarget(state, cache)).toBe(target);
214
+ });
215
+ });
216
+
217
+ describe('refreshWgpuRenderCache', () => {
218
+ it('opens and submits a standalone frame when called outside the visible frame', () => {
219
+ const screen = fakeScreen();
220
+ const cacheState = createWgpuCacheState(screen);
221
+ refreshWgpuRenderCache(cacheState, createRenderCache(), createDisplayObject(), { padding: 5 });
222
+ expect(beginWgpuFrame).toHaveBeenCalledWith(screen);
223
+ expect(submitWgpuRenderPass).toHaveBeenCalledWith(screen);
224
+ expect(getWgpuRenderStateRuntime(screen).commandEncoder).toBeNull();
225
+ });
226
+
227
+ it('records into an active application frame without submitting it', () => {
228
+ const screen = fakeScreen();
229
+ const encoder = {} as GPUCommandEncoder;
230
+ getWgpuRenderStateRuntime(screen).commandEncoder = encoder;
231
+ const cacheState = createWgpuCacheState(screen);
232
+ vi.mocked(beginWgpuFrame).mockClear();
233
+ vi.mocked(submitWgpuRenderPass).mockClear();
234
+ refreshWgpuRenderCache(cacheState, createRenderCache(), createDisplayObject(), { padding: 5 });
235
+ expect(beginWgpuFrame).not.toHaveBeenCalled();
236
+ expect(submitWgpuRenderPass).not.toHaveBeenCalled();
237
+ expect(getWgpuRenderStateRuntime(screen).commandEncoder).toBe(encoder);
238
+ });
239
+
240
+ it('bakes on the first call and allocates the target on the screen state', () => {
241
+ const screen = fakeScreen();
242
+ const cacheState = createWgpuCacheState(screen);
243
+ const cache = createRenderCache();
244
+ const obj = createDisplayObject();
245
+ const rebaked = refreshWgpuRenderCache(cacheState, cache, obj, { padding: 5 });
246
+ expect(rebaked).toBe(true);
247
+ expect(renderWgpuScene2D).toHaveBeenCalled();
248
+ const target = getWgpuRenderCacheTarget(screen, cache);
249
+ expect(target).not.toBeNull();
250
+ expect(target!.width).toBe(10);
251
+ });
252
+
253
+ it('skips the bake under requiresInvalidation when nothing changed', () => {
254
+ const screen = fakeScreen({ sceneGraphSyncPolicy: 'requiresInvalidation' });
255
+ const cacheState = createWgpuCacheState(screen);
256
+ const cache = createRenderCache();
257
+ const obj = createDisplayObject();
258
+ refreshWgpuRenderCache(cacheState, cache, obj, { padding: 5 });
259
+ expect(refreshWgpuRenderCache(cacheState, cache, obj, { padding: 5 })).toBe(false);
260
+ });
261
+ });
262
+
263
+ describe('releaseWgpuRenderCache', () => {
264
+ it('destroys and drops the target for the cache', () => {
265
+ const state = fakeScreen();
266
+ const cache = createRenderCache();
267
+ const target = ensureWgpuRenderCacheTarget(state, cache, 8, 8);
268
+ releaseWgpuRenderCache(state, cache);
269
+ expect(destroyWgpuRenderTarget).toHaveBeenCalledWith(state, target);
270
+ expect(getWgpuRenderCacheTarget(state, cache)).toBeNull();
271
+ });
272
+ });
@@ -0,0 +1,15 @@
1
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
2
+
3
+ import { enableWgpuClipSupport } from './wgpuClip';
4
+
5
+ beforeAll(() => {
6
+ installWgpuMock();
7
+ });
8
+
9
+ describe('enableWgpuClipSupport', () => {
10
+ it('sets displayObjectClipHooks', async () => {
11
+ const state = await createWgpuRenderStateForTest();
12
+ enableWgpuClipSupport(state);
13
+ expect(state.displayObjectClipHooks).not.toBeNull();
14
+ });
15
+ });
@@ -0,0 +1,101 @@
1
+ import { createMatrix } from '@flighthq/geometry';
2
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+ import type { WgpuRenderState } from '@flighthq/types';
5
+
6
+ import { popWgpuClipContours, pushWgpuClipContours } from './wgpuClipContours';
7
+
8
+ beforeAll(() => {
9
+ installWgpuMock();
10
+ });
11
+
12
+ const SQUARE = [[0, 0, 50, 0, 50, 50, 0, 50]];
13
+
14
+ function makePassSpy(): GPURenderPassEncoder {
15
+ return {
16
+ setPipeline: vi.fn(),
17
+ setBindGroup: vi.fn(),
18
+ setVertexBuffer: vi.fn(),
19
+ setStencilReference: vi.fn(),
20
+ draw: vi.fn(),
21
+ } as unknown as GPURenderPassEncoder;
22
+ }
23
+
24
+ async function makeState(): Promise<WgpuRenderState> {
25
+ const state = await createWgpuRenderStateForTest();
26
+ getWgpuRenderStateRuntime(state).renderPass = makePassSpy();
27
+ return state;
28
+ }
29
+
30
+ describe('popWgpuClipContours', () => {
31
+ it('decrements the stencil depth and pops the contour stack', async () => {
32
+ const state = await makeState();
33
+ const runtime = getWgpuRenderStateRuntime(state);
34
+ pushWgpuClipContours(state, SQUARE, 'nonZero', createMatrix());
35
+ expect(runtime.currentMaskDepth).toBe(1);
36
+ expect(runtime.clipContourStack.length).toBe(1);
37
+
38
+ popWgpuClipContours(state);
39
+
40
+ expect(runtime.currentMaskDepth).toBe(0);
41
+ expect(runtime.clipContourStack.length).toBe(0);
42
+ });
43
+
44
+ it('redraws the stored geometry with the erase pipeline to undo its stencil region', async () => {
45
+ const state = await makeState();
46
+ pushWgpuClipContours(state, SQUARE, 'nonZero', createMatrix());
47
+ const pass = getWgpuRenderStateRuntime(state).renderPass as unknown as {
48
+ setStencilReference: ReturnType<typeof vi.fn>;
49
+ draw: ReturnType<typeof vi.fn>;
50
+ };
51
+ pass.setStencilReference.mockClear();
52
+ pass.draw.mockClear();
53
+
54
+ popWgpuClipContours(state);
55
+
56
+ // Erase decrements the region stamped at depth 1, so the reference is the removed depth (1).
57
+ expect(pass.setStencilReference).toHaveBeenCalledWith(1);
58
+ expect(pass.draw).toHaveBeenCalled();
59
+ });
60
+ });
61
+
62
+ describe('pushWgpuClipContours', () => {
63
+ it('increments the stencil depth and records the clip on the stack', async () => {
64
+ const state = await makeState();
65
+ const runtime = getWgpuRenderStateRuntime(state);
66
+
67
+ pushWgpuClipContours(state, SQUARE, 'nonZero', createMatrix());
68
+
69
+ expect(runtime.currentMaskDepth).toBe(1);
70
+ expect(runtime.clipContourStack.length).toBe(1);
71
+ expect(runtime.clipContourStack[0].vertexCount).toBe(6); // one quad fan -> two triangles -> 6 verts
72
+ });
73
+
74
+ it('draws the contour with the parent depth as the stencil reference', async () => {
75
+ const state = await makeState();
76
+ const pass = getWgpuRenderStateRuntime(state).renderPass as unknown as {
77
+ setPipeline: ReturnType<typeof vi.fn>;
78
+ setStencilReference: ReturnType<typeof vi.fn>;
79
+ draw: ReturnType<typeof vi.fn>;
80
+ };
81
+
82
+ pushWgpuClipContours(state, SQUARE, 'nonZero', createMatrix());
83
+
84
+ expect(pass.setStencilReference).toHaveBeenCalledWith(0); // first clip increments from depth 0
85
+ expect(pass.setPipeline).toHaveBeenCalled();
86
+ expect(pass.draw).toHaveBeenCalled();
87
+ });
88
+
89
+ it('nests: a second clip increments from depth 1', async () => {
90
+ const state = await makeState();
91
+ const runtime = getWgpuRenderStateRuntime(state);
92
+ pushWgpuClipContours(state, SQUARE, 'nonZero', createMatrix());
93
+ const pass = runtime.renderPass as unknown as { setStencilReference: ReturnType<typeof vi.fn> };
94
+ pass.setStencilReference.mockClear();
95
+
96
+ pushWgpuClipContours(state, SQUARE, 'nonZero', createMatrix());
97
+
98
+ expect(runtime.currentMaskDepth).toBe(2);
99
+ expect(pass.setStencilReference).toHaveBeenCalledWith(1);
100
+ });
101
+ });
@@ -0,0 +1,45 @@
1
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
2
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+
5
+ import { popWgpuClipRectangle, pushWgpuClipRectangle } from './wgpuClipRectangle';
6
+
7
+ beforeAll(() => {
8
+ installWgpuMock();
9
+ });
10
+
11
+ describe('popWgpuClipRectangle', () => {
12
+ it('pops the scissor rect from the stack', async () => {
13
+ const state = await createWgpuRenderStateForTest();
14
+ renderWgpuBackground(state);
15
+ const runtime = getWgpuRenderStateRuntime(state);
16
+
17
+ pushWgpuClipRectangle(state, { x: 0, y: 0, width: 50, height: 50 }, { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 });
18
+ popWgpuClipRectangle(state);
19
+
20
+ expect(runtime.scissorStack.length).toBe(0);
21
+ expect(runtime.currentScissorRect).toBeNull();
22
+ submitWgpuRenderPass(state);
23
+ });
24
+
25
+ it('is safe to pop from empty stack', async () => {
26
+ const state = await createWgpuRenderStateForTest();
27
+ renderWgpuBackground(state);
28
+ expect(() => popWgpuClipRectangle(state)).not.toThrow();
29
+ submitWgpuRenderPass(state);
30
+ });
31
+ });
32
+
33
+ describe('pushWgpuClipRectangle', () => {
34
+ it('pushes a scissor rect onto the stack', async () => {
35
+ const state = await createWgpuRenderStateForTest();
36
+ renderWgpuBackground(state);
37
+ const runtime = getWgpuRenderStateRuntime(state);
38
+
39
+ pushWgpuClipRectangle(state, { x: 10, y: 10, width: 100, height: 100 }, { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 });
40
+
41
+ expect(runtime.scissorStack.length).toBe(1);
42
+ expect(runtime.currentScissorRect).not.toBeNull();
43
+ submitWgpuRenderPass(state);
44
+ });
45
+ });
@@ -0,0 +1,120 @@
1
+ import { createImageResource } from '@flighthq/image';
2
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
3
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
4
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
5
+ import type { ColorTransform } from '@flighthq/types';
6
+
7
+ import { enableWgpuColorAdjustment } from './wgpuColorAdjustment';
8
+ import { defaultWgpuMaterialRenderer } from './wgpuDefaultMaterial';
9
+ import {
10
+ flushWgpuSpriteBatch,
11
+ prepareWgpuSpriteBatchWrite,
12
+ recordWgpuSpriteBatchColorTransform,
13
+ } from './wgpuSpriteBatch';
14
+
15
+ beforeAll(() => {
16
+ installWgpuMock();
17
+ });
18
+
19
+ function ct(
20
+ redMultiplier = 1,
21
+ greenMultiplier = 1,
22
+ blueMultiplier = 1,
23
+ alphaMultiplier = 1,
24
+ redOffset = 0,
25
+ greenOffset = 0,
26
+ blueOffset = 0,
27
+ alphaOffset = 0,
28
+ ): ColorTransform {
29
+ return {
30
+ redMultiplier,
31
+ greenMultiplier,
32
+ blueMultiplier,
33
+ alphaMultiplier,
34
+ redOffset,
35
+ greenOffset,
36
+ blueOffset,
37
+ alphaOffset,
38
+ } as ColorTransform;
39
+ }
40
+
41
+ const CT_MODE_NONE = 0;
42
+ const CT_MODE_UNIFORM = 1;
43
+ const CT_MODE_PER_INSTANCE = 2;
44
+
45
+ describe('enableWgpuColorAdjustment', () => {
46
+ it('installs the fold so recorded tints drive the color-adjustment state machine', async () => {
47
+ const state = await createWgpuRenderStateForTest();
48
+ const runtime = getWgpuRenderStateRuntime(state);
49
+ enableWgpuColorAdjustment(state);
50
+ expect(runtime.wgpuColorAdjustmentFold).toBeDefined();
51
+ expect(runtime.wgpuColorAdjustmentFold).not.toBeNull();
52
+ });
53
+
54
+ it('is idempotent', async () => {
55
+ const state = await createWgpuRenderStateForTest();
56
+ const runtime = getWgpuRenderStateRuntime(state);
57
+ enableWgpuColorAdjustment(state);
58
+ const fold = runtime.wgpuColorAdjustmentFold;
59
+ enableWgpuColorAdjustment(state);
60
+ expect(runtime.wgpuColorAdjustmentFold).toBe(fold);
61
+ });
62
+
63
+ it('stays untinted (mode NONE) when no instance carries a color transform', async () => {
64
+ const state = await createWgpuRenderStateForTest();
65
+ const runtime = getWgpuRenderStateRuntime(state);
66
+ enableWgpuColorAdjustment(state);
67
+ recordWgpuSpriteBatchColorTransform(state, null, 0);
68
+ recordWgpuSpriteBatchColorTransform(state, null, 1);
69
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_NONE);
70
+ });
71
+
72
+ it('uses one whole-batch uniform when every instance shares one tint', async () => {
73
+ const state = await createWgpuRenderStateForTest();
74
+ const runtime = getWgpuRenderStateRuntime(state);
75
+ enableWgpuColorAdjustment(state);
76
+ const tint = ct(0.5);
77
+ recordWgpuSpriteBatchColorTransform(state, tint, 0);
78
+ recordWgpuSpriteBatchColorTransform(state, tint, 1);
79
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_UNIFORM);
80
+ expect(runtime.spriteBatchUniformColorTransform).toBe(tint);
81
+ });
82
+
83
+ it('promotes to per-instance (never splits) when tints diverge, back-filling the earlier tint', async () => {
84
+ const state = await createWgpuRenderStateForTest();
85
+ const runtime = getWgpuRenderStateRuntime(state);
86
+ enableWgpuColorAdjustment(state);
87
+ recordWgpuSpriteBatchColorTransform(state, ct(0.5), 0);
88
+ recordWgpuSpriteBatchColorTransform(state, ct(0.25), 1);
89
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_PER_INSTANCE);
90
+ expect(runtime.spriteBatchColorTransformData![0]).toBe(0.5);
91
+ expect(runtime.spriteBatchColorTransformData![8]).toBe(0.25);
92
+ });
93
+
94
+ it('promotes with identity fill when a tinted instance follows an untinted one', async () => {
95
+ const state = await createWgpuRenderStateForTest();
96
+ const runtime = getWgpuRenderStateRuntime(state);
97
+ enableWgpuColorAdjustment(state);
98
+ recordWgpuSpriteBatchColorTransform(state, null, 0);
99
+ recordWgpuSpriteBatchColorTransform(state, ct(0.5), 1);
100
+ expect(runtime.spriteBatchColorTransformMode).toBe(CT_MODE_PER_INSTANCE);
101
+ expect(runtime.spriteBatchColorTransformData![0]).toBe(1);
102
+ expect(runtime.spriteBatchColorTransformData![8]).toBe(0.5);
103
+ });
104
+
105
+ it('replicates the uniform tint per instance on flush', async () => {
106
+ const state = await createWgpuRenderStateForTest();
107
+ enableWgpuColorAdjustment(state);
108
+ renderWgpuBackground(state);
109
+ const runtime = getWgpuRenderStateRuntime(state);
110
+ const tex = createImageResource(document.createElement('img'));
111
+ prepareWgpuSpriteBatchWrite(state, tex, null, null, defaultWgpuMaterialRenderer, 2);
112
+ recordWgpuSpriteBatchColorTransform(state, ct(0.5), 0);
113
+ recordWgpuSpriteBatchColorTransform(state, ct(0.5), 1);
114
+ runtime.spriteBatchCount = 2;
115
+ flushWgpuSpriteBatch(state);
116
+ expect(runtime.spriteBatchColorTransformData![0]).toBe(0.5);
117
+ expect(runtime.spriteBatchColorTransformData![8]).toBe(0.5);
118
+ submitWgpuRenderPass(state);
119
+ });
120
+ });
@@ -0,0 +1,21 @@
1
+ import { getWgpuMaterialRenderer } from '@flighthq/render-wgpu';
2
+ import { createWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
3
+ import type { WgpuRenderState } from '@flighthq/types';
4
+ import { DefaultMaterialKind, EntityRuntimeKey } from '@flighthq/types';
5
+
6
+ import { defaultWgpuMaterialRenderer, registerDefaultWgpuMaterial } from './wgpuDefaultMaterial';
7
+
8
+ describe('defaultWgpuMaterialRenderer', () => {
9
+ it('declares no per-instance float data', () => {
10
+ expect(defaultWgpuMaterialRenderer.instanceFloatCount).toBe(0);
11
+ });
12
+ });
13
+
14
+ describe('registerDefaultWgpuMaterial', () => {
15
+ it('registers the default renderer under DefaultMaterialKind', () => {
16
+ const state = {} as WgpuRenderState;
17
+ state[EntityRuntimeKey] = createWgpuRenderStateRuntime();
18
+ registerDefaultWgpuMaterial(state);
19
+ expect(getWgpuMaterialRenderer(state, DefaultMaterialKind)).toBe(defaultWgpuMaterialRenderer);
20
+ });
21
+ });
@@ -0,0 +1,36 @@
1
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
2
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
3
+ import { createDisplayObject } from '@flighthq/scene2d';
4
+
5
+ import { defaultWgpuScene2DRenderer, drawWgpuScene2D, renderWgpuScene2D } from './wgpuNode2D';
6
+
7
+ beforeAll(() => {
8
+ installWgpuMock();
9
+ });
10
+
11
+ describe('defaultWgpuScene2DRenderer', () => {
12
+ it('has createData and draw functions', () => {
13
+ expect(typeof defaultWgpuScene2DRenderer.createData).toBe('function');
14
+ expect(typeof defaultWgpuScene2DRenderer.submit).toBe('function');
15
+ });
16
+ });
17
+
18
+ describe('drawWgpuScene2D', () => {
19
+ it('is a no-op (plain display objects have no geometry)', async () => {
20
+ const state = await createWgpuRenderStateForTest();
21
+ renderWgpuBackground(state);
22
+ const renderProxy = { source: createDisplayObject() } as never;
23
+ expect(() => drawWgpuScene2D(state, renderProxy)).not.toThrow();
24
+ submitWgpuRenderPass(state);
25
+ });
26
+ });
27
+
28
+ describe('renderWgpuScene2D', () => {
29
+ it('traverses a display object without error', async () => {
30
+ const state = await createWgpuRenderStateForTest();
31
+ renderWgpuBackground(state);
32
+ const root = createDisplayObject();
33
+ expect(() => renderWgpuScene2D(state, root)).not.toThrow();
34
+ submitWgpuRenderPass(state);
35
+ });
36
+ });