@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,19 @@
1
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
2
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
3
+ import { createSprite } from '@flighthq/sprite';
4
+
5
+ import { renderWgpuSprite } from './wgpuSprite';
6
+
7
+ beforeAll(() => {
8
+ installWgpuMock();
9
+ });
10
+
11
+ describe('renderWgpuSprite', () => {
12
+ it('traverses a sprite node without error', async () => {
13
+ const state = await createWgpuRenderStateForTest();
14
+ renderWgpuBackground(state);
15
+ const root = createSprite();
16
+ expect(() => renderWgpuSprite(state, root)).not.toThrow();
17
+ submitWgpuRenderPass(state);
18
+ });
19
+ });
@@ -0,0 +1,180 @@
1
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
2
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+ import type { Material } from '@flighthq/types';
5
+
6
+ import { defaultWgpuMaterialRenderer } from './wgpuDefaultMaterial';
7
+ import {
8
+ ensureWgpuQuadBatchResources,
9
+ flushWgpuSpriteBatch,
10
+ getWgpuQuadBatchPipeline,
11
+ getWgpuQuadBatchPreludeWGSL,
12
+ packWgpuSpriteBatchMaterialInstance,
13
+ prepareWgpuSpriteBatchWrite,
14
+ resetWgpuSpriteBatchBufferPool,
15
+ } from './wgpuSpriteBatch';
16
+
17
+ beforeAll(() => {
18
+ installWgpuMock();
19
+ });
20
+
21
+ function makeMaterial(): Material {
22
+ return { kind: 'TestMaterial' } as Material;
23
+ }
24
+
25
+ describe('ensureWgpuQuadBatchResources', () => {
26
+ it('returns resources with bind group layouts and a pipelines WeakMap', async () => {
27
+ const state = await createWgpuRenderStateForTest();
28
+ const resources = ensureWgpuQuadBatchResources(state);
29
+ expect(resources.instanceBindGroupLayout).toBeDefined();
30
+ expect(resources.materialBindGroupLayout).toBeDefined();
31
+ expect(resources.basePipelineLayout).toBeDefined();
32
+ expect(resources.materialPipelineLayout).toBeDefined();
33
+ expect(resources.pipelines).toBeInstanceOf(WeakMap);
34
+ });
35
+
36
+ it('is idempotent — returns the same resources on repeated calls', async () => {
37
+ const state = await createWgpuRenderStateForTest();
38
+ const r1 = ensureWgpuQuadBatchResources(state);
39
+ const r2 = ensureWgpuQuadBatchResources(state);
40
+ expect(r1).toBe(r2);
41
+ });
42
+ });
43
+
44
+ describe('flushWgpuSpriteBatch', () => {
45
+ it('does nothing when batch count is zero', async () => {
46
+ const state = await createWgpuRenderStateForTest();
47
+ renderWgpuBackground(state);
48
+ expect(() => flushWgpuSpriteBatch(state)).not.toThrow();
49
+ submitWgpuRenderPass(state);
50
+ });
51
+
52
+ it('resets state after flush', async () => {
53
+ const state = await createWgpuRenderStateForTest();
54
+ renderWgpuBackground(state);
55
+ const runtime = getWgpuRenderStateRuntime(state);
56
+ const tex = document.createElement('img');
57
+
58
+ prepareWgpuSpriteBatchWrite(state, tex, null, null, defaultWgpuMaterialRenderer, 1);
59
+ runtime.spriteBatchCount = 1;
60
+ flushWgpuSpriteBatch(state);
61
+
62
+ expect(runtime.spriteBatchCount).toBe(0);
63
+ expect(runtime.spriteBatchTexture).toBeNull();
64
+ expect(runtime.spriteBatchBlendMode).toBeNull();
65
+ expect(runtime.spriteBatchMaterial).toBeNull();
66
+ submitWgpuRenderPass(state);
67
+ });
68
+
69
+ it('claims a distinct buffer per flush so deferred draws never share one', async () => {
70
+ // The canvas pass is submitted once at end of frame; if successive flushes reused one instance
71
+ // buffer, every draw would read the last flush's data and the batch would collapse to one spot.
72
+ const state = await createWgpuRenderStateForTest();
73
+ renderWgpuBackground(state);
74
+ const runtime = getWgpuRenderStateRuntime(state);
75
+ const tex1 = document.createElement('img');
76
+ const tex2 = document.createElement('img');
77
+
78
+ prepareWgpuSpriteBatchWrite(state, tex1, null, null, defaultWgpuMaterialRenderer, 1);
79
+ runtime.spriteBatchCount = 1;
80
+ flushWgpuSpriteBatch(state);
81
+
82
+ prepareWgpuSpriteBatchWrite(state, tex2, null, null, defaultWgpuMaterialRenderer, 1);
83
+ runtime.spriteBatchCount = 1;
84
+ flushWgpuSpriteBatch(state);
85
+
86
+ expect(runtime.spriteBatchBufferCursor).toBe(2);
87
+ expect(runtime.spriteBatchBufferPool[0].instanceBuffer).not.toBeNull();
88
+ expect(runtime.spriteBatchBufferPool[0].instanceBuffer).not.toBe(runtime.spriteBatchBufferPool[1].instanceBuffer);
89
+ submitWgpuRenderPass(state);
90
+ });
91
+ });
92
+
93
+ describe('getWgpuQuadBatchPipeline', () => {
94
+ it('creates and caches a pipeline for a given shader module', async () => {
95
+ const state = await createWgpuRenderStateForTest();
96
+ const resources = ensureWgpuQuadBatchResources(state);
97
+ const module = state.device.createShaderModule({ code: '' });
98
+ const pipeline = getWgpuQuadBatchPipeline(state, resources, module, false, null);
99
+ expect(pipeline).toBeDefined();
100
+ const pipeline2 = getWgpuQuadBatchPipeline(state, resources, module, false, null);
101
+ expect(pipeline2).toBe(pipeline);
102
+ });
103
+ });
104
+
105
+ describe('getWgpuQuadBatchPreludeWGSL', () => {
106
+ it('returns the shared WGSL prelude string containing the base structs', () => {
107
+ const wgsl = getWgpuQuadBatchPreludeWGSL();
108
+ expect(typeof wgsl).toBe('string');
109
+ expect(wgsl).toContain('struct Uniforms');
110
+ expect(wgsl).toContain('quadBaseVertex');
111
+ });
112
+ });
113
+
114
+ describe('packWgpuSpriteBatchMaterialInstance', () => {
115
+ it('is a no-op when no per-instance material data is active', async () => {
116
+ const state = await createWgpuRenderStateForTest();
117
+ expect(() => packWgpuSpriteBatchMaterialInstance(state, null, 0)).not.toThrow();
118
+ });
119
+ });
120
+
121
+ describe('prepareWgpuSpriteBatchWrite', () => {
122
+ it('returns float index 0 for an empty batch', async () => {
123
+ const state = await createWgpuRenderStateForTest();
124
+ const tex = document.createElement('img');
125
+
126
+ const base = prepareWgpuSpriteBatchWrite(state, tex, null, null, defaultWgpuMaterialRenderer, 1);
127
+ expect(base).toBe(0);
128
+ });
129
+
130
+ it('flushes when texture changes', async () => {
131
+ const state = await createWgpuRenderStateForTest();
132
+ renderWgpuBackground(state);
133
+ const runtime = getWgpuRenderStateRuntime(state);
134
+ const tex1 = document.createElement('img');
135
+ const tex2 = document.createElement('img');
136
+
137
+ prepareWgpuSpriteBatchWrite(state, tex1, null, null, defaultWgpuMaterialRenderer, 1);
138
+ runtime.spriteBatchCount = 1;
139
+ prepareWgpuSpriteBatchWrite(state, tex2, null, null, defaultWgpuMaterialRenderer, 1);
140
+
141
+ expect(runtime.spriteBatchTexture).toBe(tex2);
142
+ expect(runtime.spriteBatchCount).toBe(0);
143
+ submitWgpuRenderPass(state);
144
+ });
145
+
146
+ it('flushes when material changes', async () => {
147
+ const state = await createWgpuRenderStateForTest();
148
+ renderWgpuBackground(state);
149
+ const runtime = getWgpuRenderStateRuntime(state);
150
+ const tex = document.createElement('img');
151
+ const materialA = makeMaterial();
152
+ const materialB = makeMaterial();
153
+
154
+ prepareWgpuSpriteBatchWrite(state, tex, null, materialA, defaultWgpuMaterialRenderer, 1);
155
+ runtime.spriteBatchCount = 1;
156
+ prepareWgpuSpriteBatchWrite(state, tex, null, materialB, defaultWgpuMaterialRenderer, 1);
157
+
158
+ expect(runtime.spriteBatchMaterial).toBe(materialB);
159
+ expect(runtime.spriteBatchCount).toBe(0);
160
+ submitWgpuRenderPass(state);
161
+ });
162
+ });
163
+
164
+ describe('resetWgpuSpriteBatchBufferPool', () => {
165
+ it('rewinds the pool cursor so slots are reclaimed next frame', async () => {
166
+ const state = await createWgpuRenderStateForTest();
167
+ renderWgpuBackground(state);
168
+ const runtime = getWgpuRenderStateRuntime(state);
169
+ const tex = document.createElement('img');
170
+
171
+ prepareWgpuSpriteBatchWrite(state, tex, null, null, defaultWgpuMaterialRenderer, 1);
172
+ runtime.spriteBatchCount = 1;
173
+ flushWgpuSpriteBatch(state);
174
+ expect(runtime.spriteBatchBufferCursor).toBe(1);
175
+
176
+ resetWgpuSpriteBatchBufferPool(state);
177
+ expect(runtime.spriteBatchBufferCursor).toBe(0);
178
+ submitWgpuRenderPass(state);
179
+ });
180
+ });
@@ -0,0 +1,38 @@
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 { createSprite } from '@flighthq/sprite';
5
+
6
+ import { flushWgpuSpriteBatch } from './wgpuSpriteBatch';
7
+ import { defaultWgpuSpriteRenderer } from './wgpuSpriteRenderer';
8
+
9
+ beforeAll(() => {
10
+ installWgpuMock();
11
+ });
12
+
13
+ describe('defaultWgpuSpriteRenderer', () => {
14
+ it('has a createData function', () => {
15
+ expect(typeof defaultWgpuSpriteRenderer.createData).toBe('function');
16
+ });
17
+
18
+ it('has a submit function', () => {
19
+ expect(typeof defaultWgpuSpriteRenderer.submit).toBe('function');
20
+ });
21
+ });
22
+
23
+ describe('defaultWgpuSpriteRenderer.submit', () => {
24
+ it('does not throw when atlas is null', async () => {
25
+ const state = await createWgpuRenderStateForTest();
26
+ renderWgpuBackground(state);
27
+
28
+ const sprite = createSprite();
29
+ prepareDisplayObjectRender(state, sprite);
30
+ const renderProxy = getRenderProxy2D(state, sprite)!;
31
+
32
+ expect(() => {
33
+ defaultWgpuSpriteRenderer.submit(state, renderProxy);
34
+ flushWgpuSpriteBatch(state as any);
35
+ }).not.toThrow();
36
+ submitWgpuRenderPass(state);
37
+ });
38
+ });
@@ -0,0 +1,37 @@
1
+ import { getOrCreateRenderProxy2D, prepareDisplayObjectRender } from '@flighthq/render';
2
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+ import { createRichText } from '@flighthq/text';
5
+ import { enableTextInput } from '@flighthq/textinput';
6
+
7
+ import { drawWgpuRichText } from './wgpuRichText';
8
+ import { drawWgpuTextInputOverlay, enableWgpuTextInput } from './wgpuTextInput';
9
+
10
+ beforeAll(() => {
11
+ installWgpuMock();
12
+ });
13
+
14
+ describe('drawWgpuTextInputOverlay', () => {
15
+ it('is the installed overlay function', () => {
16
+ expect(typeof drawWgpuTextInputOverlay).toBe('function');
17
+ });
18
+
19
+ it('rasterizes a focused editable field without throwing', async () => {
20
+ enableWgpuTextInput();
21
+ const state = await createWgpuRenderStateForTest();
22
+ renderWgpuBackground(state);
23
+ const input = createRichText({ data: { height: 40, text: 'hi', width: 100 } });
24
+ enableTextInput(input).focused = true;
25
+ prepareDisplayObjectRender(state, input);
26
+ const renderProxy = getOrCreateRenderProxy2D(state, input);
27
+
28
+ expect(() => drawWgpuRichText(state, renderProxy)).not.toThrow();
29
+ submitWgpuRenderPass(state);
30
+ });
31
+ });
32
+
33
+ describe('enableWgpuTextInput', () => {
34
+ it('installs the overlay without throwing', () => {
35
+ expect(() => enableWgpuTextInput()).not.toThrow();
36
+ });
37
+ });
@@ -0,0 +1,113 @@
1
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
2
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+ import { createTextLabel } from '@flighthq/text';
5
+ import type { RenderProxy2D } from '@flighthq/types';
6
+ import { BatchFormat } from '@flighthq/types';
7
+
8
+ import { scopeModuleMocks } from './moduleMockTestHelper';
9
+ import { registerDefaultWgpuMaterial } from './wgpuDefaultMaterial';
10
+ import type * as WgpuTextLabelModule from './wgpuTextLabel';
11
+
12
+ // @flighthq/textlayout.computeTextLayout is stubbed to emit one deterministic glyph group.
13
+ // scopeModuleMocks scopes the stub to this file (registry reset before the mock applies, unmock +
14
+ // reset after), so under a shared (isolate:false) worker it never leaks into the real text-layout
15
+ // consumers (text package) — and a sibling that pre-evaluated ./wgpuTextLabel still picks up the stub.
16
+ let defaultWgpuTextLabelRenderer: typeof WgpuTextLabelModule.defaultWgpuTextLabelRenderer;
17
+ let drawWgpuTextLabel: typeof WgpuTextLabelModule.drawWgpuTextLabel;
18
+
19
+ scopeModuleMocks(['@flighthq/textlayout']);
20
+
21
+ beforeAll(async () => {
22
+ vi.doMock('@flighthq/textlayout', async (importOriginal) => {
23
+ const actual = (await importOriginal()) as Record<string, unknown>;
24
+ return {
25
+ ...actual,
26
+ computeTextLayout: vi.fn((result: { groups: object[] }) => {
27
+ result.groups.push({
28
+ offsetX: 0,
29
+ offsetY: 0,
30
+ width: 50,
31
+ ascent: 12,
32
+ descent: 4,
33
+ format: {},
34
+ startIndex: 0,
35
+ endIndex: 5,
36
+ });
37
+ }),
38
+ };
39
+ });
40
+ ({ defaultWgpuTextLabelRenderer, drawWgpuTextLabel } = await import('./wgpuTextLabel'));
41
+ installWgpuMock();
42
+ });
43
+
44
+ function makeTextData() {
45
+ const canvas = document.createElement('canvas');
46
+ canvas.width = 1;
47
+ canvas.height = 1;
48
+ const ctx = canvas.getContext('2d')!;
49
+ return { canvas, ctx, lastContentId: -1, lastPixelRatio: 0, logW: 0, logH: 0, lastPW: 0, lastPH: 0 };
50
+ }
51
+
52
+ function makeTextProxy(text = '', rendererData: unknown = null): RenderProxy2D {
53
+ const source = createTextLabel();
54
+ source.data.text = text;
55
+ source.data.textFormat = {};
56
+ source.data.width = 200;
57
+ source.data.height = 100;
58
+ return {
59
+ source,
60
+ blendMode: 0,
61
+ alpha: 1,
62
+ material: null,
63
+ materialData: null,
64
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
65
+ rendererData,
66
+ } as unknown as RenderProxy2D;
67
+ }
68
+
69
+ describe('defaultWgpuTextLabelRenderer', () => {
70
+ it('declares BatchFormat.Quad', () => {
71
+ expect(defaultWgpuTextLabelRenderer.format).toBe(BatchFormat.Quad);
72
+ });
73
+
74
+ it('has createData and submit functions', () => {
75
+ expect(typeof defaultWgpuTextLabelRenderer.createData).toBe('function');
76
+ expect(typeof defaultWgpuTextLabelRenderer.submit).toBe('function');
77
+ });
78
+ });
79
+
80
+ describe('drawWgpuTextLabel', () => {
81
+ it('returns early when text is empty', async () => {
82
+ const state = await createWgpuRenderStateForTest();
83
+ renderWgpuBackground(state);
84
+ registerDefaultWgpuMaterial(state);
85
+ expect(() => drawWgpuTextLabel(state, makeTextProxy('', makeTextData()))).not.toThrow();
86
+ expect(getWgpuRenderStateRuntime(state).spriteBatchCount).toBe(0);
87
+ submitWgpuRenderPass(state);
88
+ });
89
+
90
+ it('writes one instance to the sprite batch when text has content', async () => {
91
+ const state = await createWgpuRenderStateForTest();
92
+ renderWgpuBackground(state);
93
+ registerDefaultWgpuMaterial(state);
94
+ drawWgpuTextLabel(state, makeTextProxy('hello', makeTextData()));
95
+ expect(getWgpuRenderStateRuntime(state).spriteBatchCount).toBe(1);
96
+ submitWgpuRenderPass(state);
97
+ });
98
+
99
+ it('does not re-rasterize when only alpha changes (content version unchanged)', async () => {
100
+ const state = await createWgpuRenderStateForTest();
101
+ renderWgpuBackground(state);
102
+ registerDefaultWgpuMaterial(state);
103
+ const proxy = makeTextProxy('hello', makeTextData());
104
+ drawWgpuTextLabel(state, proxy);
105
+ const updateSpy = vi.spyOn(getWgpuRenderStateRuntime(state).textureCache, 'get');
106
+ proxy.alpha = 0.5;
107
+ drawWgpuTextLabel(state, proxy);
108
+ // Version is unchanged, so the rasterization block is skipped entirely on the second draw.
109
+ expect((proxy.rendererData as any).lastContentId).toBe(0);
110
+ submitWgpuRenderPass(state);
111
+ updateSpy.mockRestore();
112
+ });
113
+ });
@@ -0,0 +1,47 @@
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 { createTilemap } from '@flighthq/sprite';
5
+
6
+ import { flushWgpuSpriteBatch } from './wgpuSpriteBatch';
7
+ import { defaultWgpuTilemapRenderer } from './wgpuTilemap';
8
+
9
+ beforeAll(() => {
10
+ installWgpuMock();
11
+ });
12
+
13
+ describe('defaultWgpuTilemapRenderer', () => {
14
+ it('has a createData function', () => {
15
+ expect(typeof defaultWgpuTilemapRenderer.createData).toBe('function');
16
+ });
17
+
18
+ it('has a submit function', () => {
19
+ expect(typeof defaultWgpuTilemapRenderer.submit).toBe('function');
20
+ });
21
+ });
22
+
23
+ describe('defaultWgpuTilemapRenderer.submit', () => {
24
+ it('does not throw when tileset is null', async () => {
25
+ const state = await createWgpuRenderStateForTest();
26
+ renderWgpuBackground(state);
27
+
28
+ const tilemap = createTilemap();
29
+ prepareDisplayObjectRender(state, tilemap);
30
+ const renderProxy = getRenderProxy2D(state, tilemap)!;
31
+
32
+ expect(() => {
33
+ defaultWgpuTilemapRenderer.submit(state, renderProxy);
34
+ flushWgpuSpriteBatch(state as any);
35
+ }).not.toThrow();
36
+ submitWgpuRenderPass(state);
37
+ });
38
+
39
+ it('does not throw when renderPass is null', async () => {
40
+ const state = await createWgpuRenderStateForTest();
41
+ const tilemap = createTilemap();
42
+ prepareDisplayObjectRender(state, tilemap);
43
+ const renderProxy = getRenderProxy2D(state, tilemap)!;
44
+
45
+ expect(() => defaultWgpuTilemapRenderer.submit(state, renderProxy)).not.toThrow();
46
+ });
47
+ });
@@ -0,0 +1,147 @@
1
+ import { createDisplayObject } from '@flighthq/displayobject';
2
+ import { renderWgpuBackground } from '@flighthq/render-wgpu';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
4
+ import { createParticleEmitter, createQuadBatch, getQuadBatchRuntime, reserveParticleEmitter } from '@flighthq/sprite';
5
+ import type { QuadBatchRuntime, TextureAtlas, TextureAtlasRegion } from '@flighthq/types';
6
+ import { QuadBatchKind } from '@flighthq/types';
7
+ import { beginVelocityFrame, contributeVelocity, createVelocityField } from '@flighthq/velocity';
8
+ import { beforeAll, describe, expect, it } from 'vitest';
9
+
10
+ import {
11
+ createWgpuVelocityTarget,
12
+ defaultWgpuDisplayObjectVelocityWriter,
13
+ defaultWgpuParticleEmitterVelocityWriter,
14
+ defaultWgpuQuadBatchVelocityWriter,
15
+ drawWgpuVelocityQuad,
16
+ getWgpuVelocityWriter,
17
+ registerWgpuVelocityWriter,
18
+ renderWgpuVelocity,
19
+ } from './wgpuVelocity';
20
+
21
+ beforeAll(() => {
22
+ installWgpuMock();
23
+ });
24
+
25
+ describe('createWgpuVelocityTarget', () => {
26
+ it('allocates an rgba16float target at the requested size', async () => {
27
+ const state = await createWgpuRenderStateForTest();
28
+ const target = createWgpuVelocityTarget(state, 128, 64);
29
+ expect(target.format).toBe('rgba16float');
30
+ expect(target.width).toBe(128);
31
+ expect(target.height).toBe(64);
32
+ });
33
+ });
34
+
35
+ describe('defaultWgpuDisplayObjectVelocityWriter', () => {
36
+ it('is a velocity writer function', () => {
37
+ expect(typeof defaultWgpuDisplayObjectVelocityWriter).toBe('function');
38
+ });
39
+ });
40
+
41
+ describe('defaultWgpuParticleEmitterVelocityWriter', () => {
42
+ it('is a velocity writer function', () => {
43
+ expect(typeof defaultWgpuParticleEmitterVelocityWriter).toBe('function');
44
+ });
45
+
46
+ it('emits per-particle velocity for an emitter with a velocities array without throwing', async () => {
47
+ const state = await createWgpuRenderStateForTest();
48
+ const target = createWgpuVelocityTarget(state, 128, 64);
49
+ const region = { id: 0, x: 0, y: 0, width: 16, height: 16, pivotX: null, pivotY: null } as TextureAtlasRegion;
50
+ const atlas = { image: null, regions: [region] } as TextureAtlas;
51
+ const emitter = createParticleEmitter();
52
+ reserveParticleEmitter(emitter, 2);
53
+ emitter.data.atlas = atlas;
54
+ emitter.data.particleCount = 2;
55
+ emitter.data.ids[0] = 0;
56
+ emitter.data.ids[1] = 0;
57
+ emitter.data.transforms.set([10, 10, 0, 1, 40, 20, 0, 1]);
58
+ emitter.data.velocities.set([3, -2, -1, 4]);
59
+
60
+ registerWgpuVelocityWriter(state, emitter.kind, defaultWgpuParticleEmitterVelocityWriter);
61
+ const field = createVelocityField();
62
+ beginVelocityFrame(field);
63
+
64
+ renderWgpuBackground(state);
65
+ expect(() => renderWgpuVelocity(state, emitter, field, target)).not.toThrow();
66
+ });
67
+ });
68
+
69
+ describe('defaultWgpuQuadBatchVelocityWriter', () => {
70
+ it('is a velocity writer function', () => {
71
+ expect(typeof defaultWgpuQuadBatchVelocityWriter).toBe('function');
72
+ });
73
+
74
+ it('emits per-instance velocity for a batch with an instanceVelocities array without throwing', async () => {
75
+ const state = await createWgpuRenderStateForTest();
76
+ const target = createWgpuVelocityTarget(state, 128, 64);
77
+ const region = { id: 0, x: 0, y: 0, width: 32, height: 32, pivotX: null, pivotY: null } as TextureAtlasRegion;
78
+ const atlas = { image: null, regions: [region] } as TextureAtlas;
79
+ const batch = createQuadBatch({
80
+ data: {
81
+ atlas,
82
+ ids: new Uint16Array([0, 0]),
83
+ instanceCount: 2,
84
+ transforms: new Float32Array([0, 0, 40, 10]),
85
+ transformType: 'vector2',
86
+ },
87
+ });
88
+ (getQuadBatchRuntime(batch) as QuadBatchRuntime).instanceVelocities = new Float32Array([3, -2, -1, 4]);
89
+
90
+ registerWgpuVelocityWriter(state, QuadBatchKind, defaultWgpuQuadBatchVelocityWriter);
91
+ const field = createVelocityField();
92
+ beginVelocityFrame(field);
93
+
94
+ renderWgpuBackground(state);
95
+ expect(() => renderWgpuVelocity(state, batch, field, target)).not.toThrow();
96
+ });
97
+ });
98
+
99
+ describe('drawWgpuVelocityQuad', () => {
100
+ it('is a no-op outside an active velocity pass', async () => {
101
+ const state = await createWgpuRenderStateForTest();
102
+ const ctx = { state, field: createVelocityField(), width: 128, height: 64, pixelRatio: 1 };
103
+ expect(() => drawWgpuVelocityQuad(ctx, 0, 0, 10, 10, 1, 0)).not.toThrow();
104
+ });
105
+ });
106
+
107
+ describe('getWgpuVelocityWriter', () => {
108
+ it('returns null for an unregistered kind', async () => {
109
+ const state = await createWgpuRenderStateForTest();
110
+ expect(getWgpuVelocityWriter(state, 'unregistered')).toBeNull();
111
+ });
112
+ });
113
+
114
+ describe('registerWgpuVelocityWriter', () => {
115
+ it('registers a writer dispatched by kind', async () => {
116
+ const state = await createWgpuRenderStateForTest();
117
+ const root = createDisplayObject();
118
+ registerWgpuVelocityWriter(state, root.kind, defaultWgpuDisplayObjectVelocityWriter);
119
+ expect(getWgpuVelocityWriter(state, root.kind)).toBe(defaultWgpuDisplayObjectVelocityWriter);
120
+ });
121
+ });
122
+
123
+ describe('renderWgpuVelocity', () => {
124
+ it('dispatches the registered writer for a moving node without throwing', async () => {
125
+ const state = await createWgpuRenderStateForTest();
126
+ const target = createWgpuVelocityTarget(state, 128, 64);
127
+ const root = createDisplayObject();
128
+ registerWgpuVelocityWriter(state, root.kind, defaultWgpuDisplayObjectVelocityWriter);
129
+
130
+ const field = createVelocityField();
131
+ beginVelocityFrame(field);
132
+ contributeVelocity(field, root, 3, -2);
133
+
134
+ renderWgpuBackground(state);
135
+ expect(() => renderWgpuVelocity(state, root, field, target)).not.toThrow();
136
+ });
137
+
138
+ it('throws when no command encoder is open (renderWgpuBackground not called)', async () => {
139
+ const state = await createWgpuRenderStateForTest();
140
+ const target = createWgpuVelocityTarget(state, 128, 64);
141
+ const root = createDisplayObject();
142
+ const field = createVelocityField();
143
+ beginVelocityFrame(field);
144
+
145
+ expect(() => renderWgpuVelocity(state, root, field, target)).toThrow(/command encoder/);
146
+ });
147
+ });
@@ -0,0 +1,59 @@
1
+ import { createVideo } from '@flighthq/displayobject';
2
+ import { getOrCreateRenderProxy2D, prepareDisplayObjectRender } from '@flighthq/render';
3
+ import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu';
4
+ import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu';
5
+
6
+ import { createWgpuVideoData, defaultWgpuVideoRenderer, destroyWgpuVideoData, drawWgpuVideo } from './wgpuVideo';
7
+
8
+ beforeAll(() => {
9
+ installWgpuMock();
10
+ });
11
+
12
+ describe('createWgpuVideoData', () => {
13
+ it('allocates per-node data with no texture entry yet', () => {
14
+ const data = createWgpuVideoData({} as never, {} as never) as unknown as { entry: unknown };
15
+ expect(data.entry).toBeNull();
16
+ });
17
+ });
18
+
19
+ describe('defaultWgpuVideoRenderer', () => {
20
+ it('has createData and submit functions', () => {
21
+ expect(typeof defaultWgpuVideoRenderer.createData).toBe('function');
22
+ expect(typeof defaultWgpuVideoRenderer.submit).toBe('function');
23
+ });
24
+ });
25
+
26
+ describe('destroyWgpuVideoData', () => {
27
+ it('destroys the GPU texture the node owns', () => {
28
+ const destroy = vi.fn();
29
+ destroyWgpuVideoData({} as never, { entry: { texture: { destroy } } } as never);
30
+ expect(destroy).toHaveBeenCalled();
31
+ });
32
+
33
+ it('is a no-op when no texture entry was allocated', () => {
34
+ expect(() => destroyWgpuVideoData({} as never, { entry: null } as never)).not.toThrow();
35
+ });
36
+ });
37
+
38
+ describe('drawWgpuVideo', () => {
39
+ it('does not throw when video source is null', async () => {
40
+ const state = await createWgpuRenderStateForTest();
41
+ renderWgpuBackground(state);
42
+
43
+ const video = createVideo();
44
+ prepareDisplayObjectRender(state, video);
45
+ const renderProxy = getOrCreateRenderProxy2D(state, video);
46
+
47
+ expect(() => drawWgpuVideo(state, renderProxy)).not.toThrow();
48
+ submitWgpuRenderPass(state);
49
+ });
50
+
51
+ it('does not throw when renderPass is null', async () => {
52
+ const state = await createWgpuRenderStateForTest();
53
+ const video = createVideo();
54
+ prepareDisplayObjectRender(state, video);
55
+ const renderProxy = getOrCreateRenderProxy2D(state, video);
56
+
57
+ expect(() => drawWgpuVideo(state, renderProxy)).not.toThrow();
58
+ });
59
+ });