@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.
- package/README.md +215 -0
- package/dist/assembly/dynamic-geometry-host.d.ts +31 -0
- package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
- package/dist/assembly/host-contract.d.ts +11 -0
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/authoring.mjs +2 -2
- package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
- package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
- package/dist/{chunk-HKXTW355.mjs → chunk-IQMKJLNU.mjs} +3 -3
- package/dist/{chunk-HKXTW355.mjs.map → chunk-IQMKJLNU.mjs.map} +1 -1
- package/dist/{chunk-KM2NOX2I.mjs → chunk-J52NMQKR.mjs} +462 -6
- package/dist/chunk-J52NMQKR.mjs.map +1 -0
- package/dist/{chunk-ZZ4YQ474.mjs → chunk-J6KACNOH.mjs} +2 -2
- package/dist/{chunk-ZZ4YQ474.mjs.map → chunk-J6KACNOH.mjs.map} +1 -1
- package/dist/{chunk-ADHHVYLW.mjs → chunk-JAASCMXA.mjs} +3 -3
- package/dist/{chunk-ADHHVYLW.mjs.map → chunk-JAASCMXA.mjs.map} +1 -1
- package/dist/{chunk-TMIASV2N.mjs → chunk-MBU4ZVE7.mjs} +20 -12
- package/dist/chunk-MBU4ZVE7.mjs.map +1 -0
- package/dist/{chunk-E34VL5VI.mjs → chunk-MYJ5RPJO.mjs} +3 -3
- package/dist/{chunk-E34VL5VI.mjs.map → chunk-MYJ5RPJO.mjs.map} +1 -1
- package/dist/{chunk-JDWARUOI.mjs → chunk-RBDBZ5R7.mjs} +17 -3
- package/dist/chunk-RBDBZ5R7.mjs.map +1 -0
- package/dist/{chunk-YUQXSEHG.mjs → chunk-VSL23LZE.mjs} +26 -12
- package/dist/chunk-VSL23LZE.mjs.map +1 -0
- package/dist/construct-renderer.mjs +862 -106
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +10 -5
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/device/mesh-residency-lifetime.d.ts +21 -0
- package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
- package/dist/dynamic-geometry.d.ts +125 -0
- package/dist/dynamic-geometry.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +7 -6
- package/dist/index.mjs.map +1 -1
- package/dist/internal.mjs +7 -7
- package/dist/record/frame.d.ts +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -0
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/render-contract.d.ts +12 -0
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +6 -0
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +7 -0
- package/dist/render-system.d.ts.map +1 -1
- package/dist/temporal/index.mjs +4 -4
- package/package.json +20 -20
- package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
- package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
- package/src/__tests__/factory-contract.integration.test.ts +6 -14
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +1 -1
- package/src/assembly/dynamic-geometry-host.ts +839 -0
- package/src/assembly/host-contract.ts +26 -0
- package/src/assembly/renderer-facade.ts +17 -0
- package/src/assembly/webgpu-renderer.ts +49 -2
- package/src/components/sprite-animation.ts +1 -1
- package/src/device/gpu-residency.ts +62 -62
- package/src/device/mesh-residency-lifetime.ts +73 -0
- package/src/dynamic-geometry.ts +736 -0
- package/src/index.ts +16 -0
- package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +68 -1
- package/src/record/frame.ts +2 -0
- package/src/record/main-pass-geometry.ts +13 -4
- package/src/record/main-pass-sprite-draws.ts +2 -0
- package/src/record/render-context.ts +2 -0
- package/src/record/typed-frame-graph.ts +2 -9
- package/src/render-contract.ts +24 -0
- package/src/render-system-extract-tail.ts +3 -2
- package/src/render-system-extract.ts +30 -1
- package/src/render-system.ts +42 -0
- package/dist/chunk-FJ6P52EE.mjs.map +0 -1
- package/dist/chunk-JDWARUOI.mjs.map +0 -1
- package/dist/chunk-KM2NOX2I.mjs.map +0 -1
- package/dist/chunk-TMIASV2N.mjs.map +0 -1
- 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('
|
|
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(
|
|
734
|
-
expect(pending).toHaveLength(
|
|
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(
|
|
747
|
-
expect(pending).toHaveLength(
|
|
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(
|
|
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');
|