@flighthq/scene2d-wgpu 0.3.0-next.906.07cea63 → 0.3.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 (128) hide show
  1. package/dist/contract.d.ts +8 -6
  2. package/dist/contract.d.ts.map +1 -1
  3. package/dist/contract.js +8 -6
  4. package/dist/contract.js.map +1 -1
  5. package/dist/enableWgpuColorAdjustmentGuards.js +2 -2
  6. package/dist/enableWgpuColorAdjustmentGuards.js.map +1 -1
  7. package/dist/enableWgpuStrokePathTessellation.d.ts +3 -0
  8. package/dist/enableWgpuStrokePathTessellation.d.ts.map +1 -0
  9. package/dist/enableWgpuStrokePathTessellation.js +8 -0
  10. package/dist/enableWgpuStrokePathTessellation.js.map +1 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/wgpuBitmapText.d.ts.map +1 -1
  16. package/dist/wgpuBitmapText.js +20 -16
  17. package/dist/wgpuBitmapText.js.map +1 -1
  18. package/dist/wgpuCache.d.ts.map +1 -1
  19. package/dist/wgpuCache.js +19 -16
  20. package/dist/wgpuCache.js.map +1 -1
  21. package/dist/wgpuClipContours.js +3 -3
  22. package/dist/wgpuClipContours.js.map +1 -1
  23. package/dist/wgpuClipRectangle.js +3 -3
  24. package/dist/wgpuClipRectangle.js.map +1 -1
  25. package/dist/wgpuColorAdjustmentMaterialFeature.d.ts.map +1 -1
  26. package/dist/wgpuColorAdjustmentMaterialFeature.js +156 -67
  27. package/dist/wgpuColorAdjustmentMaterialFeature.js.map +1 -1
  28. package/dist/wgpuMeshShapeRenderer.d.ts +4 -0
  29. package/dist/wgpuMeshShapeRenderer.d.ts.map +1 -0
  30. package/dist/wgpuMeshShapeRenderer.js +92 -0
  31. package/dist/wgpuMeshShapeRenderer.js.map +1 -0
  32. package/dist/wgpuNode2D.js +2 -2
  33. package/dist/wgpuNode2D.js.map +1 -1
  34. package/dist/wgpuParticleEmitter2D.d.ts.map +1 -1
  35. package/dist/wgpuParticleEmitter2D.js +26 -10
  36. package/dist/wgpuParticleEmitter2D.js.map +1 -1
  37. package/dist/wgpuQuadBatch.d.ts +1 -1
  38. package/dist/wgpuQuadBatch.d.ts.map +1 -1
  39. package/dist/wgpuQuadBatch.js +18 -13
  40. package/dist/wgpuQuadBatch.js.map +1 -1
  41. package/dist/wgpuQuadBatchWriter.d.ts +12 -0
  42. package/dist/wgpuQuadBatchWriter.d.ts.map +1 -0
  43. package/dist/{wgpuSpriteBatch.js → wgpuQuadBatchWriter.js} +97 -72
  44. package/dist/wgpuQuadBatchWriter.js.map +1 -0
  45. package/dist/wgpuRasterShapeRenderer.d.ts +4 -0
  46. package/dist/wgpuRasterShapeRenderer.d.ts.map +1 -0
  47. package/dist/wgpuRasterShapeRenderer.js +110 -0
  48. package/dist/wgpuRasterShapeRenderer.js.map +1 -0
  49. package/dist/wgpuRichText.d.ts.map +1 -1
  50. package/dist/wgpuRichText.js +7 -4
  51. package/dist/wgpuRichText.js.map +1 -1
  52. package/dist/wgpuScale9Shape.d.ts +1 -3
  53. package/dist/wgpuScale9Shape.d.ts.map +1 -1
  54. package/dist/wgpuScale9Shape.js +36 -33
  55. package/dist/wgpuScale9Shape.js.map +1 -1
  56. package/dist/wgpuShape.d.ts +2 -1
  57. package/dist/wgpuShape.d.ts.map +1 -1
  58. package/dist/wgpuShape.js +19 -153
  59. package/dist/wgpuShape.js.map +1 -1
  60. package/dist/wgpuShapeData.d.ts +6 -0
  61. package/dist/wgpuShapeData.d.ts.map +1 -0
  62. package/dist/wgpuShapeData.js +81 -0
  63. package/dist/wgpuShapeData.js.map +1 -0
  64. package/dist/wgpuShapeMesh.d.ts +2 -1
  65. package/dist/wgpuShapeMesh.d.ts.map +1 -1
  66. package/dist/wgpuShapeMesh.js +74 -62
  67. package/dist/wgpuShapeMesh.js.map +1 -1
  68. package/dist/wgpuShapeRasterizer.d.ts +4 -0
  69. package/dist/wgpuShapeRasterizer.d.ts.map +1 -0
  70. package/dist/wgpuShapeRasterizer.js +11 -0
  71. package/dist/wgpuShapeRasterizer.js.map +1 -0
  72. package/dist/wgpuSprite.d.ts +3 -2
  73. package/dist/wgpuSprite.d.ts.map +1 -1
  74. package/dist/wgpuSprite.js +67 -25
  75. package/dist/wgpuSprite.js.map +1 -1
  76. package/dist/wgpuStandardMaterial.d.ts +1 -1
  77. package/dist/wgpuStandardMaterial.js +3 -3
  78. package/dist/wgpuStandardMaterial.js.map +1 -1
  79. package/dist/wgpuTextLabel.d.ts.map +1 -1
  80. package/dist/wgpuTextLabel.js +15 -12
  81. package/dist/wgpuTextLabel.js.map +1 -1
  82. package/dist/wgpuTilemap.d.ts.map +1 -1
  83. package/dist/wgpuTilemap.js +19 -18
  84. package/dist/wgpuTilemap.js.map +1 -1
  85. package/package.json +22 -18
  86. package/src/enableWgpuColorAdjustmentGuards.test.ts +4 -4
  87. package/src/enableWgpuStrokePathTessellation.test.ts +23 -0
  88. package/src/wgpuBitmapText.test.ts +2 -2
  89. package/src/wgpuCache.test.ts +14 -11
  90. package/src/wgpuColorAdjustmentMaterialFeature.test.ts +52 -46
  91. package/src/wgpuMeshShapeRenderer.test.ts +167 -0
  92. package/src/wgpuParticleEmitter2D.test.ts +16 -7
  93. package/src/wgpuQuadBatch.test.ts +3 -3
  94. package/src/{wgpuSpriteBatch.test.ts → wgpuQuadBatchWriter.test.ts} +79 -70
  95. package/src/wgpuRasterShapeRenderer.test.ts +156 -0
  96. package/src/wgpuScale9Shape.test.ts +0 -13
  97. package/src/wgpuShape.test.ts +169 -15
  98. package/src/wgpuShapeData.test.ts +97 -0
  99. package/src/wgpuShapeMesh.test.ts +170 -15
  100. package/src/wgpuShapeRasterizer.test.ts +36 -0
  101. package/src/wgpuSprite.test.ts +45 -10
  102. package/src/wgpuStandardMaterial.test.ts +3 -3
  103. package/src/wgpuTextLabel.test.ts +7 -7
  104. package/src/wgpuTilemap.test.ts +4 -4
  105. package/src/wgpuVelocity.test.ts +3 -3
  106. package/dist/wgpuBitmap.d.ts +0 -4
  107. package/dist/wgpuBitmap.d.ts.map +0 -1
  108. package/dist/wgpuBitmap.js +0 -84
  109. package/dist/wgpuBitmap.js.map +0 -1
  110. package/dist/wgpuRenderTargetNode2D.d.ts +0 -11
  111. package/dist/wgpuRenderTargetNode2D.d.ts.map +0 -1
  112. package/dist/wgpuRenderTargetNode2D.js +0 -100
  113. package/dist/wgpuRenderTargetNode2D.js.map +0 -1
  114. package/dist/wgpuSpriteBatch.d.ts +0 -12
  115. package/dist/wgpuSpriteBatch.d.ts.map +0 -1
  116. package/dist/wgpuSpriteBatch.js.map +0 -1
  117. package/dist/wgpuSpriteRenderer.d.ts +0 -3
  118. package/dist/wgpuSpriteRenderer.d.ts.map +0 -1
  119. package/dist/wgpuSpriteRenderer.js +0 -55
  120. package/dist/wgpuSpriteRenderer.js.map +0 -1
  121. package/dist/wgpuVideo.d.ts +0 -6
  122. package/dist/wgpuVideo.d.ts.map +0 -1
  123. package/dist/wgpuVideo.js +0 -63
  124. package/dist/wgpuVideo.js.map +0 -1
  125. package/src/wgpuBitmap.test.ts +0 -40
  126. package/src/wgpuRenderTargetNode2D.test.ts +0 -267
  127. package/src/wgpuSpriteRenderer.test.ts +0 -63
  128. package/src/wgpuVideo.test.ts +0 -59
@@ -0,0 +1,167 @@
1
+ import type * as FlightNodeModule from '@flighthq/node/contract';
2
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu/contract';
3
+ import { createWgpuRenderStateForTest, installWgpuMock, renderWgpuBackground } from '@flighthq/render-wgpu/contract';
4
+ import { enableRenderRegistryGuards, explainRenderRegistryMisses } from '@flighthq/render/contract';
5
+ import {
6
+ appendShapeBeginFill,
7
+ appendShapeBeginGradientFill,
8
+ appendShapeEndFill,
9
+ appendShapeRectangle,
10
+ createShape,
11
+ } from '@flighthq/shape/contract';
12
+ import type { RenderProxy2D } from '@flighthq/types/contract';
13
+ import { BatchFormat, RenderRegistry } from '@flighthq/types/contract';
14
+
15
+ import { scopeModuleMocks } from './moduleMockTestHelper';
16
+ import type * as WgpuMeshShapeModule from './wgpuMeshShapeRenderer';
17
+ import { registerWgpuShapeRasterizer } from './wgpuShapeRasterizer';
18
+
19
+ let defaultWgpuMeshShapeRenderer: typeof WgpuMeshShapeModule.defaultWgpuMeshShapeRenderer;
20
+ let drawWgpuMeshShape: typeof WgpuMeshShapeModule.drawWgpuMeshShape;
21
+
22
+ scopeModuleMocks(['@flighthq/node']);
23
+
24
+ beforeAll(async () => {
25
+ vi.doMock('@flighthq/node/contract', async (importOriginal) => ({
26
+ ...(await importOriginal<typeof FlightNodeModule>()),
27
+ getNodeLocalBoundsRectangle: () => ({ x: 0, y: 0, width: 64, height: 48 }),
28
+ getNodeLocalContentRevision: (source: any) => source?.data?.version ?? 0,
29
+ }));
30
+ ({ defaultWgpuMeshShapeRenderer, drawWgpuMeshShape } = await import('./wgpuMeshShapeRenderer'));
31
+ installWgpuMock();
32
+ });
33
+
34
+ function makeShapeData() {
35
+ return {
36
+ surface: null,
37
+ lastContentId: -1,
38
+ lastPixelRatio: 0,
39
+ lastW: 0,
40
+ lastH: 0,
41
+ meshVersion: -1,
42
+ meshes: null,
43
+ meshBuffers: {
44
+ vertexBuffers: [],
45
+ vertexCapacities: [],
46
+ indexBuffers: [],
47
+ indexCapacities: [],
48
+ uniformBuffers: [],
49
+ bindGroups: [],
50
+ colorScaleBiasUniformBuffers: [],
51
+ colorScaleBiasBindGroups: [],
52
+ },
53
+ };
54
+ }
55
+
56
+ function makeShapeProxy(data: Record<string, unknown>, rendererData: unknown = makeShapeData()): RenderProxy2D {
57
+ return {
58
+ source: { data: { commands: [], version: 0, ...data } },
59
+ blendMode: 0,
60
+ alpha: 1,
61
+ material: null,
62
+ materialData: null,
63
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
64
+ rendererData,
65
+ } as unknown as RenderProxy2D;
66
+ }
67
+
68
+ function makeMeshPassSpy(): GPURenderPassEncoder {
69
+ return {
70
+ setPipeline: vi.fn(),
71
+ setBindGroup: vi.fn(),
72
+ setVertexBuffer: vi.fn(),
73
+ setIndexBuffer: vi.fn(),
74
+ setStencilReference: vi.fn(),
75
+ draw: vi.fn(),
76
+ drawIndexed: vi.fn(),
77
+ end: vi.fn(),
78
+ } as unknown as GPURenderPassEncoder;
79
+ }
80
+
81
+ function gradientShape() {
82
+ const shape = createShape();
83
+ appendShapeBeginGradientFill(shape, 'linear', [0xff0000, 0x0000ff], [1, 1], [0, 255], null);
84
+ appendShapeRectangle(shape, 8, 8, 32, 24);
85
+ appendShapeEndFill(shape);
86
+ return shape;
87
+ }
88
+
89
+ describe('defaultWgpuMeshShapeRenderer', () => {
90
+ it('declares BatchFormat.Quad and the shared shape data lifecycle', () => {
91
+ expect(defaultWgpuMeshShapeRenderer.format).toBe(BatchFormat.Quad);
92
+ expect(typeof defaultWgpuMeshShapeRenderer.createData).toBe('function');
93
+ expect(typeof defaultWgpuMeshShapeRenderer.destroyData).toBe('function');
94
+ });
95
+
96
+ it('never rasterizes, even with a rasterizer registered and a fill that cannot tessellate', async () => {
97
+ const state = await createWgpuRenderStateForTest();
98
+ renderWgpuBackground(state);
99
+ getWgpuRenderStateRuntime(state).renderPass = makeMeshPassSpy();
100
+ const rasterizer = vi.fn();
101
+ registerWgpuShapeRasterizer(state, rasterizer);
102
+
103
+ defaultWgpuMeshShapeRenderer.submit!(
104
+ state,
105
+ makeShapeProxy({ commands: gradientShape().data.commands, version: 1 }),
106
+ );
107
+
108
+ expect(rasterizer).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it('reports a ShapeRasterizer miss rather than silently dropping an untessellatable fill', async () => {
112
+ const state = await createWgpuRenderStateForTest();
113
+ renderWgpuBackground(state);
114
+ getWgpuRenderStateRuntime(state).renderPass = makeMeshPassSpy();
115
+ enableRenderRegistryGuards(state);
116
+
117
+ defaultWgpuMeshShapeRenderer.submit!(
118
+ state,
119
+ makeShapeProxy({ commands: gradientShape().data.commands, version: 1 }),
120
+ );
121
+
122
+ expect(explainRenderRegistryMisses(state).misses).toContainEqual({
123
+ kind: 'Shape',
124
+ registry: RenderRegistry.ShapeRasterizer,
125
+ });
126
+ });
127
+ });
128
+
129
+ describe('drawWgpuMeshShape', () => {
130
+ it('draws a solid fill as a GPU mesh and reports that it drew', async () => {
131
+ const state = await createWgpuRenderStateForTest();
132
+ renderWgpuBackground(state);
133
+ const pass = makeMeshPassSpy();
134
+ getWgpuRenderStateRuntime(state).renderPass = pass;
135
+ const shape = createShape();
136
+ appendShapeBeginFill(shape, 0x00cc00);
137
+ appendShapeRectangle(shape, 8, 8, 32, 24);
138
+ appendShapeEndFill(shape);
139
+
140
+ expect(drawWgpuMeshShape(state, makeShapeProxy({ commands: shape.data.commands, version: 1 }))).toBe(true);
141
+ expect(pass.drawIndexed).toHaveBeenCalled();
142
+ });
143
+
144
+ it('reports false for a fill with no tessellated form, which is the hybrid fall-through signal', async () => {
145
+ const state = await createWgpuRenderStateForTest();
146
+ renderWgpuBackground(state);
147
+ getWgpuRenderStateRuntime(state).renderPass = makeMeshPassSpy();
148
+ expect(drawWgpuMeshShape(state, makeShapeProxy({ commands: gradientShape().data.commands, version: 1 }))).toBe(
149
+ false,
150
+ );
151
+ });
152
+
153
+ it('reports false with no render pass, an empty command list, or absent renderer data', async () => {
154
+ const state = await createWgpuRenderStateForTest();
155
+ renderWgpuBackground(state);
156
+ const shape = createShape();
157
+ appendShapeBeginFill(shape, 0x00cc00);
158
+ appendShapeRectangle(shape, 8, 8, 32, 24);
159
+ appendShapeEndFill(shape);
160
+
161
+ getWgpuRenderStateRuntime(state).renderPass = null;
162
+ expect(drawWgpuMeshShape(state, makeShapeProxy({ commands: shape.data.commands, version: 1 }))).toBe(false);
163
+ getWgpuRenderStateRuntime(state).renderPass = makeMeshPassSpy();
164
+ expect(drawWgpuMeshShape(state, makeShapeProxy({ commands: [], version: 1 }))).toBe(false);
165
+ expect(drawWgpuMeshShape(state, makeShapeProxy({ commands: shape.data.commands, version: 1 }, null))).toBe(false);
166
+ });
167
+ });
@@ -1,8 +1,15 @@
1
1
  import { createParticleEmitter2D } from '@flighthq/particleemitter/contract';
2
- import { getWgpuRenderStateRuntime, renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu/contract';
2
+ import {
3
+ getWgpuRenderStateRuntime,
4
+ registerWgpuCompressedImageTextureResolver,
5
+ renderWgpuBackground,
6
+ submitWgpuRenderPass,
7
+ } from '@flighthq/render-wgpu/contract';
3
8
  import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu/contract';
4
9
  import { getRenderProxy2D, prepareScene2DRender } from '@flighthq/render/contract';
5
- import type { ImageResource, RenderProxy2D } from '@flighthq/types/contract';
10
+ import { createTexture } from '@flighthq/texture/contract';
11
+ import type { CompressedImage, RenderProxy2D } from '@flighthq/types/contract';
12
+ import { CompressedImageTextureSourceKind } from '@flighthq/types/contract';
6
13
 
7
14
  import { defaultWgpuParticleEmitter2DRenderer, drawWgpuParticleEmitter2D } from './wgpuParticleEmitter2D';
8
15
 
@@ -36,17 +43,16 @@ describe('drawWgpuParticleEmitter2D', () => {
36
43
  it('threads a native compressed atlas straight-alpha flag through the particle uniform', async () => {
37
44
  const state = await createWgpuRenderStateForTest();
38
45
  const runtime = getWgpuRenderStateRuntime(state);
46
+ registerWgpuCompressedImageTextureResolver(state);
39
47
  renderWgpuBackground(state);
40
48
  const before = runtime.uniformOffset;
41
49
  const image = {
42
- alphaType: 'straight',
43
50
  compressed: { container: {}, payload: new Uint8Array() },
44
- data: null,
45
51
  height: 4,
46
- source: null,
52
+ kind: CompressedImageTextureSourceKind,
47
53
  version: 1,
48
54
  width: 4,
49
- } as unknown as ImageResource;
55
+ } as unknown as CompressedImage;
50
56
  runtime.compressedTextureUpload = () => {
51
57
  const texture = state.device.createTexture({
52
58
  size: [4, 4],
@@ -67,7 +73,10 @@ describe('drawWgpuParticleEmitter2D', () => {
67
73
  source: {
68
74
  data: {
69
75
  alphas: new Float32Array([1]),
70
- atlas: { image, regions: [{ height: 4, width: 4, x: 0, y: 0 }] },
76
+ atlas: {
77
+ regions: [{ height: 4, width: 4, x: 0, y: 0 }],
78
+ texture: createTexture({ dimension: '2d', source: image }),
79
+ },
71
80
  colors: new Float32Array([1, 1, 1]),
72
81
  ids: new Uint16Array([0]),
73
82
  particleCount: 1,
@@ -1,10 +1,10 @@
1
+ import { createQuadBatch } from '@flighthq/quadbatch/contract';
1
2
  import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu/contract';
2
3
  import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu/contract';
3
4
  import { getRenderProxy2D, prepareScene2DRender } from '@flighthq/render/contract';
4
- import { createQuadBatch } from '@flighthq/sprite/contract';
5
5
 
6
6
  import { defaultWgpuQuadBatchRenderer, ensureWgpuQuadBatchResources, getWgpuQuadBatchPipeline } from './wgpuQuadBatch';
7
- import { flushWgpuSpriteBatch } from './wgpuSpriteBatch';
7
+ import { flushWgpuQuadBatchWriter } from './wgpuQuadBatchWriter';
8
8
 
9
9
  beforeAll(() => {
10
10
  installWgpuMock();
@@ -31,7 +31,7 @@ describe('defaultWgpuQuadBatchRenderer.submit', () => {
31
31
 
32
32
  expect(() => {
33
33
  defaultWgpuQuadBatchRenderer.submit(state, renderProxy);
34
- flushWgpuSpriteBatch(state as any);
34
+ flushWgpuQuadBatchWriter(state as any);
35
35
  }).not.toThrow();
36
36
  submitWgpuRenderPass(state);
37
37
  });
@@ -1,21 +1,20 @@
1
- import { createImageResource } from '@flighthq/image/contract';
2
1
  import { renderWgpuBackground, submitWgpuRenderPass } from '@flighthq/render-wgpu/contract';
3
2
  import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu/contract';
4
3
  import { createWgpuRenderStateForTest, installWgpuMock } from '@flighthq/render-wgpu/contract';
5
- import type { ColorScaleBias, Material } from '@flighthq/types/contract';
4
+ import type { ColorScaleBias, Material, WgpuTextureEntry } from '@flighthq/types/contract';
6
5
  import { BlendMode } from '@flighthq/types/contract';
7
6
 
8
7
  import { registerWgpuColorAdjustmentMaterialFeature } from './wgpuColorAdjustmentMaterialFeature';
9
8
  import {
10
9
  ensureWgpuQuadBatchResources,
11
- flushWgpuSpriteBatch,
10
+ flushWgpuQuadBatchWriter,
12
11
  getWgpuQuadBatchPipeline,
13
12
  getWgpuQuadBatchPreludeWGSL,
14
- packWgpuSpriteBatchMaterialInstance,
15
- prepareWgpuSpriteBatchWrite,
16
- recordWgpuSpriteBatchColorScaleBias,
17
- resetWgpuSpriteBatchBufferPool,
18
- } from './wgpuSpriteBatch';
13
+ packWgpuQuadBatchMaterialInstance,
14
+ prepareWgpuQuadBatchWrite,
15
+ recordWgpuQuadBatchColorScaleBias,
16
+ resetWgpuQuadBatchWriterBufferPool,
17
+ } from './wgpuQuadBatchWriter';
19
18
  import { standardWgpuMaterialRenderer } from './wgpuStandardMaterial';
20
19
 
21
20
  beforeAll(() => {
@@ -26,6 +25,14 @@ function makeMaterial(): Material {
26
25
  return { kind: 'TestMaterial' } as Material;
27
26
  }
28
27
 
28
+ function makeTexture(): WgpuTextureEntry {
29
+ return {
30
+ bindGroup: {} as GPUBindGroup,
31
+ texture: {} as GPUTexture,
32
+ view: {} as GPUTextureView,
33
+ };
34
+ }
35
+
29
36
  function ct(
30
37
  redScale = 1,
31
38
  greenScale = 1,
@@ -70,11 +77,11 @@ describe('ensureWgpuQuadBatchResources', () => {
70
77
  });
71
78
  });
72
79
 
73
- describe('flushWgpuSpriteBatch', () => {
80
+ describe('flushWgpuQuadBatchWriter', () => {
74
81
  it('does nothing when batch count is zero', async () => {
75
82
  const state = await createWgpuRenderStateForTest();
76
83
  renderWgpuBackground(state);
77
- expect(() => flushWgpuSpriteBatch(state)).not.toThrow();
84
+ expect(() => flushWgpuQuadBatchWriter(state)).not.toThrow();
78
85
  submitWgpuRenderPass(state);
79
86
  });
80
87
 
@@ -82,16 +89,16 @@ describe('flushWgpuSpriteBatch', () => {
82
89
  const state = await createWgpuRenderStateForTest();
83
90
  renderWgpuBackground(state);
84
91
  const runtime = getWgpuRenderStateRuntime(state);
85
- const tex = createImageResource(document.createElement('img'));
92
+ const tex = makeTexture();
86
93
 
87
- prepareWgpuSpriteBatchWrite(state, tex, null, null, standardWgpuMaterialRenderer, 1);
88
- runtime.spriteBatchCount = 1;
89
- flushWgpuSpriteBatch(state);
94
+ prepareWgpuQuadBatchWrite(state, tex, null, null, null, standardWgpuMaterialRenderer, 1);
95
+ runtime.quadBatchWriterCount = 1;
96
+ flushWgpuQuadBatchWriter(state);
90
97
 
91
- expect(runtime.spriteBatchCount).toBe(0);
92
- expect(runtime.spriteBatchTexture).toBeNull();
93
- expect(runtime.spriteBatchBlendMode).toBeNull();
94
- expect(runtime.spriteBatchMaterial).toBeNull();
98
+ expect(runtime.quadBatchWriterCount).toBe(0);
99
+ expect(runtime.quadBatchWriterTexture).toBeNull();
100
+ expect(runtime.quadBatchWriterBlendMode).toBeNull();
101
+ expect(runtime.quadBatchWriterMaterial).toBeNull();
95
102
  submitWgpuRenderPass(state);
96
103
  });
97
104
 
@@ -101,20 +108,22 @@ describe('flushWgpuSpriteBatch', () => {
101
108
  const state = await createWgpuRenderStateForTest();
102
109
  renderWgpuBackground(state);
103
110
  const runtime = getWgpuRenderStateRuntime(state);
104
- const tex1 = createImageResource(document.createElement('img'));
105
- const tex2 = createImageResource(document.createElement('img'));
106
-
107
- prepareWgpuSpriteBatchWrite(state, tex1, null, null, standardWgpuMaterialRenderer, 1);
108
- runtime.spriteBatchCount = 1;
109
- flushWgpuSpriteBatch(state);
110
-
111
- prepareWgpuSpriteBatchWrite(state, tex2, null, null, standardWgpuMaterialRenderer, 1);
112
- runtime.spriteBatchCount = 1;
113
- flushWgpuSpriteBatch(state);
114
-
115
- expect(runtime.spriteBatchBufferCursor).toBe(2);
116
- expect(runtime.spriteBatchBufferPool[0].instanceBuffer).not.toBeNull();
117
- expect(runtime.spriteBatchBufferPool[0].instanceBuffer).not.toBe(runtime.spriteBatchBufferPool[1].instanceBuffer);
111
+ const tex1 = makeTexture();
112
+ const tex2 = makeTexture();
113
+
114
+ prepareWgpuQuadBatchWrite(state, tex1, null, null, null, standardWgpuMaterialRenderer, 1);
115
+ runtime.quadBatchWriterCount = 1;
116
+ flushWgpuQuadBatchWriter(state);
117
+
118
+ prepareWgpuQuadBatchWrite(state, tex2, null, null, null, standardWgpuMaterialRenderer, 1);
119
+ runtime.quadBatchWriterCount = 1;
120
+ flushWgpuQuadBatchWriter(state);
121
+
122
+ expect(runtime.quadBatchWriterBufferCursor).toBe(2);
123
+ expect(runtime.quadBatchWriterBufferPool[0].instanceBuffer).not.toBeNull();
124
+ expect(runtime.quadBatchWriterBufferPool[0].instanceBuffer).not.toBe(
125
+ runtime.quadBatchWriterBufferPool[1].instanceBuffer,
126
+ );
118
127
  submitWgpuRenderPass(state);
119
128
  });
120
129
  });
@@ -166,19 +175,19 @@ describe('getWgpuQuadBatchPreludeWGSL', () => {
166
175
  });
167
176
  });
168
177
 
169
- describe('packWgpuSpriteBatchMaterialInstance', () => {
178
+ describe('packWgpuQuadBatchMaterialInstance', () => {
170
179
  it('is a no-op when no per-instance material data is active', async () => {
171
180
  const state = await createWgpuRenderStateForTest();
172
- expect(() => packWgpuSpriteBatchMaterialInstance(state, null, 0)).not.toThrow();
181
+ expect(() => packWgpuQuadBatchMaterialInstance(state, null, 0)).not.toThrow();
173
182
  });
174
183
  });
175
184
 
176
- describe('prepareWgpuSpriteBatchWrite', () => {
185
+ describe('prepareWgpuQuadBatchWrite', () => {
177
186
  it('returns float index 0 for an empty batch', async () => {
178
187
  const state = await createWgpuRenderStateForTest();
179
- const tex = createImageResource(document.createElement('img'));
188
+ const tex = makeTexture();
180
189
 
181
- const base = prepareWgpuSpriteBatchWrite(state, tex, null, null, standardWgpuMaterialRenderer, 1);
190
+ const base = prepareWgpuQuadBatchWrite(state, tex, null, null, null, standardWgpuMaterialRenderer, 1);
182
191
  expect(base).toBe(0);
183
192
  });
184
193
 
@@ -186,15 +195,15 @@ describe('prepareWgpuSpriteBatchWrite', () => {
186
195
  const state = await createWgpuRenderStateForTest();
187
196
  renderWgpuBackground(state);
188
197
  const runtime = getWgpuRenderStateRuntime(state);
189
- const tex1 = createImageResource(document.createElement('img'));
190
- const tex2 = createImageResource(document.createElement('img'));
198
+ const tex1 = makeTexture();
199
+ const tex2 = makeTexture();
191
200
 
192
- prepareWgpuSpriteBatchWrite(state, tex1, null, null, standardWgpuMaterialRenderer, 1);
193
- runtime.spriteBatchCount = 1;
194
- prepareWgpuSpriteBatchWrite(state, tex2, null, null, standardWgpuMaterialRenderer, 1);
201
+ prepareWgpuQuadBatchWrite(state, tex1, null, null, null, standardWgpuMaterialRenderer, 1);
202
+ runtime.quadBatchWriterCount = 1;
203
+ prepareWgpuQuadBatchWrite(state, tex2, null, null, null, standardWgpuMaterialRenderer, 1);
195
204
 
196
- expect(runtime.spriteBatchTexture).toBe(tex2);
197
- expect(runtime.spriteBatchCount).toBe(0);
205
+ expect(runtime.quadBatchWriterTexture).toBe(tex2);
206
+ expect(runtime.quadBatchWriterCount).toBe(0);
198
207
  submitWgpuRenderPass(state);
199
208
  });
200
209
 
@@ -202,41 +211,41 @@ describe('prepareWgpuSpriteBatchWrite', () => {
202
211
  const state = await createWgpuRenderStateForTest();
203
212
  renderWgpuBackground(state);
204
213
  const runtime = getWgpuRenderStateRuntime(state);
205
- const tex = createImageResource(document.createElement('img'));
214
+ const tex = makeTexture();
206
215
  const materialA = makeMaterial();
207
216
  const materialB = makeMaterial();
208
217
 
209
- prepareWgpuSpriteBatchWrite(state, tex, null, materialA, standardWgpuMaterialRenderer, 1);
210
- runtime.spriteBatchCount = 1;
211
- prepareWgpuSpriteBatchWrite(state, tex, null, materialB, standardWgpuMaterialRenderer, 1);
218
+ prepareWgpuQuadBatchWrite(state, tex, null, null, materialA, standardWgpuMaterialRenderer, 1);
219
+ runtime.quadBatchWriterCount = 1;
220
+ prepareWgpuQuadBatchWrite(state, tex, null, null, materialB, standardWgpuMaterialRenderer, 1);
212
221
 
213
- expect(runtime.spriteBatchMaterial).toBe(materialB);
214
- expect(runtime.spriteBatchCount).toBe(0);
222
+ expect(runtime.quadBatchWriterMaterial).toBe(materialB);
223
+ expect(runtime.quadBatchWriterCount).toBe(0);
215
224
  submitWgpuRenderPass(state);
216
225
  });
217
226
  });
218
227
 
219
- describe('recordWgpuSpriteBatchColorScaleBias', () => {
228
+ describe('recordWgpuQuadBatchColorScaleBias', () => {
220
229
  it('skips the tint and records no fold state when color adjustment is not enabled', async () => {
221
230
  const state = await createWgpuRenderStateForTest();
222
231
  const runtime = getWgpuRenderStateRuntime(state);
223
- recordWgpuSpriteBatchColorScaleBias(state, ct(0.5), 0);
224
- expect(runtime.spriteBatchColorScaleBiasMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
232
+ recordWgpuQuadBatchColorScaleBias(state, ct(0.5), 0);
233
+ expect(runtime.quadBatchWriterColorScaleBiasMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
225
234
  });
226
235
 
227
236
  it('is a no-op for an untinted instance whether or not the fold is enabled', async () => {
228
237
  const state = await createWgpuRenderStateForTest();
229
238
  const runtime = getWgpuRenderStateRuntime(state);
230
- expect(() => recordWgpuSpriteBatchColorScaleBias(state, null, 0)).not.toThrow();
231
- expect(runtime.spriteBatchColorScaleBiasMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
239
+ expect(() => recordWgpuQuadBatchColorScaleBias(state, null, 0)).not.toThrow();
240
+ expect(runtime.quadBatchWriterColorScaleBiasMode ?? CT_MODE_NONE).toBe(CT_MODE_NONE);
232
241
  });
233
242
 
234
243
  it('delegates to the installed fold when color adjustment is enabled', async () => {
235
244
  const state = await createWgpuRenderStateForTest();
236
245
  const runtime = getWgpuRenderStateRuntime(state);
237
246
  registerWgpuColorAdjustmentMaterialFeature(state);
238
- recordWgpuSpriteBatchColorScaleBias(state, ct(0.5), 0);
239
- expect(runtime.spriteBatchColorScaleBiasMode).toBe(CT_MODE_UNIFORM);
247
+ recordWgpuQuadBatchColorScaleBias(state, ct(0.5), 0);
248
+ expect(runtime.quadBatchWriterColorScaleBiasMode).toBe(CT_MODE_UNIFORM);
240
249
  });
241
250
 
242
251
  it('an untinted batch on flush uses the lean material module (no fold)', async () => {
@@ -244,29 +253,29 @@ describe('recordWgpuSpriteBatchColorScaleBias', () => {
244
253
  registerWgpuColorAdjustmentMaterialFeature(state);
245
254
  renderWgpuBackground(state);
246
255
  const runtime = getWgpuRenderStateRuntime(state);
247
- const tex = createImageResource(document.createElement('img'));
248
- prepareWgpuSpriteBatchWrite(state, tex, null, null, standardWgpuMaterialRenderer, 1);
249
- recordWgpuSpriteBatchColorScaleBias(state, null, 0);
250
- runtime.spriteBatchCount = 1;
251
- expect(() => flushWgpuSpriteBatch(state)).not.toThrow();
256
+ const tex = makeTexture();
257
+ prepareWgpuQuadBatchWrite(state, tex, null, null, null, standardWgpuMaterialRenderer, 1);
258
+ recordWgpuQuadBatchColorScaleBias(state, null, 0);
259
+ runtime.quadBatchWriterCount = 1;
260
+ expect(() => flushWgpuQuadBatchWriter(state)).not.toThrow();
252
261
  submitWgpuRenderPass(state);
253
262
  });
254
263
  });
255
264
 
256
- describe('resetWgpuSpriteBatchBufferPool', () => {
265
+ describe('resetWgpuQuadBatchWriterBufferPool', () => {
257
266
  it('rewinds the pool cursor so slots are reclaimed next frame', async () => {
258
267
  const state = await createWgpuRenderStateForTest();
259
268
  renderWgpuBackground(state);
260
269
  const runtime = getWgpuRenderStateRuntime(state);
261
- const tex = createImageResource(document.createElement('img'));
270
+ const tex = makeTexture();
262
271
 
263
- prepareWgpuSpriteBatchWrite(state, tex, null, null, standardWgpuMaterialRenderer, 1);
264
- runtime.spriteBatchCount = 1;
265
- flushWgpuSpriteBatch(state);
266
- expect(runtime.spriteBatchBufferCursor).toBe(1);
272
+ prepareWgpuQuadBatchWrite(state, tex, null, null, null, standardWgpuMaterialRenderer, 1);
273
+ runtime.quadBatchWriterCount = 1;
274
+ flushWgpuQuadBatchWriter(state);
275
+ expect(runtime.quadBatchWriterBufferCursor).toBe(1);
267
276
 
268
- resetWgpuSpriteBatchBufferPool(state);
269
- expect(runtime.spriteBatchBufferCursor).toBe(0);
277
+ resetWgpuQuadBatchWriterBufferPool(state);
278
+ expect(runtime.quadBatchWriterBufferCursor).toBe(0);
270
279
  submitWgpuRenderPass(state);
271
280
  });
272
281
  });
@@ -0,0 +1,156 @@
1
+ import type * as FlightNodeModule from '@flighthq/node/contract';
2
+ import { getWgpuRenderStateRuntime } from '@flighthq/render-wgpu/contract';
3
+ import { createWgpuRenderStateForTest, installWgpuMock, renderWgpuBackground } from '@flighthq/render-wgpu/contract';
4
+ import { enableRenderRegistryGuards, explainRenderRegistryMisses } from '@flighthq/render/contract';
5
+ import { appendShapeBeginFill, appendShapeEndFill, appendShapeRectangle, createShape } from '@flighthq/shape/contract';
6
+ import type { RenderProxy2D } from '@flighthq/types/contract';
7
+ import { BatchFormat, RenderRegistry } from '@flighthq/types/contract';
8
+
9
+ import { scopeModuleMocks } from './moduleMockTestHelper';
10
+ import type * as WgpuRasterShapeModule from './wgpuRasterShapeRenderer';
11
+ import { registerWgpuShapeRasterizer } from './wgpuShapeRasterizer';
12
+ import { registerWgpuStandardMaterial } from './wgpuStandardMaterial';
13
+
14
+ let defaultWgpuRasterShapeRenderer: typeof WgpuRasterShapeModule.defaultWgpuRasterShapeRenderer;
15
+ let drawWgpuRasterShape: typeof WgpuRasterShapeModule.drawWgpuRasterShape;
16
+
17
+ scopeModuleMocks(['@flighthq/node']);
18
+
19
+ beforeAll(async () => {
20
+ vi.doMock('@flighthq/node/contract', async (importOriginal) => ({
21
+ ...(await importOriginal<typeof FlightNodeModule>()),
22
+ getNodeLocalBoundsRectangle: () => ({ x: 0, y: 0, width: 64, height: 48 }),
23
+ getNodeLocalContentRevision: (source: any) => source?.data?.version ?? 0,
24
+ }));
25
+ ({ defaultWgpuRasterShapeRenderer, drawWgpuRasterShape } = await import('./wgpuRasterShapeRenderer'));
26
+ installWgpuMock();
27
+ });
28
+
29
+ function makeShapeData() {
30
+ return {
31
+ surface: null,
32
+ lastContentId: -1,
33
+ lastPixelRatio: 0,
34
+ lastW: 0,
35
+ lastH: 0,
36
+ meshVersion: -1,
37
+ meshes: null,
38
+ meshBuffers: {
39
+ vertexBuffers: [],
40
+ vertexCapacities: [],
41
+ indexBuffers: [],
42
+ indexCapacities: [],
43
+ uniformBuffers: [],
44
+ bindGroups: [],
45
+ colorScaleBiasUniformBuffers: [],
46
+ colorScaleBiasBindGroups: [],
47
+ },
48
+ };
49
+ }
50
+
51
+ function makeShapeProxy(data: Record<string, unknown>, rendererData: unknown = makeShapeData()): RenderProxy2D {
52
+ return {
53
+ source: { data: { commands: [], version: 0, ...data } },
54
+ blendMode: 0,
55
+ alpha: 1,
56
+ material: null,
57
+ materialData: null,
58
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
59
+ rendererData,
60
+ } as unknown as RenderProxy2D;
61
+ }
62
+
63
+ function makeMeshPassSpy(): GPURenderPassEncoder {
64
+ return {
65
+ setPipeline: vi.fn(),
66
+ setBindGroup: vi.fn(),
67
+ setVertexBuffer: vi.fn(),
68
+ setIndexBuffer: vi.fn(),
69
+ setStencilReference: vi.fn(),
70
+ draw: vi.fn(),
71
+ drawIndexed: vi.fn(),
72
+ end: vi.fn(),
73
+ } as unknown as GPURenderPassEncoder;
74
+ }
75
+
76
+ // A plain solid rectangle — precisely the shape the mesh strategy would tessellate.
77
+ function solidShape() {
78
+ const shape = createShape();
79
+ appendShapeBeginFill(shape, 0x00cc00);
80
+ appendShapeRectangle(shape, 8, 8, 32, 24);
81
+ appendShapeEndFill(shape);
82
+ return shape;
83
+ }
84
+
85
+ describe('defaultWgpuRasterShapeRenderer', () => {
86
+ it('declares BatchFormat.Quad and the shared shape data lifecycle', () => {
87
+ expect(defaultWgpuRasterShapeRenderer.format).toBe(BatchFormat.Quad);
88
+ expect(typeof defaultWgpuRasterShapeRenderer.createData).toBe('function');
89
+ expect(typeof defaultWgpuRasterShapeRenderer.destroyData).toBe('function');
90
+ expect(defaultWgpuRasterShapeRenderer.submit).toBe(drawWgpuRasterShape);
91
+ });
92
+ });
93
+
94
+ describe('drawWgpuRasterShape', () => {
95
+ it('rasterizes a fill the mesh path could have tessellated, which is what pinning this strategy means', async () => {
96
+ const state = await createWgpuRenderStateForTest();
97
+ renderWgpuBackground(state);
98
+ registerWgpuStandardMaterial(state);
99
+ const pass = makeMeshPassSpy();
100
+ getWgpuRenderStateRuntime(state).renderPass = pass;
101
+ const rasterizer = vi.fn();
102
+ registerWgpuShapeRasterizer(state, rasterizer);
103
+
104
+ drawWgpuRasterShape(state, makeShapeProxy({ commands: solidShape().data.commands, version: 1 }));
105
+
106
+ expect(rasterizer).toHaveBeenCalledTimes(1);
107
+ expect(pass.drawIndexed).not.toHaveBeenCalled();
108
+ expect(getWgpuRenderStateRuntime(state).quadBatchWriterCount).toBe(1);
109
+ });
110
+
111
+ it('replays the whole command stream, not the subset a mesh path could not express', async () => {
112
+ const state = await createWgpuRenderStateForTest();
113
+ renderWgpuBackground(state);
114
+ registerWgpuStandardMaterial(state);
115
+ getWgpuRenderStateRuntime(state).renderPass = makeMeshPassSpy();
116
+ const shape = solidShape();
117
+ let replayed: readonly unknown[] = [];
118
+ registerWgpuShapeRasterizer(state, (_ctx, commands) => {
119
+ replayed = commands;
120
+ });
121
+
122
+ drawWgpuRasterShape(state, makeShapeProxy({ commands: shape.data.commands, version: 1 }));
123
+
124
+ expect(replayed).toEqual(shape.data.commands);
125
+ });
126
+
127
+ it('reports a ShapeRasterizer miss when no rasterizer is registered', async () => {
128
+ const state = await createWgpuRenderStateForTest();
129
+ renderWgpuBackground(state);
130
+ getWgpuRenderStateRuntime(state).renderPass = makeMeshPassSpy();
131
+ enableRenderRegistryGuards(state);
132
+
133
+ drawWgpuRasterShape(state, makeShapeProxy({ commands: solidShape().data.commands, version: 1 }));
134
+
135
+ expect(explainRenderRegistryMisses(state).misses).toContainEqual({
136
+ kind: 'Shape',
137
+ registry: RenderRegistry.ShapeRasterizer,
138
+ });
139
+ });
140
+
141
+ it('does nothing without a render pass, for an empty command list, or with absent renderer data', async () => {
142
+ const state = await createWgpuRenderStateForTest();
143
+ renderWgpuBackground(state);
144
+ registerWgpuStandardMaterial(state);
145
+ const rasterizer = vi.fn();
146
+ registerWgpuShapeRasterizer(state, rasterizer);
147
+
148
+ getWgpuRenderStateRuntime(state).renderPass = null;
149
+ drawWgpuRasterShape(state, makeShapeProxy({ commands: solidShape().data.commands, version: 1 }));
150
+ getWgpuRenderStateRuntime(state).renderPass = makeMeshPassSpy();
151
+ drawWgpuRasterShape(state, makeShapeProxy({ commands: [], version: 1 }));
152
+ drawWgpuRasterShape(state, makeShapeProxy({ commands: solidShape().data.commands, version: 1 }, null));
153
+
154
+ expect(rasterizer).not.toHaveBeenCalled();
155
+ });
156
+ });