@forgeax/engine-scene 0.1.20 → 0.1.23
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 +37 -9
- package/dist/__tests__/flat-propagation.contract.test.d.ts +2 -0
- package/dist/__tests__/flat-propagation.contract.test.d.ts.map +1 -0
- package/dist/__tests__/flat-propagation.derived-writer.contract.test.d.ts +2 -0
- package/dist/__tests__/flat-propagation.derived-writer.contract.test.d.ts.map +1 -0
- package/dist/__tests__/flat-propagation.perf.test.d.ts +2 -0
- package/dist/__tests__/flat-propagation.perf.test.d.ts.map +1 -0
- package/dist/__tests__/scene-binding.integration.test.d.ts +2 -0
- package/dist/__tests__/scene-binding.integration.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-order.contract.test.d.ts +2 -0
- package/dist/__tests__/schedule-order.contract.test.d.ts.map +1 -0
- package/dist/__tests__/structural.test.d.ts +2 -0
- package/dist/__tests__/structural.test.d.ts.map +1 -0
- package/dist/__tests__/transform-carrier-consumers.test-d.d.ts +2 -0
- package/dist/__tests__/transform-carrier-consumers.test-d.d.ts.map +1 -0
- package/dist/__tests__/transform-carrier-prototype.bench.d.ts +39 -0
- package/dist/__tests__/transform-carrier-prototype.bench.d.ts.map +1 -0
- package/dist/__tests__/transform-carrier-prototype.test-d.d.ts +2 -0
- package/dist/__tests__/transform-carrier-prototype.test-d.d.ts.map +1 -0
- package/dist/__tests__/transform-carrier-prototype.test.d.ts +2 -0
- package/dist/__tests__/transform-carrier-prototype.test.d.ts.map +1 -0
- package/dist/__tests__/transform-component-version.unit.test.d.ts +2 -0
- package/dist/__tests__/transform-component-version.unit.test.d.ts.map +1 -0
- package/dist/assets/scene-decoder.d.ts.map +1 -1
- package/dist/components/child-of.d.ts +15 -8
- package/dist/components/child-of.d.ts.map +1 -1
- package/dist/components/children.d.ts.map +1 -1
- package/dist/components/transform.d.ts +7 -43
- package/dist/components/transform.d.ts.map +1 -1
- package/dist/errors.d.ts +23 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +616 -372
- package/dist/index.mjs.map +1 -1
- package/dist/instances/binding.d.ts +29 -0
- package/dist/instances/binding.d.ts.map +1 -0
- package/dist/instances/externalization.d.ts.map +1 -1
- package/dist/instances/scene-instances.d.ts +5 -17
- package/dist/instances/scene-instances.d.ts.map +1 -1
- package/dist/instances/state.d.ts +28 -0
- package/dist/instances/state.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/systems/hierarchy-projection.d.ts.map +1 -1
- package/dist/systems/index.d.ts +1 -1
- package/dist/systems/index.d.ts.map +1 -1
- package/dist/systems/propagate-transforms.d.ts +1 -2
- package/dist/systems/propagate-transforms.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/components.unit.test.ts +34 -2
- package/src/__tests__/flat-propagation.contract.test.ts +128 -0
- package/src/__tests__/flat-propagation.derived-writer.contract.test.ts +39 -0
- package/src/__tests__/flat-propagation.perf.test.ts +336 -0
- package/src/__tests__/plugin.integration.test.ts +2 -2
- package/src/__tests__/propagation.unit.test.ts +207 -91
- package/src/__tests__/scene-binding.integration.test.ts +124 -0
- package/src/__tests__/schedule-order.contract.test.ts +43 -0
- package/src/__tests__/structural.test.ts +23 -0
- package/src/__tests__/transform-carrier-consumers.test-d.ts +156 -0
- package/src/__tests__/transform-carrier-prototype.bench.ts +168 -0
- package/src/__tests__/transform-carrier-prototype.test-d.ts +71 -0
- package/src/__tests__/transform-carrier-prototype.test.ts +170 -0
- package/src/__tests__/{transform-change-journal.unit.test.ts → transform-component-version.unit.test.ts} +32 -39
- package/src/assets/scene-decoder.ts +6 -1
- package/src/components/child-of.ts +15 -8
- package/src/components/children.ts +6 -0
- package/src/components/transform.ts +23 -81
- package/src/errors.ts +26 -2
- package/src/index.ts +10 -1
- package/src/instances/binding.ts +77 -0
- package/src/instances/externalization.ts +7 -1
- package/src/instances/scene-instances.ts +47 -76
- package/src/instances/state.ts +62 -0
- package/src/plugin.ts +9 -2
- package/src/systems/hierarchy-projection.ts +25 -8
- package/src/systems/index.ts +3 -0
- package/src/systems/propagate-transforms.ts +376 -245
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/transform-change-journal.unit.test.d.ts +0 -2
- package/dist/__tests__/transform-change-journal.unit.test.d.ts.map +0 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// @perf-budget-skip: intentional 100k-row allocation-sensitive performance benchmark.
|
|
2
|
+
// @ts-expect-error The Vitest runtime provides Node's fs module; scene declarations stay browser-safe.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { type EntityHandle, World } from '@forgeax/engine-ecs';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
beginDerivedRangeAllocationTrace,
|
|
8
|
+
type DerivedRangeAllocationTrace,
|
|
9
|
+
endDerivedRangeAllocationTrace,
|
|
10
|
+
} from '../../../ecs/src/query/derived-range-writer';
|
|
11
|
+
import { GlobalTransform, propagateTransforms, Transform } from '../index';
|
|
12
|
+
|
|
13
|
+
const SEED = 20260901;
|
|
14
|
+
const WARMUP_FRAMES = 5;
|
|
15
|
+
const SAMPLE_FRAMES = 20;
|
|
16
|
+
|
|
17
|
+
type Mode = 'full' | 'sparse' | 'mixed';
|
|
18
|
+
|
|
19
|
+
type Workload = {
|
|
20
|
+
readonly mode: Mode;
|
|
21
|
+
readonly rows: number;
|
|
22
|
+
readonly movers: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type FlatReceipt = {
|
|
26
|
+
readonly schemaVersion: 1;
|
|
27
|
+
readonly runner: string;
|
|
28
|
+
readonly featureSha: string;
|
|
29
|
+
readonly seed: number;
|
|
30
|
+
readonly workload: Workload;
|
|
31
|
+
readonly frameWindow: { readonly warmup: number; readonly samples: number };
|
|
32
|
+
readonly moverDensity: number;
|
|
33
|
+
readonly p50Ms: {
|
|
34
|
+
readonly writer: number;
|
|
35
|
+
readonly propagation: number;
|
|
36
|
+
readonly query: number;
|
|
37
|
+
readonly total: number;
|
|
38
|
+
};
|
|
39
|
+
readonly p95Ms: {
|
|
40
|
+
readonly writer: number;
|
|
41
|
+
readonly propagation: number;
|
|
42
|
+
readonly query: number;
|
|
43
|
+
readonly total: number;
|
|
44
|
+
};
|
|
45
|
+
readonly changedRows: number;
|
|
46
|
+
readonly changedRuns: number;
|
|
47
|
+
readonly querySpanCount: number;
|
|
48
|
+
readonly hierarchyRows: 0;
|
|
49
|
+
readonly worldChecksum: number;
|
|
50
|
+
readonly expectedChecksum: number;
|
|
51
|
+
readonly retainedHeapDeltaBytes: number | null;
|
|
52
|
+
readonly allocationEvidence: {
|
|
53
|
+
readonly typedArraySubarrayCalls: number;
|
|
54
|
+
readonly shapeAllocations: number;
|
|
55
|
+
readonly closureAllocations: number;
|
|
56
|
+
readonly columnWindowAllocations: number;
|
|
57
|
+
readonly rowFacadeAllocations: number;
|
|
58
|
+
readonly proxyAllocations: number;
|
|
59
|
+
readonly rangeCacheAllocations: number;
|
|
60
|
+
readonly bindingRebuilds: number;
|
|
61
|
+
readonly warmupFrames: number;
|
|
62
|
+
readonly propagationOnly: true;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const WORKLOADS: readonly Workload[] = [
|
|
67
|
+
{ mode: 'full', rows: 10_000, movers: 0 },
|
|
68
|
+
{ mode: 'full', rows: 10_000, movers: 10_000 },
|
|
69
|
+
{ mode: 'sparse', rows: 10_000, movers: 1 },
|
|
70
|
+
{ mode: 'sparse', rows: 10_000, movers: 100 },
|
|
71
|
+
{ mode: 'full', rows: 100_000, movers: 100_000 },
|
|
72
|
+
{ mode: 'sparse', rows: 100_000, movers: 1 },
|
|
73
|
+
{ mode: 'sparse', rows: 100_000, movers: 100 },
|
|
74
|
+
{ mode: 'mixed', rows: 100_000, movers: 10_000 },
|
|
75
|
+
{ mode: 'full', rows: 100_000, movers: 0 },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const processLike = (
|
|
79
|
+
globalThis as typeof globalThis & {
|
|
80
|
+
process?: { memoryUsage(): { heapUsed: number }; env?: Record<string, string | undefined> };
|
|
81
|
+
}
|
|
82
|
+
).process;
|
|
83
|
+
|
|
84
|
+
function percentile(values: readonly number[], fraction: number): number {
|
|
85
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
86
|
+
const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1);
|
|
87
|
+
return Number((sorted[index] ?? 0).toFixed(3));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function addMovers(
|
|
91
|
+
world: World,
|
|
92
|
+
entities: readonly EntityHandle[],
|
|
93
|
+
rows: number,
|
|
94
|
+
movers: number,
|
|
95
|
+
frame: number,
|
|
96
|
+
): number {
|
|
97
|
+
for (let mover = 0; mover < movers; mover += 1) {
|
|
98
|
+
const row = (mover * 97 + frame * 17 + SEED) % rows;
|
|
99
|
+
const entity = entities[row];
|
|
100
|
+
if (entity === undefined) throw new Error(`missing flat entity at row ${row}`);
|
|
101
|
+
world.set(entity, Transform, { pos: [row + frame + 1, 0, 0] }).unwrap();
|
|
102
|
+
}
|
|
103
|
+
return movers;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function countRuns(indices: number[]): number {
|
|
107
|
+
if (indices.length === 0) return 0;
|
|
108
|
+
indices.sort((left, right) => left - right);
|
|
109
|
+
let runs = 1;
|
|
110
|
+
for (let index = 1; index < indices.length; index += 1) {
|
|
111
|
+
if ((indices[index] ?? 0) !== (indices[index - 1] ?? 0) + 1) runs += 1;
|
|
112
|
+
}
|
|
113
|
+
return runs;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function spawnFlatWorld(rows: number): {
|
|
117
|
+
readonly world: World;
|
|
118
|
+
readonly entities: readonly EntityHandle[];
|
|
119
|
+
} {
|
|
120
|
+
const world = new World();
|
|
121
|
+
const entities = Array.from({ length: rows }, (_, row) =>
|
|
122
|
+
world.spawn({ component: Transform, data: { pos: [row, 0, 0] } }).unwrap(),
|
|
123
|
+
);
|
|
124
|
+
return { world, entities };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function measurePropagationAllocations(world: World): DerivedRangeAllocationTrace {
|
|
128
|
+
const prototype = Float32Array.prototype;
|
|
129
|
+
const original = prototype.subarray;
|
|
130
|
+
let calls = 0;
|
|
131
|
+
beginDerivedRangeAllocationTrace();
|
|
132
|
+
Object.defineProperty(prototype, 'subarray', {
|
|
133
|
+
configurable: true,
|
|
134
|
+
value: function countedSubarray(
|
|
135
|
+
this: Float32Array,
|
|
136
|
+
begin?: number,
|
|
137
|
+
end?: number,
|
|
138
|
+
): Float32Array {
|
|
139
|
+
calls += 1;
|
|
140
|
+
return original.call(this, begin, end);
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
propagateTransforms(world).unwrap();
|
|
145
|
+
} finally {
|
|
146
|
+
Object.defineProperty(prototype, 'subarray', {
|
|
147
|
+
configurable: true,
|
|
148
|
+
value: original,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const trace = endDerivedRangeAllocationTrace();
|
|
152
|
+
trace.subarrayCalls = calls;
|
|
153
|
+
return trace;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function addAllocationTrace(
|
|
157
|
+
target: DerivedRangeAllocationTrace,
|
|
158
|
+
source: DerivedRangeAllocationTrace,
|
|
159
|
+
): void {
|
|
160
|
+
target.subarrayCalls += source.subarrayCalls;
|
|
161
|
+
target.shapeAllocations += source.shapeAllocations;
|
|
162
|
+
target.closureAllocations += source.closureAllocations;
|
|
163
|
+
target.columnWindowAllocations += source.columnWindowAllocations;
|
|
164
|
+
target.rowFacadeAllocations += source.rowFacadeAllocations;
|
|
165
|
+
target.proxyAllocations += source.proxyAllocations;
|
|
166
|
+
target.rangeCacheAllocations += source.rangeCacheAllocations;
|
|
167
|
+
target.bindingRebuilds += source.bindingRebuilds;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function runWorkload(workload: Workload): FlatReceipt {
|
|
171
|
+
const { world, entities } = spawnFlatWorld(workload.rows);
|
|
172
|
+
const source = world.query({ read: [Transform, GlobalTransform] }).unwrap();
|
|
173
|
+
const spans = source.spans().unwrap();
|
|
174
|
+
const querySpanCount = [...spans].length;
|
|
175
|
+
const changed = world.query({ read: [Transform], changed: [Transform] }).unwrap();
|
|
176
|
+
const expectedLocal = new Float64Array(workload.rows);
|
|
177
|
+
for (let row = 0; row < workload.rows; row += 1) expectedLocal[row] = row;
|
|
178
|
+
propagateTransforms(world).unwrap();
|
|
179
|
+
const propagationAllocationTrace: DerivedRangeAllocationTrace = {
|
|
180
|
+
subarrayCalls: 0,
|
|
181
|
+
shapeAllocations: 0,
|
|
182
|
+
closureAllocations: 0,
|
|
183
|
+
columnWindowAllocations: 0,
|
|
184
|
+
rowFacadeAllocations: 0,
|
|
185
|
+
proxyAllocations: 0,
|
|
186
|
+
rangeCacheAllocations: 0,
|
|
187
|
+
bindingRebuilds: 0,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
for (let frame = 0; frame < WARMUP_FRAMES; frame += 1) {
|
|
191
|
+
addMovers(world, entities, workload.rows, workload.movers, frame);
|
|
192
|
+
for (let mover = 0; mover < workload.movers; mover += 1) {
|
|
193
|
+
const row = (mover * 97 + frame * 17 + SEED) % workload.rows;
|
|
194
|
+
expectedLocal[row] = row + frame + 1;
|
|
195
|
+
}
|
|
196
|
+
addAllocationTrace(propagationAllocationTrace, measurePropagationAllocations(world));
|
|
197
|
+
[...changed];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const writerTimes: number[] = [];
|
|
201
|
+
const propagationTimes: number[] = [];
|
|
202
|
+
const queryTimes: number[] = [];
|
|
203
|
+
const totalTimes: number[] = [];
|
|
204
|
+
let changedRows = 0;
|
|
205
|
+
let changedRuns = 0;
|
|
206
|
+
let checksum = 0;
|
|
207
|
+
const expectedFrameChecksums: number[] = [];
|
|
208
|
+
const heapBefore = processLike?.memoryUsage().heapUsed;
|
|
209
|
+
for (let frame = WARMUP_FRAMES; frame < WARMUP_FRAMES + SAMPLE_FRAMES; frame += 1) {
|
|
210
|
+
const totalStart = performance.now();
|
|
211
|
+
const writerStart = performance.now();
|
|
212
|
+
const expectedChangedRows = addMovers(world, entities, workload.rows, workload.movers, frame);
|
|
213
|
+
for (let mover = 0; mover < workload.movers; mover += 1) {
|
|
214
|
+
const row = (mover * 97 + frame * 17 + SEED) % workload.rows;
|
|
215
|
+
expectedLocal[row] = row + frame + 1;
|
|
216
|
+
}
|
|
217
|
+
writerTimes.push(performance.now() - writerStart);
|
|
218
|
+
|
|
219
|
+
const changedIndices: number[] = [];
|
|
220
|
+
for (const row of changed) changedIndices.push(row.entity & 0xffffff);
|
|
221
|
+
expect(changedIndices.length).toBe(expectedChangedRows);
|
|
222
|
+
changedRows += changedIndices.length;
|
|
223
|
+
changedRuns += countRuns(changedIndices);
|
|
224
|
+
|
|
225
|
+
const propagationStart = performance.now();
|
|
226
|
+
addAllocationTrace(propagationAllocationTrace, measurePropagationAllocations(world));
|
|
227
|
+
propagationTimes.push(performance.now() - propagationStart);
|
|
228
|
+
|
|
229
|
+
const queryStart = performance.now();
|
|
230
|
+
let frameChecksum = 0;
|
|
231
|
+
for (const row of world.query({ read: [GlobalTransform] }).unwrap()) {
|
|
232
|
+
frameChecksum += row.get(GlobalTransform).world[12] ?? 0;
|
|
233
|
+
}
|
|
234
|
+
let expectedFrameChecksum = 0;
|
|
235
|
+
for (let row = 0; row < workload.rows; row += 1)
|
|
236
|
+
expectedFrameChecksum += expectedLocal[row] ?? 0;
|
|
237
|
+
expectedFrameChecksums.push(expectedFrameChecksum);
|
|
238
|
+
queryTimes.push(performance.now() - queryStart);
|
|
239
|
+
checksum = (checksum + Math.round(frameChecksum * 1000) + SEED + frame) >>> 0;
|
|
240
|
+
totalTimes.push(performance.now() - totalStart);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let expectedChecksum = 0;
|
|
244
|
+
for (let sample = 0; sample < SAMPLE_FRAMES; sample += 1) {
|
|
245
|
+
const frame = WARMUP_FRAMES + sample;
|
|
246
|
+
const frameChecksum = expectedFrameChecksums[sample] ?? 0;
|
|
247
|
+
expectedChecksum = (expectedChecksum + Math.round(frameChecksum * 1000) + SEED + frame) >>> 0;
|
|
248
|
+
}
|
|
249
|
+
const heapAfter = processLike?.memoryUsage().heapUsed;
|
|
250
|
+
const retainedHeapDeltaBytes =
|
|
251
|
+
heapBefore === undefined || heapAfter === undefined
|
|
252
|
+
? null
|
|
253
|
+
: Math.max(0, heapAfter - heapBefore);
|
|
254
|
+
return {
|
|
255
|
+
schemaVersion: 1,
|
|
256
|
+
runner: 'vitest-node',
|
|
257
|
+
featureSha: processLike?.env?.FORGEAX_FEATURE_SHA ?? 'unbound',
|
|
258
|
+
seed: SEED,
|
|
259
|
+
workload,
|
|
260
|
+
frameWindow: { warmup: WARMUP_FRAMES, samples: SAMPLE_FRAMES },
|
|
261
|
+
moverDensity: workload.movers / workload.rows,
|
|
262
|
+
p50Ms: {
|
|
263
|
+
writer: percentile(writerTimes, 0.5),
|
|
264
|
+
propagation: percentile(propagationTimes, 0.5),
|
|
265
|
+
query: percentile(queryTimes, 0.5),
|
|
266
|
+
total: percentile(totalTimes, 0.5),
|
|
267
|
+
},
|
|
268
|
+
p95Ms: {
|
|
269
|
+
writer: percentile(writerTimes, 0.95),
|
|
270
|
+
propagation: percentile(propagationTimes, 0.95),
|
|
271
|
+
query: percentile(queryTimes, 0.95),
|
|
272
|
+
total: percentile(totalTimes, 0.95),
|
|
273
|
+
},
|
|
274
|
+
changedRows: Math.round(changedRows / SAMPLE_FRAMES),
|
|
275
|
+
changedRuns: Math.round(changedRuns / SAMPLE_FRAMES),
|
|
276
|
+
querySpanCount,
|
|
277
|
+
hierarchyRows: 0,
|
|
278
|
+
worldChecksum: checksum,
|
|
279
|
+
expectedChecksum,
|
|
280
|
+
retainedHeapDeltaBytes,
|
|
281
|
+
allocationEvidence: {
|
|
282
|
+
typedArraySubarrayCalls: propagationAllocationTrace.subarrayCalls,
|
|
283
|
+
shapeAllocations: propagationAllocationTrace.shapeAllocations,
|
|
284
|
+
closureAllocations: propagationAllocationTrace.closureAllocations,
|
|
285
|
+
columnWindowAllocations: propagationAllocationTrace.columnWindowAllocations,
|
|
286
|
+
rowFacadeAllocations: propagationAllocationTrace.rowFacadeAllocations,
|
|
287
|
+
proxyAllocations: propagationAllocationTrace.proxyAllocations,
|
|
288
|
+
rangeCacheAllocations: propagationAllocationTrace.rangeCacheAllocations,
|
|
289
|
+
bindingRebuilds: propagationAllocationTrace.bindingRebuilds,
|
|
290
|
+
warmupFrames: WARMUP_FRAMES,
|
|
291
|
+
propagationOnly: true,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
describe('real ECS flat Transform propagation benchmark', () => {
|
|
297
|
+
// Coverage instrumentation expands the 100k-row allocation-sensitive workload substantially;
|
|
298
|
+
// keep its explicit benchmark budget separate from ordinary unit tests.
|
|
299
|
+
it('covers dynamic full, sparse, and mixed C workloads without a static shortcut', () => {
|
|
300
|
+
const receipts = WORKLOADS.map(runWorkload);
|
|
301
|
+
// biome-ignore lint/suspicious/noConsole: benchmark output is the machine evidence receipt.
|
|
302
|
+
console.info(JSON.stringify({ schemaVersion: 1, receipts }));
|
|
303
|
+
expect(receipts).toHaveLength(WORKLOADS.length);
|
|
304
|
+
expect(receipts.every((receipt) => receipt.querySpanCount === 1)).toBe(true);
|
|
305
|
+
expect(receipts.every((receipt) => receipt.hierarchyRows === 0)).toBe(true);
|
|
306
|
+
expect(receipts.every((receipt) => receipt.worldChecksum === receipt.expectedChecksum)).toBe(
|
|
307
|
+
true,
|
|
308
|
+
);
|
|
309
|
+
expect(
|
|
310
|
+
receipts.every(
|
|
311
|
+
(receipt) =>
|
|
312
|
+
receipt.workload.movers === 0 || (receipt.changedRows > 0 && receipt.changedRuns > 0),
|
|
313
|
+
),
|
|
314
|
+
).toBe(true);
|
|
315
|
+
expect(receipts.every((receipt) => receipt.allocationEvidence.propagationOnly)).toBe(true);
|
|
316
|
+
expect(
|
|
317
|
+
receipts.every((receipt) =>
|
|
318
|
+
Object.entries(receipt.allocationEvidence)
|
|
319
|
+
.filter(([name]) => name.endsWith('Allocations'))
|
|
320
|
+
.every(([, count]) => count === 0),
|
|
321
|
+
),
|
|
322
|
+
).toBe(true);
|
|
323
|
+
expect(
|
|
324
|
+
receipts.every(
|
|
325
|
+
(receipt) =>
|
|
326
|
+
receipt.allocationEvidence.typedArraySubarrayCalls <=
|
|
327
|
+
receipt.changedRuns * 4 * (WARMUP_FRAMES + SAMPLE_FRAMES),
|
|
328
|
+
),
|
|
329
|
+
).toBe(true);
|
|
330
|
+
|
|
331
|
+
const source = readFileSync('packages/scene/src/systems/propagate-transforms.ts', 'utf8');
|
|
332
|
+
expect(source).not.toMatch(/new (Map|Set)|\\b(Map|Set)</);
|
|
333
|
+
// biome-ignore lint/suspicious/noConsole: benchmark output is the machine evidence receipt.
|
|
334
|
+
console.info(JSON.stringify({ schemaVersion: 1, receipts }));
|
|
335
|
+
}, 120_000);
|
|
336
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createWorldContext, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { ChildOf, scenePlugin, Transform } from '../index';
|
|
3
|
+
import { ChildOf, GlobalTransform, scenePlugin, Transform } from '../index';
|
|
4
4
|
|
|
5
5
|
describe('scenePlugin', () => {
|
|
6
6
|
it('installs propagation without render or animation capabilities', async () => {
|
|
@@ -14,7 +14,7 @@ describe('scenePlugin', () => {
|
|
|
14
14
|
)
|
|
15
15
|
.unwrap();
|
|
16
16
|
world.update(1 / 60).unwrap();
|
|
17
|
-
expect(world.get(child,
|
|
17
|
+
expect(world.get(child, GlobalTransform).unwrap().world[12]).toBeCloseTo(5);
|
|
18
18
|
await ctx.fiber.dispose();
|
|
19
19
|
expect(world.inspect().systems.some((system) => system.name === 'propagateTransforms')).toBe(
|
|
20
20
|
false,
|