@forgeax/engine-render 0.1.26 → 0.1.28

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 (83) hide show
  1. package/README.md +215 -0
  2. package/dist/assembly/dynamic-geometry-host.d.ts +31 -0
  3. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
  4. package/dist/assembly/host-contract.d.ts +11 -0
  5. package/dist/assembly/host-contract.d.ts.map +1 -1
  6. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  7. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  8. package/dist/authoring.mjs +2 -2
  9. package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
  10. package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
  11. package/dist/{chunk-HKXTW355.mjs → chunk-IQMKJLNU.mjs} +3 -3
  12. package/dist/{chunk-HKXTW355.mjs.map → chunk-IQMKJLNU.mjs.map} +1 -1
  13. package/dist/{chunk-KM2NOX2I.mjs → chunk-J52NMQKR.mjs} +462 -6
  14. package/dist/chunk-J52NMQKR.mjs.map +1 -0
  15. package/dist/{chunk-ZZ4YQ474.mjs → chunk-J6KACNOH.mjs} +2 -2
  16. package/dist/{chunk-ZZ4YQ474.mjs.map → chunk-J6KACNOH.mjs.map} +1 -1
  17. package/dist/{chunk-ADHHVYLW.mjs → chunk-JAASCMXA.mjs} +3 -3
  18. package/dist/{chunk-ADHHVYLW.mjs.map → chunk-JAASCMXA.mjs.map} +1 -1
  19. package/dist/{chunk-TMIASV2N.mjs → chunk-MBU4ZVE7.mjs} +20 -12
  20. package/dist/chunk-MBU4ZVE7.mjs.map +1 -0
  21. package/dist/{chunk-E34VL5VI.mjs → chunk-MYJ5RPJO.mjs} +3 -3
  22. package/dist/{chunk-E34VL5VI.mjs.map → chunk-MYJ5RPJO.mjs.map} +1 -1
  23. package/dist/{chunk-JDWARUOI.mjs → chunk-RBDBZ5R7.mjs} +17 -3
  24. package/dist/chunk-RBDBZ5R7.mjs.map +1 -0
  25. package/dist/{chunk-YUQXSEHG.mjs → chunk-VSL23LZE.mjs} +26 -12
  26. package/dist/chunk-VSL23LZE.mjs.map +1 -0
  27. package/dist/construct-renderer.mjs +862 -106
  28. package/dist/construct-renderer.mjs.map +1 -1
  29. package/dist/device/gpu-residency.d.ts +10 -5
  30. package/dist/device/gpu-residency.d.ts.map +1 -1
  31. package/dist/device/mesh-residency-lifetime.d.ts +21 -0
  32. package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
  33. package/dist/dynamic-geometry.d.ts +125 -0
  34. package/dist/dynamic-geometry.d.ts.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.mjs +7 -6
  38. package/dist/index.mjs.map +1 -1
  39. package/dist/internal.mjs +7 -7
  40. package/dist/record/frame.d.ts +1 -1
  41. package/dist/record/frame.d.ts.map +1 -1
  42. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  43. package/dist/record/render-context.d.ts +2 -0
  44. package/dist/record/render-context.d.ts.map +1 -1
  45. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  46. package/dist/render-contract.d.ts +12 -0
  47. package/dist/render-contract.d.ts.map +1 -1
  48. package/dist/render-system-extract-tail.d.ts.map +1 -1
  49. package/dist/render-system-extract.d.ts +6 -0
  50. package/dist/render-system-extract.d.ts.map +1 -1
  51. package/dist/render-system.d.ts +7 -0
  52. package/dist/render-system.d.ts.map +1 -1
  53. package/dist/temporal/index.mjs +4 -4
  54. package/package.json +20 -20
  55. package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
  56. package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
  57. package/src/__tests__/factory-contract.integration.test.ts +6 -14
  58. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
  59. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +1 -1
  60. package/src/assembly/dynamic-geometry-host.ts +839 -0
  61. package/src/assembly/host-contract.ts +26 -0
  62. package/src/assembly/renderer-facade.ts +17 -0
  63. package/src/assembly/webgpu-renderer.ts +49 -2
  64. package/src/components/sprite-animation.ts +1 -1
  65. package/src/device/gpu-residency.ts +62 -62
  66. package/src/device/mesh-residency-lifetime.ts +73 -0
  67. package/src/dynamic-geometry.ts +736 -0
  68. package/src/index.ts +16 -0
  69. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +68 -1
  70. package/src/record/frame.ts +2 -0
  71. package/src/record/main-pass-geometry.ts +13 -4
  72. package/src/record/main-pass-sprite-draws.ts +2 -0
  73. package/src/record/render-context.ts +2 -0
  74. package/src/record/typed-frame-graph.ts +2 -9
  75. package/src/render-contract.ts +24 -0
  76. package/src/render-system-extract-tail.ts +3 -2
  77. package/src/render-system-extract.ts +30 -1
  78. package/src/render-system.ts +42 -0
  79. package/dist/chunk-FJ6P52EE.mjs.map +0 -1
  80. package/dist/chunk-JDWARUOI.mjs.map +0 -1
  81. package/dist/chunk-KM2NOX2I.mjs.map +0 -1
  82. package/dist/chunk-TMIASV2N.mjs.map +0 -1
  83. package/dist/chunk-YUQXSEHG.mjs.map +0 -1
@@ -0,0 +1,233 @@
1
+ import type { MeshAsset } from '@forgeax/engine-types';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createDynamicGeometryLifecycle } from '../dynamic-geometry';
4
+
5
+ const mesh = (): MeshAsset => ({
6
+ kind: 'mesh',
7
+ vertices: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
8
+ indices: new Uint16Array([0, 1, 2]),
9
+ attributes: { position: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]) },
10
+ aabb: new Float32Array([0, 0, 0, 1, 1, 0]),
11
+ submeshes: [
12
+ { indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 },
13
+ ],
14
+ materialSlots: [{ slotName: 'default', sourceKey: 'default' }],
15
+ });
16
+
17
+ describe('standard dynamic geometry lifecycle', () => {
18
+ it('copies a MeshAsset and publishes only after a matching frame generation', () => {
19
+ const lifecycle = createDynamicGeometryLifecycle();
20
+ const world = {};
21
+ const source = mesh();
22
+ const prepared = lifecycle.prepare(
23
+ { world, mesh: source, revision: 1, topologyRevision: 7 },
24
+ 3,
25
+ );
26
+ expect(prepared.ok).toBe(true);
27
+ if (!prepared.ok) return;
28
+
29
+ prepared.value.mesh.vertices[0] = 77;
30
+
31
+ const accepted = lifecycle.accept(prepared.value);
32
+ expect(accepted.ok).toBe(true);
33
+ if (!accepted.ok) return;
34
+ expect(accepted.value.state).toBe('accepted');
35
+ expect(accepted.value.mesh).not.toBe(source);
36
+ source.vertices[0] = 99;
37
+ expect(accepted.value.mesh.vertices[0]).toBe(0);
38
+ const receiptsBefore = lifecycle.publishFrame({ frameId: 4, deviceGeneration: 2 });
39
+ expect(receiptsBefore).toEqual([]);
40
+ const receipts = lifecycle.publishFrame({ frameId: 5, deviceGeneration: 3 });
41
+ expect(receipts).toHaveLength(1);
42
+ expect(receipts[0]).toMatchObject({
43
+ candidateId: accepted.value.candidateId,
44
+ frameId: 5,
45
+ topologyRevision: 7,
46
+ recordStageConsumed: false,
47
+ });
48
+ expect(lifecycle.receipt(accepted.value)).toMatchObject({
49
+ candidateId: accepted.value.candidateId,
50
+ frameId: 5,
51
+ deviceGeneration: 3,
52
+ });
53
+ expect(lifecycle.inspect()).toMatchObject({
54
+ generation: 3,
55
+ published: 1,
56
+ latestRevision: 1,
57
+ latestFrameId: 5,
58
+ });
59
+ expect(lifecycle.retire(accepted.value).ok).toBe(true);
60
+ expect(lifecycle.finalizeRetirement(accepted.value).ok).toBe(true);
61
+ const stale = lifecycle.prepare({ world, mesh: mesh(), revision: 1 }, 3);
62
+ expect(stale.ok).toBe(true);
63
+ if (!stale.ok) return;
64
+ expect(lifecycle.accept(stale.value)).toMatchObject({
65
+ ok: false,
66
+ error: { code: 'dynamic-geometry-stale' },
67
+ });
68
+ });
69
+
70
+ it('keeps old state when preparation is invalid and bounds in-flight work', () => {
71
+ const lifecycle = createDynamicGeometryLifecycle(1);
72
+ const world = {};
73
+ const first = lifecycle.prepare({ world, mesh: mesh(), revision: 1 }, 1);
74
+ expect(first.ok).toBe(true);
75
+ const invalid = lifecycle.prepare(
76
+ { world, mesh: { ...mesh(), vertices: new Float32Array() }, revision: 2 },
77
+ 1,
78
+ );
79
+ expect(invalid.ok).toBe(false);
80
+ if (!first.ok) return;
81
+ expect(lifecycle.cancel(first.value).ok).toBe(true);
82
+ const replacement = lifecycle.prepare({ world, mesh: mesh(), revision: 2 }, 1);
83
+ expect(replacement.ok).toBe(true);
84
+ });
85
+
86
+ it('invalidates candidates on world teardown and device-generation change', () => {
87
+ const lifecycle = createDynamicGeometryLifecycle();
88
+ const world = {};
89
+ const prepared = lifecycle.prepare({ world, mesh: mesh(), revision: 1 }, 1);
90
+ expect(prepared.ok).toBe(true);
91
+ lifecycle.invalidateWorld(world);
92
+ expect(lifecycle.inspect()).toMatchObject({ prepared: 0, invalidated: 1 });
93
+ const next = lifecycle.prepare({ world, mesh: mesh(), revision: 2 }, 1);
94
+ expect(next.ok).toBe(true);
95
+ lifecycle.invalidateGeneration(2);
96
+ expect(lifecycle.inspect()).toMatchObject({ generation: 2, prepared: 0, invalidated: 2 });
97
+ });
98
+
99
+ it('rejects cross-lifecycle credentials and publishes only the ordered World step', () => {
100
+ const lifecycle = createDynamicGeometryLifecycle();
101
+ const otherLifecycle = createDynamicGeometryLifecycle();
102
+ const world = {};
103
+ const otherWorld = {};
104
+ const prepared = lifecycle.prepare({ world, mesh: mesh(), revision: 1 }, 1);
105
+ expect(prepared.ok).toBe(true);
106
+ if (!prepared.ok) return;
107
+ expect(otherLifecycle.accept(prepared.value)).toMatchObject({
108
+ ok: false,
109
+ error: { code: 'dynamic-geometry-candidate-not-found' },
110
+ });
111
+ const colliding = otherLifecycle.prepare({ world, mesh: mesh(), revision: 1 }, 1);
112
+ expect(colliding.ok).toBe(true);
113
+ if (!colliding.ok) return;
114
+ expect(colliding.value.candidateId).toBe(prepared.value.candidateId);
115
+ expect(lifecycle.receipt(colliding.value)).toBeUndefined();
116
+ const accepted = lifecycle.accept(prepared.value, { world, fixedStep: 4 });
117
+ expect(accepted.ok).toBe(true);
118
+ if (!accepted.ok) return;
119
+ expect(lifecycle.publishFrame({ frameId: 1, deviceGeneration: 1 }, [otherWorld], 4)).toEqual(
120
+ [],
121
+ );
122
+ expect(lifecycle.publishFrame({ frameId: 2, deviceGeneration: 1 }, [world], 3)).toEqual([]);
123
+ expect(lifecycle.publishFrame({ frameId: 3, deviceGeneration: 1 }, [world], 4)).toHaveLength(1);
124
+ });
125
+
126
+ it('rejects a lower prepared revision after a newer candidate is prepared', () => {
127
+ const lifecycle = createDynamicGeometryLifecycle();
128
+ const world = {};
129
+ const newer = lifecycle.prepare({ world, mesh: mesh(), revision: 4, topologyRevision: 1 }, 1);
130
+ const older = lifecycle.prepare({ world, mesh: mesh(), revision: 3, topologyRevision: 1 }, 1);
131
+ expect(newer.ok).toBe(true);
132
+ expect(older.ok).toBe(true);
133
+ if (!newer.ok || !older.ok) return;
134
+
135
+ expect(lifecycle.accept(newer.value)).toBeTruthy();
136
+ expect(lifecycle.accept(older.value)).toMatchObject({
137
+ ok: false,
138
+ error: { code: 'dynamic-geometry-stale' },
139
+ });
140
+ expect(lifecycle.publishFrame({ frameId: 1, deviceGeneration: 1 })).toHaveLength(1);
141
+ expect(lifecycle.inspect()).toMatchObject({ latestRevision: 4 });
142
+ });
143
+
144
+ it('invalidates topology history while retaining the published receipt fence', () => {
145
+ const lifecycle = createDynamicGeometryLifecycle();
146
+ const world = {};
147
+ const previous = lifecycle.prepare(
148
+ { world, mesh: mesh(), revision: 1, topologyRevision: 1 },
149
+ 1,
150
+ );
151
+ expect(previous.ok).toBe(true);
152
+ if (!previous.ok) return;
153
+ const acceptedPrevious = lifecycle.accept(previous.value);
154
+ expect(acceptedPrevious.ok).toBe(true);
155
+ if (!acceptedPrevious.ok) return;
156
+ expect(lifecycle.publishFrame({ frameId: 1, deviceGeneration: 1 })).toHaveLength(1);
157
+ expect(lifecycle.receipt(acceptedPrevious.value)).toMatchObject({ frameId: 1 });
158
+
159
+ const replacement = lifecycle.prepare(
160
+ { world, mesh: mesh(), revision: 2, topologyRevision: 2 },
161
+ 1,
162
+ );
163
+ expect(replacement.ok).toBe(true);
164
+ if (!replacement.ok) return;
165
+ expect(lifecycle.accept(replacement.value).ok).toBe(true);
166
+ // Topology invalidates temporal history but must preserve the published
167
+ // receipt fence until explicit retirement can hand GPU destruction to the
168
+ // completion promise.
169
+ expect(lifecycle.receipt(acceptedPrevious.value)).toMatchObject({ frameId: 1 });
170
+ expect(lifecycle.inspect()).toMatchObject({ historyInvalidations: 1, invalidated: 1 });
171
+ expect(lifecycle.retire(acceptedPrevious.value).ok).toBe(true);
172
+ expect(lifecycle.finalizeRetirement(acceptedPrevious.value).ok).toBe(true);
173
+ });
174
+
175
+ it('rejects a topology revision that regresses after a newer history was accepted', () => {
176
+ const lifecycle = createDynamicGeometryLifecycle();
177
+ const world = {};
178
+ const current = lifecycle.prepare({ world, mesh: mesh(), revision: 2, topologyRevision: 4 }, 1);
179
+ expect(current.ok).toBe(true);
180
+ if (!current.ok) return;
181
+ expect(lifecycle.accept(current.value).ok).toBe(true);
182
+ expect(lifecycle.publishFrame({ frameId: 1, deviceGeneration: 1 })).toHaveLength(1);
183
+
184
+ const regressed = lifecycle.prepare(
185
+ { world, mesh: mesh(), revision: 3, topologyRevision: 3 },
186
+ 1,
187
+ );
188
+ expect(regressed.ok).toBe(true);
189
+ if (!regressed.ok) return;
190
+ expect(lifecycle.accept(regressed.value)).toMatchObject({
191
+ ok: false,
192
+ error: { code: 'dynamic-geometry-stale' },
193
+ });
194
+ });
195
+
196
+ it('accounts for mesh bytes, rejects unknown retirement, and preserves a multi-step receipt', () => {
197
+ const world = {};
198
+ const lifecycle = createDynamicGeometryLifecycle(4, 1_000);
199
+ const prepared = lifecycle.prepare({ world, mesh: mesh(), revision: 1, fixedStep: 2 }, 1);
200
+ expect(prepared.ok).toBe(true);
201
+ if (!prepared.ok) return;
202
+ expect(lifecycle.inspect().meshBytes).toBeGreaterThan(0);
203
+ const tooLarge = lifecycle.prepare(
204
+ { world, mesh: { ...mesh(), vertices: new Float32Array(3_000) }, revision: 2 },
205
+ 1,
206
+ );
207
+ expect(tooLarge).toMatchObject({
208
+ ok: false,
209
+ error: { code: 'dynamic-geometry-budget-exceeded' },
210
+ });
211
+ expect(lifecycle.accept(prepared.value).ok).toBe(true);
212
+ const completed = Promise.resolve();
213
+ const receipts = lifecycle.publishFrame(
214
+ { frameId: 4, deviceGeneration: 1, completed },
215
+ [world],
216
+ 5,
217
+ );
218
+ expect(receipts).toHaveLength(1);
219
+ expect(receipts[0]).toMatchObject({
220
+ fixedStep: 2,
221
+ publicationFixedStep: 5,
222
+ recordStageConsumed: false,
223
+ frame: { completed },
224
+ });
225
+ expect(lifecycle.retire({ ...prepared.value, candidateId: 'unknown' })).toMatchObject({
226
+ ok: false,
227
+ error: { code: 'dynamic-geometry-candidate-not-found' },
228
+ });
229
+ expect(lifecycle.retire(prepared.value).ok).toBe(true);
230
+ expect(lifecycle.finalizeRetirement(prepared.value).ok).toBe(true);
231
+ expect(lifecycle.inspect().meshBytes).toBe(0);
232
+ });
233
+ });
@@ -651,7 +651,7 @@ describe('factory contract', () => {
651
651
  expect(unconfigureCalls).toBe(1);
652
652
  });
653
653
 
654
- it('invalidates deferred TAA shader epochs across off and dispose', async () => {
654
+ it('retains the device-owned prewarmed TAA shader across off and on', async () => {
655
655
  const pending: Array<{
656
656
  readonly label: string;
657
657
  readonly promise: Promise<ReturnType<typeof ok<ShaderModule>>>;
@@ -682,7 +682,6 @@ describe('factory contract', () => {
682
682
  );
683
683
  await vi.waitFor(() => expect(pending).toHaveLength(1));
684
684
  const initialization = renderer.initialization;
685
- const firstPromise = pending[0]?.promise;
686
685
  pending[0]?.resolve(ok({} as ShaderModule));
687
686
  expect((await initialization).ok).toBe(true);
688
687
 
@@ -730,12 +729,8 @@ describe('factory contract', () => {
730
729
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_TAA }).unwrap();
731
730
  expect(world.update().ok).toBe(true);
732
731
  const second = renderer.draw(frameInput());
733
- expect(second.ok).toBe(false);
734
- expect(pending).toHaveLength(2);
735
- expect(pending[1]?.promise).not.toBe(firstPromise);
736
-
737
- pending[1]?.resolve(ok({} as ShaderModule));
738
- await pending[1]?.promise;
732
+ expect(second.ok).toBe(true);
733
+ expect(pending).toHaveLength(1);
739
734
  expect(renderer.draw(frameInput()).ok).toBe(true);
740
735
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_NONE }).unwrap();
741
736
  expect(world.update().ok).toBe(true);
@@ -743,17 +738,14 @@ describe('factory contract', () => {
743
738
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_TAA }).unwrap();
744
739
  expect(world.update().ok).toBe(true);
745
740
  const replacement = renderer.draw(frameInput());
746
- expect(replacement.ok).toBe(false);
747
- expect(pending).toHaveLength(3);
748
- expect(pending[2]?.promise).not.toBe(pending[1]?.promise);
741
+ expect(replacement.ok).toBe(true);
742
+ expect(pending).toHaveLength(1);
749
743
 
750
744
  expect((await renderer.dispose()).ok).toBe(true);
751
745
  expect((await renderer.dispose()).ok).toBe(true);
752
- pending[2]?.resolve(ok({} as ShaderModule));
753
- await pending[2]?.promise;
754
746
  expect(
755
747
  moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
756
- ).toHaveLength(3);
748
+ ).toHaveLength(1);
757
749
  });
758
750
 
759
751
  it('rebuilds a TAA graph for a same-mode target resize', async () => {
@@ -0,0 +1,53 @@
1
+ import { World } from '@forgeax/engine-ecs';
2
+ import { createBoxGeometry } from '@forgeax/engine-geometry';
3
+ import { rhi } from '@forgeax/engine-rhi-null';
4
+ import { expect, it } from 'vitest';
5
+ import { GpuResidencyCache } from '../device/gpu-residency';
6
+
7
+ it('fences ordinary mesh submissions independently of candidates, including reversed completion', async () => {
8
+ const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
9
+ const store = new GpuResidencyCache();
10
+ store.configureGpuDevice(
11
+ device,
12
+ undefined,
13
+ () => {
14
+ throw new Error('no cubemap');
15
+ },
16
+ device.caps,
17
+ );
18
+ const world = new World();
19
+ const mesh = createBoxGeometry(1, 1, 1).unwrap();
20
+ const handle = world.allocSharedRef('MeshAsset', mesh);
21
+ store.ensureResident(handle, mesh, world).unwrap();
22
+ const original = store.getMeshGpuHandles(handle, world);
23
+ if (original === undefined) throw new Error('mesh residency missing');
24
+ let finishFirst!: () => void;
25
+ let finishSecond!: () => void;
26
+ const first = new Promise<void>((resolve) => {
27
+ finishFirst = resolve;
28
+ });
29
+ const second = new Promise<void>((resolve) => {
30
+ finishSecond = resolve;
31
+ });
32
+ store.trackMeshSubmission(first);
33
+ store.trackMeshSubmission(second);
34
+ const lease = store.retainMeshResidency(handle, world);
35
+ if (lease === undefined) throw new Error('residency lease missing');
36
+ store.invalidateMesh(handle, world);
37
+ lease.release(true);
38
+ expect(original.vertexBuffer.isDestroyed).toBe(false);
39
+ // The same cache key can upload its replacement before old work completes.
40
+ store.ensureResident(handle, mesh, world).unwrap();
41
+ const replacement = store.getMeshGpuHandles(handle, world);
42
+ if (replacement === undefined) throw new Error('replacement residency missing');
43
+ expect(replacement).not.toBe(original);
44
+ finishSecond();
45
+ await second;
46
+ expect(original.vertexBuffer.isDestroyed).toBe(false);
47
+ finishFirst();
48
+ await first;
49
+ expect(original.vertexBuffer.isDestroyed).toBe(true);
50
+ expect(store.getMeshGpuHandles(handle, world)).toBe(replacement);
51
+ expect(replacement.vertexBuffer.isDestroyed).toBe(false);
52
+ store.destroyAll();
53
+ });
@@ -715,7 +715,7 @@ describe('material shader color-domain contract', () => {
715
715
  readFileSync(resolve(import.meta.dirname, '../assembly/webgpu-ready.ts'), 'utf8'),
716
716
  ].join('\n');
717
717
  expect(source).toContain(
718
- "isHdr &&\n (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')",
718
+ "isHdr &&\n passKind === 'forward' &&\n (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')",
719
719
  );
720
720
  expect(source).toContain("? 'fs_main_hdr'");
721
721
  expect(source).toContain('colorFormat: isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat');