@flighthq/displayobject-wgpu 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/index.d.ts +29 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +29 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/moduleMockTestHelper.d.ts +15 -0
  6. package/dist/moduleMockTestHelper.d.ts.map +1 -0
  7. package/dist/moduleMockTestHelper.js +25 -0
  8. package/dist/moduleMockTestHelper.js.map +1 -0
  9. package/dist/wgpuBitmap.d.ts +4 -0
  10. package/dist/wgpuBitmap.d.ts.map +1 -0
  11. package/dist/wgpuBitmap.js +80 -0
  12. package/dist/wgpuBitmap.js.map +1 -0
  13. package/dist/wgpuCache.d.ts +31 -0
  14. package/dist/wgpuCache.d.ts.map +1 -0
  15. package/dist/wgpuCache.js +232 -0
  16. package/dist/wgpuCache.js.map +1 -0
  17. package/dist/wgpuClip.d.ts +3 -0
  18. package/dist/wgpuClip.d.ts.map +1 -0
  19. package/dist/wgpuClip.js +44 -0
  20. package/dist/wgpuClip.js.map +1 -0
  21. package/dist/wgpuClipContours.d.ts +4 -0
  22. package/dist/wgpuClipContours.d.ts.map +1 -0
  23. package/dist/wgpuClipContours.js +174 -0
  24. package/dist/wgpuClipContours.js.map +1 -0
  25. package/dist/wgpuClipRectangle.d.ts +4 -0
  26. package/dist/wgpuClipRectangle.d.ts.map +1 -0
  27. package/dist/wgpuClipRectangle.js +80 -0
  28. package/dist/wgpuClipRectangle.js.map +1 -0
  29. package/dist/wgpuColorTransformMaterial.d.ts +5 -0
  30. package/dist/wgpuColorTransformMaterial.d.ts.map +1 -0
  31. package/dist/wgpuColorTransformMaterial.js +97 -0
  32. package/dist/wgpuColorTransformMaterial.js.map +1 -0
  33. package/dist/wgpuDefaultMaterial.d.ts +4 -0
  34. package/dist/wgpuDefaultMaterial.d.ts.map +1 -0
  35. package/dist/wgpuDefaultMaterial.js +46 -0
  36. package/dist/wgpuDefaultMaterial.js.map +1 -0
  37. package/dist/wgpuDisplayObject.d.ts +5 -0
  38. package/dist/wgpuDisplayObject.d.ts.map +1 -0
  39. package/dist/wgpuDisplayObject.js +41 -0
  40. package/dist/wgpuDisplayObject.js.map +1 -0
  41. package/dist/wgpuMaterials.d.ts +15 -0
  42. package/dist/wgpuMaterials.d.ts.map +1 -0
  43. package/dist/wgpuMaterials.js +34 -0
  44. package/dist/wgpuMaterials.js.map +1 -0
  45. package/dist/wgpuParticleEmitter.d.ts +4 -0
  46. package/dist/wgpuParticleEmitter.d.ts.map +1 -0
  47. package/dist/wgpuParticleEmitter.js +288 -0
  48. package/dist/wgpuParticleEmitter.js.map +1 -0
  49. package/dist/wgpuQuadBatch.d.ts +7 -0
  50. package/dist/wgpuQuadBatch.d.ts.map +1 -0
  51. package/dist/wgpuQuadBatch.js +85 -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 +9 -0
  66. package/dist/wgpuRichText.d.ts.map +1 -0
  67. package/dist/wgpuRichText.js +163 -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 +157 -0
  80. package/dist/wgpuShape.js.map +1 -0
  81. package/dist/wgpuShapeMesh.d.ts +9 -0
  82. package/dist/wgpuShapeMesh.d.ts.map +1 -0
  83. package/dist/wgpuShapeMesh.js +204 -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 +18 -0
  90. package/dist/wgpuSpriteBatch.d.ts.map +1 -0
  91. package/dist/wgpuSpriteBatch.js +320 -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 +51 -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 +157 -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 +76 -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 +52 -0
  118. package/src/wgpuBitmap.test.ts +40 -0
  119. package/src/wgpuCache.test.ts +230 -0
  120. package/src/wgpuClip.test.ts +15 -0
  121. package/src/wgpuClipContours.test.ts +101 -0
  122. package/src/wgpuClipRectangle.test.ts +45 -0
  123. package/src/wgpuColorTransformMaterial.test.ts +62 -0
  124. package/src/wgpuDefaultMaterial.test.ts +21 -0
  125. package/src/wgpuDisplayObject.test.ts +36 -0
  126. package/src/wgpuMaterials.test.ts +44 -0
  127. package/src/wgpuParticleEmitter.test.ts +34 -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 +95 -0
  136. package/src/wgpuShapeMesh.test.ts +124 -0
  137. package/src/wgpuSprite.test.ts +19 -0
  138. package/src/wgpuSpriteBatch.test.ts +180 -0
  139. package/src/wgpuSpriteRenderer.test.ts +38 -0
  140. package/src/wgpuTextInput.test.ts +37 -0
  141. package/src/wgpuTextLabel.test.ts +113 -0
  142. package/src/wgpuTilemap.test.ts +47 -0
  143. package/src/wgpuVelocity.test.ts +147 -0
  144. package/src/wgpuVideo.test.ts +59 -0
@@ -0,0 +1,230 @@
1
+ import { createDisplayObject } from '@flighthq/displayobject';
2
+ import { createMatrix } from '@flighthq/geometry';
3
+ import { createRenderCache, createRenderState, RenderCacheKind, useRenderCache } from '@flighthq/render';
4
+ import type * as WgpuRenderWgpuModule from '@flighthq/render-wgpu';
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 WgpuDisplayObjectModule from './wgpuDisplayObject';
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 ./wgpuDisplayObject 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 / displayobject-
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 destroyWgpuRenderTarget: typeof WgpuRenderWgpuModule.destroyWgpuRenderTarget;
31
+ let drawWgpuRenderTargetResult: typeof WgpuRenderWgpuModule.drawWgpuRenderTargetResult;
32
+ let renderWgpuDisplayObject: typeof WgpuDisplayObjectModule.renderWgpuDisplayObject;
33
+ let flushWgpuSpriteBatch: typeof WgpuSpriteBatchModule.flushWgpuSpriteBatch;
34
+
35
+ // EntityRuntimeKey (Symbol.for) and RenderCacheKind (a string) are identity-stable across the
36
+ // registry reset scopeModuleMocks performs, and cache adapters are stored on the state, not module-
37
+ // level, so the statically-imported @flighthq/render still interoperates with the re-evaluated
38
+ // subject even though the subject re-imports @flighthq/render under the reset.
39
+ scopeModuleMocks(['./wgpuSpriteBatch', '@flighthq/render-wgpu', './wgpuDisplayObject']);
40
+
41
+ beforeAll(async () => {
42
+ vi.doMock('./wgpuSpriteBatch', async (importOriginal) => {
43
+ const actual = await importOriginal<typeof WgpuSpriteBatchModule>();
44
+ return { ...actual, flushWgpuSpriteBatch: vi.fn() };
45
+ });
46
+ vi.doMock('@flighthq/render-wgpu', async (importOriginal) => {
47
+ const actual = await importOriginal<typeof WgpuRenderWgpuModule>();
48
+ return {
49
+ ...actual,
50
+ beginWgpuRenderTarget: vi.fn(),
51
+ createWgpuRenderTarget: vi.fn(
52
+ (_state: unknown, width: number, height: number): WgpuRenderTarget => ({
53
+ bindGroup: {} as GPUBindGroup,
54
+ depthStencilTexture: {} as GPUTexture,
55
+ depthStencilView: {} as GPUTextureView,
56
+ texture: {} as GPUTexture,
57
+ view: {} as GPUTextureView,
58
+ format: 'bgra8unorm',
59
+ width,
60
+ height,
61
+ }),
62
+ ),
63
+ destroyWgpuRenderTarget: vi.fn(),
64
+ drawWgpuRenderTargetResult: vi.fn(),
65
+ endWgpuRenderTarget: vi.fn(),
66
+ resizeWgpuRenderTarget: vi.fn((_state: unknown, target: WgpuRenderTarget, width: number, height: number) => {
67
+ target.width = width;
68
+ target.height = height;
69
+ }),
70
+ };
71
+ });
72
+ vi.doMock('./wgpuDisplayObject', async (importOriginal) => {
73
+ const actual = await importOriginal<typeof WgpuDisplayObjectModule>();
74
+ return { ...actual, renderWgpuDisplayObject: vi.fn() };
75
+ });
76
+
77
+ ({ createWgpuRenderStateRuntime, getWgpuRenderStateRuntime, destroyWgpuRenderTarget, drawWgpuRenderTargetResult } =
78
+ await import('@flighthq/render-wgpu'));
79
+ ({ flushWgpuSpriteBatch } = await import('./wgpuSpriteBatch'));
80
+ ({ renderWgpuDisplayObject } = await import('./wgpuDisplayObject'));
81
+ ({
82
+ createWgpuCacheState,
83
+ defaultWgpuRenderCacheRenderer,
84
+ enableWgpuRenderCache,
85
+ ensureWgpuRenderCacheTarget,
86
+ getWgpuRenderCacheTarget,
87
+ refreshWgpuRenderCache,
88
+ releaseWgpuRenderCache,
89
+ } = await import('./wgpuCache'));
90
+ });
91
+
92
+ function fakeScreen(options = {}): WgpuRenderState {
93
+ const state = createRenderState(options) as unknown as WgpuRenderState;
94
+ (state as any).device = {} as GPUDevice;
95
+ state[EntityRuntimeKey] = createWgpuRenderStateRuntime();
96
+ getWgpuRenderStateRuntime(state).currentBlendMode = null;
97
+ return state;
98
+ }
99
+
100
+ function makeCacheNode(source: unknown): any {
101
+ return { source, kind: RenderCacheKind, transform2D: createMatrix(), alpha: 1, blendMode: null };
102
+ }
103
+
104
+ describe('createWgpuCacheState', () => {
105
+ it('copies renderers and shares the GPU device but keeps its own node map', () => {
106
+ const screen = fakeScreen();
107
+ enableWgpuRenderCache(screen);
108
+ const cacheState = createWgpuCacheState(screen);
109
+ expect(getWgpuRenderStateRuntime(cacheState).rendererMap.get(RenderCacheKind)).toBe(defaultWgpuRenderCacheRenderer);
110
+ expect((cacheState as any).device).toBe((screen as any).device);
111
+ expect(getWgpuRenderStateRuntime(cacheState).renderProxyMap).not.toBe(
112
+ getWgpuRenderStateRuntime(screen).renderProxyMap,
113
+ );
114
+ });
115
+ });
116
+
117
+ describe('defaultWgpuRenderCacheRenderer', () => {
118
+ it('does nothing when no cache is attached to the source', () => {
119
+ const state = fakeScreen();
120
+ defaultWgpuRenderCacheRenderer.submit(state, makeCacheNode(createDisplayObject()));
121
+ expect(drawWgpuRenderTargetResult).not.toHaveBeenCalled();
122
+ });
123
+
124
+ it('composites the cache target attached to the source node', () => {
125
+ const state = fakeScreen();
126
+ const obj = createDisplayObject();
127
+ const cache = createRenderCache();
128
+ useRenderCache(state, obj, cache);
129
+ const target = ensureWgpuRenderCacheTarget(state, cache, 16, 16);
130
+ defaultWgpuRenderCacheRenderer.submit(state, makeCacheNode(obj));
131
+ expect(drawWgpuRenderTargetResult).toHaveBeenCalledWith(state, expect.anything(), target, expect.anything());
132
+ });
133
+
134
+ it('flushes pending batched geometry before the immediate composite', () => {
135
+ const state = fakeScreen();
136
+ const obj = createDisplayObject();
137
+ const cache = createRenderCache();
138
+ useRenderCache(state, obj, cache);
139
+ ensureWgpuRenderCacheTarget(state, cache, 16, 16);
140
+ defaultWgpuRenderCacheRenderer.submit(state, makeCacheNode(obj));
141
+ // The composite draws an immediate quad outside the sprite batch; geometry submitted earlier in
142
+ // the walk must be drained first, or the immediate quad interleaves with the un-flushed batch's
143
+ // instance buffer and bind-group state and corrupts it.
144
+ expect(flushWgpuSpriteBatch).toHaveBeenCalledWith(state);
145
+ expect((flushWgpuSpriteBatch as any).mock.invocationCallOrder[0]).toBeLessThan(
146
+ (drawWgpuRenderTargetResult as any).mock.invocationCallOrder[0],
147
+ );
148
+ });
149
+ });
150
+
151
+ describe('enableWgpuRenderCache', () => {
152
+ it('registers the renderer for the render cache kind', () => {
153
+ const state = fakeScreen();
154
+ enableWgpuRenderCache(state);
155
+ expect(getWgpuRenderStateRuntime(state).rendererMap.get(RenderCacheKind)).toBe(defaultWgpuRenderCacheRenderer);
156
+ });
157
+ });
158
+
159
+ describe('ensureWgpuRenderCacheTarget', () => {
160
+ it('creates a target sized to the request', () => {
161
+ const state = fakeScreen();
162
+ const target = ensureWgpuRenderCacheTarget(state, createRenderCache(), 64, 32);
163
+ expect(target.width).toBe(64);
164
+ expect(target.height).toBe(32);
165
+ });
166
+
167
+ it('reuses and resizes the same target on subsequent calls', () => {
168
+ const state = fakeScreen();
169
+ const cache = createRenderCache();
170
+ const first = ensureWgpuRenderCacheTarget(state, cache, 64, 32);
171
+ const second = ensureWgpuRenderCacheTarget(state, cache, 16, 16);
172
+ expect(second).toBe(first);
173
+ expect(second.width).toBe(16);
174
+ });
175
+
176
+ it('keeps targets isolated per state for the same handle', () => {
177
+ const stateA = fakeScreen();
178
+ const stateB = fakeScreen();
179
+ const cache = createRenderCache();
180
+ expect(ensureWgpuRenderCacheTarget(stateA, cache, 8, 8)).not.toBe(ensureWgpuRenderCacheTarget(stateB, cache, 8, 8));
181
+ });
182
+ });
183
+
184
+ describe('getWgpuRenderCacheTarget', () => {
185
+ it('returns null before a target is allocated', () => {
186
+ expect(getWgpuRenderCacheTarget(fakeScreen(), createRenderCache())).toBeNull();
187
+ });
188
+
189
+ it('returns the allocated target', () => {
190
+ const state = fakeScreen();
191
+ const cache = createRenderCache();
192
+ const target = ensureWgpuRenderCacheTarget(state, cache, 8, 8);
193
+ expect(getWgpuRenderCacheTarget(state, cache)).toBe(target);
194
+ });
195
+ });
196
+
197
+ describe('refreshWgpuRenderCache', () => {
198
+ it('bakes on the first call and allocates the target on the screen state', () => {
199
+ const screen = fakeScreen();
200
+ const cacheState = createWgpuCacheState(screen);
201
+ const cache = createRenderCache();
202
+ const obj = createDisplayObject();
203
+ const rebaked = refreshWgpuRenderCache(cacheState, cache, obj, { padding: 5 });
204
+ expect(rebaked).toBe(true);
205
+ expect(renderWgpuDisplayObject).toHaveBeenCalled();
206
+ const target = getWgpuRenderCacheTarget(screen, cache);
207
+ expect(target).not.toBeNull();
208
+ expect(target!.width).toBe(10);
209
+ });
210
+
211
+ it('skips the bake under requiresInvalidation when nothing changed', () => {
212
+ const screen = fakeScreen({ sceneGraphSyncPolicy: 'requiresInvalidation' });
213
+ const cacheState = createWgpuCacheState(screen);
214
+ const cache = createRenderCache();
215
+ const obj = createDisplayObject();
216
+ refreshWgpuRenderCache(cacheState, cache, obj, { padding: 5 });
217
+ expect(refreshWgpuRenderCache(cacheState, cache, obj, { padding: 5 })).toBe(false);
218
+ });
219
+ });
220
+
221
+ describe('releaseWgpuRenderCache', () => {
222
+ it('destroys and drops the target for the cache', () => {
223
+ const state = fakeScreen();
224
+ const cache = createRenderCache();
225
+ const target = ensureWgpuRenderCacheTarget(state, cache, 8, 8);
226
+ releaseWgpuRenderCache(state, cache);
227
+ expect(destroyWgpuRenderTarget).toHaveBeenCalledWith(state, target);
228
+ expect(getWgpuRenderCacheTarget(state, cache)).toBeNull();
229
+ });
230
+ });
@@ -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,62 @@
1
+ import { getWgpuMaterialRenderer } from '@flighthq/render-wgpu';
2
+ import { createWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
3
+ import type { WgpuRenderState } from '@flighthq/types';
4
+ import { ColorTransformMaterialKind, EntityRuntimeKey, UniformColorTransformMaterialKind } from '@flighthq/types';
5
+
6
+ import {
7
+ colorTransformWgpuMaterialRenderer,
8
+ registerWgpuColorTransformMaterials,
9
+ uniformColorTransformWgpuMaterialRenderer,
10
+ } from './wgpuColorTransformMaterial';
11
+
12
+ function makeColorTransform(redMultiplier: number) {
13
+ return {
14
+ redMultiplier,
15
+ greenMultiplier: 1,
16
+ blueMultiplier: 1,
17
+ alphaMultiplier: 1,
18
+ redOffset: 0,
19
+ greenOffset: 0,
20
+ blueOffset: 0,
21
+ alphaOffset: 0,
22
+ };
23
+ }
24
+
25
+ describe('colorTransformWgpuMaterialRenderer', () => {
26
+ it('declares 8 per-instance floats', () => {
27
+ expect(colorTransformWgpuMaterialRenderer.instanceFloatCount).toBe(8);
28
+ });
29
+
30
+ it('packs the materialData color transform', () => {
31
+ const out = new Float32Array(8);
32
+ colorTransformWgpuMaterialRenderer.packInstance!(null as never, null, makeColorTransform(0.5) as never, out, 0);
33
+ expect(out[0]).toBe(0.5);
34
+ });
35
+
36
+ it('packs identity when materialData is null', () => {
37
+ const out = new Float32Array(8);
38
+ colorTransformWgpuMaterialRenderer.packInstance!(null as never, null, null, out, 0);
39
+ expect(Array.from(out)).toEqual([1, 1, 1, 1, 0, 0, 0, 0]);
40
+ });
41
+ });
42
+
43
+ describe('registerWgpuColorTransformMaterials', () => {
44
+ it('registers both color transform material renderers', () => {
45
+ const state = {} as WgpuRenderState;
46
+ state[EntityRuntimeKey] = createWgpuRenderStateRuntime();
47
+ registerWgpuColorTransformMaterials(state);
48
+ expect(getWgpuMaterialRenderer(state, UniformColorTransformMaterialKind)).toBe(
49
+ uniformColorTransformWgpuMaterialRenderer,
50
+ );
51
+ expect(getWgpuMaterialRenderer(state, ColorTransformMaterialKind)).toBe(colorTransformWgpuMaterialRenderer);
52
+ });
53
+ });
54
+
55
+ describe('uniformColorTransformWgpuMaterialRenderer', () => {
56
+ it('packs the material color transform onto the instance', () => {
57
+ const out = new Float32Array(8);
58
+ const material = { kind: UniformColorTransformMaterialKind, colorTransform: makeColorTransform(0.5) } as never;
59
+ uniformColorTransformWgpuMaterialRenderer.packInstance!(null as never, material, null, out, 0);
60
+ expect(out[0]).toBe(0.5);
61
+ });
62
+ });
@@ -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 { createDisplayObject } from '@flighthq/displayobject';
2
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+
5
+ import { defaultWgpuDisplayObjectRenderer, drawWgpuDisplayObject, renderWgpuDisplayObject } from './wgpuDisplayObject';
6
+
7
+ beforeAll(() => {
8
+ installWgpuMock();
9
+ });
10
+
11
+ describe('defaultWgpuDisplayObjectRenderer', () => {
12
+ it('has createData and draw functions', () => {
13
+ expect(typeof defaultWgpuDisplayObjectRenderer.createData).toBe('function');
14
+ expect(typeof defaultWgpuDisplayObjectRenderer.submit).toBe('function');
15
+ });
16
+ });
17
+
18
+ describe('drawWgpuDisplayObject', () => {
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(() => drawWgpuDisplayObject(state, renderProxy)).not.toThrow();
24
+ submitWgpuRenderPass(state);
25
+ });
26
+ });
27
+
28
+ describe('renderWgpuDisplayObject', () => {
29
+ it('traverses a display object without error', async () => {
30
+ const state = await createWgpuRenderStateForTest();
31
+ renderWgpuBackground(state);
32
+ const root = createDisplayObject();
33
+ expect(() => renderWgpuDisplayObject(state, root)).not.toThrow();
34
+ submitWgpuRenderPass(state);
35
+ });
36
+ });
@@ -0,0 +1,44 @@
1
+ import { createBitmap } from '@flighthq/displayobject';
2
+ import { getOrCreateRenderProxy2D, prepareDisplayObjectRender } from '@flighthq/render';
3
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
4
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
5
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
6
+
7
+ import { drawWgpuColorTransformBitmap, registerWgpuColorTransformShader } from './wgpuMaterials';
8
+
9
+ beforeAll(() => {
10
+ installWgpuMock();
11
+ });
12
+
13
+ describe('drawWgpuColorTransformBitmap', () => {
14
+ it('does not throw when render pass is open', async () => {
15
+ const state = await createWgpuRenderStateForTest();
16
+ renderWgpuBackground(state);
17
+ const canvas = document.createElement('canvas');
18
+ canvas.width = 4;
19
+ canvas.height = 4;
20
+ const fakeRenderProxy = {
21
+ alpha: 1,
22
+ useColorTransform: false,
23
+ colorTransform: null,
24
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
25
+ blendMode: null,
26
+ };
27
+ expect(() => drawWgpuColorTransformBitmap(state, fakeRenderProxy, canvas, 0, 0, 4, 4, 0, 0, 1, 1)).not.toThrow();
28
+ submitWgpuRenderPass(state);
29
+ });
30
+ });
31
+
32
+ describe('registerWgpuColorTransformShader', () => {
33
+ it('registers the color transform shader on the state', async () => {
34
+ const state = await createWgpuRenderStateForTest();
35
+ registerWgpuColorTransformShader(state);
36
+ expect(getWgpuRenderStateRuntime(state).colorTransformBitmapShader).toBeDefined();
37
+ });
38
+
39
+ it('is idempotent — calling twice does not throw', async () => {
40
+ const state = await createWgpuRenderStateForTest();
41
+ registerWgpuColorTransformShader(state);
42
+ expect(() => registerWgpuColorTransformShader(state)).not.toThrow();
43
+ });
44
+ });
@@ -0,0 +1,34 @@
1
+ import { getRenderProxy2D, prepareDisplayObjectRender } from '@flighthq/render';
2
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+ import { createParticleEmitter } from '@flighthq/sprite';
5
+
6
+ import { defaultWgpuParticleEmitterRenderer, drawWgpuParticleEmitter } from './wgpuParticleEmitter';
7
+
8
+ beforeAll(() => {
9
+ installWgpuMock();
10
+ });
11
+
12
+ describe('defaultWgpuParticleEmitterRenderer', () => {
13
+ it('has a createData function', () => {
14
+ expect(typeof defaultWgpuParticleEmitterRenderer.createData).toBe('function');
15
+ });
16
+
17
+ it('has a submit function', () => {
18
+ expect(typeof defaultWgpuParticleEmitterRenderer.submit).toBe('function');
19
+ });
20
+ });
21
+
22
+ describe('drawWgpuParticleEmitter', () => {
23
+ it('does not throw when atlas is null', async () => {
24
+ const state = await createWgpuRenderStateForTest();
25
+ renderWgpuBackground(state);
26
+
27
+ const emitter = createParticleEmitter();
28
+ prepareDisplayObjectRender(state, emitter);
29
+ const renderProxy = getRenderProxy2D(state, emitter)!;
30
+
31
+ expect(() => drawWgpuParticleEmitter(state, renderProxy)).not.toThrow();
32
+ submitWgpuRenderPass(state);
33
+ });
34
+ });
@@ -0,0 +1,69 @@
1
+ import { getRenderProxy2D, prepareDisplayObjectRender } from '@flighthq/render';
2
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+ import { createQuadBatch } from '@flighthq/sprite';
5
+
6
+ import { defaultWgpuQuadBatchRenderer, ensureWgpuQuadBatchResources, getWgpuQuadBatchPipeline } from './wgpuQuadBatch';
7
+ import { flushWgpuSpriteBatch } from './wgpuSpriteBatch';
8
+
9
+ beforeAll(() => {
10
+ installWgpuMock();
11
+ });
12
+
13
+ describe('defaultWgpuQuadBatchRenderer', () => {
14
+ it('has a createData function', () => {
15
+ expect(typeof defaultWgpuQuadBatchRenderer.createData).toBe('function');
16
+ });
17
+
18
+ it('has a submit function', () => {
19
+ expect(typeof defaultWgpuQuadBatchRenderer.submit).toBe('function');
20
+ });
21
+ });
22
+
23
+ describe('defaultWgpuQuadBatchRenderer.submit', () => {
24
+ it('does not throw when atlas is null', async () => {
25
+ const state = await createWgpuRenderStateForTest();
26
+ renderWgpuBackground(state);
27
+
28
+ const batch = createQuadBatch();
29
+ prepareDisplayObjectRender(state, batch);
30
+ const renderProxy = getRenderProxy2D(state, batch)!;
31
+
32
+ expect(() => {
33
+ defaultWgpuQuadBatchRenderer.submit(state, renderProxy);
34
+ flushWgpuSpriteBatch(state as any);
35
+ }).not.toThrow();
36
+ submitWgpuRenderPass(state);
37
+ });
38
+ });
39
+
40
+ describe('ensureWgpuQuadBatchResources', () => {
41
+ it('returns resources with bind group layouts and a pipelines WeakMap', async () => {
42
+ const state = await createWgpuRenderStateForTest();
43
+ const resources = ensureWgpuQuadBatchResources(state);
44
+ expect(resources.instanceBindGroupLayout).toBeDefined();
45
+ expect(resources.materialBindGroupLayout).toBeDefined();
46
+ expect(resources.basePipelineLayout).toBeDefined();
47
+ expect(resources.materialPipelineLayout).toBeDefined();
48
+ expect(resources.pipelines).toBeInstanceOf(WeakMap);
49
+ });
50
+
51
+ it('is idempotent — returns the same resources on repeated calls', async () => {
52
+ const state = await createWgpuRenderStateForTest();
53
+ const r1 = ensureWgpuQuadBatchResources(state);
54
+ const r2 = ensureWgpuQuadBatchResources(state);
55
+ expect(r1).toBe(r2);
56
+ });
57
+ });
58
+
59
+ describe('getWgpuQuadBatchPipeline', () => {
60
+ it('creates and caches a pipeline for a given shader module', async () => {
61
+ const state = await createWgpuRenderStateForTest();
62
+ const resources = ensureWgpuQuadBatchResources(state);
63
+ const module = state.device.createShaderModule({ code: '' });
64
+ const pipeline = getWgpuQuadBatchPipeline(state, resources, module, false, null);
65
+ expect(pipeline).toBeDefined();
66
+ const pipeline2 = getWgpuQuadBatchPipeline(state, resources, module, false, null);
67
+ expect(pipeline2).toBe(pipeline);
68
+ });
69
+ });