@forgeax/engine-scene 0.1.23 → 0.1.25
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 +45 -2
- package/dist/__tests__/fixtures/malformed-hierarchy-edge.d.ts +5 -3
- package/dist/__tests__/fixtures/malformed-hierarchy-edge.d.ts.map +1 -1
- package/dist/__tests__/hierarchy-propagation.perf.test.d.ts +2 -0
- package/dist/__tests__/hierarchy-propagation.perf.test.d.ts.map +1 -0
- package/dist/components/child-of.d.ts +11 -9
- package/dist/components/child-of.d.ts.map +1 -1
- package/dist/components/children.d.ts +14 -13
- package/dist/components/children.d.ts.map +1 -1
- package/dist/index.mjs +677 -157
- package/dist/index.mjs.map +1 -1
- package/dist/instances/scene-instances.d.ts +1 -1
- package/dist/systems/propagate-transforms.d.ts +21 -0
- package/dist/systems/propagate-transforms.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/fixtures/malformed-hierarchy-edge.ts +26 -4
- package/src/__tests__/hierarchy-propagation.perf.test.ts +149 -0
- package/src/__tests__/propagation.unit.test.ts +70 -6
- package/src/components/child-of.ts +26 -22
- package/src/components/children.ts +30 -37
- package/src/instances/scene-instances.ts +4 -4
- package/src/systems/propagate-transforms.ts +966 -195
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
defineSystem,
|
|
3
3
|
defineSystemSet,
|
|
4
4
|
type EcsError,
|
|
5
|
+
ENTITY_NULL_RAW,
|
|
5
6
|
type EntityHandle,
|
|
6
7
|
FixedUpdate,
|
|
7
8
|
type Query,
|
|
@@ -9,9 +10,17 @@ import {
|
|
|
9
10
|
Update,
|
|
10
11
|
type World,
|
|
11
12
|
} from '@forgeax/engine-ecs';
|
|
13
|
+
import {
|
|
14
|
+
type DerivedColumnBinding,
|
|
15
|
+
type DerivedRangeCursor,
|
|
16
|
+
type DerivedRangeWriter,
|
|
17
|
+
getDerivedWriter,
|
|
18
|
+
worldInternal,
|
|
19
|
+
} from '@forgeax/engine-ecs/internal';
|
|
12
20
|
import { type Mat4, mat4 } from '@forgeax/engine-math';
|
|
13
21
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
14
22
|
import { ChildOf } from '../components/child-of';
|
|
23
|
+
import { Children } from '../components/children';
|
|
15
24
|
import { GlobalTransform, Transform } from '../components/transform';
|
|
16
25
|
import { SceneError } from '../errors';
|
|
17
26
|
|
|
@@ -20,22 +29,129 @@ export const PROPAGATE_TRANSFORMS_FIXED_SYSTEM = 'propagateTransformsFixed' as c
|
|
|
20
29
|
export const TransformSet = defineSystemSet({ name: 'transform' });
|
|
21
30
|
export const TransformFixedSet = defineSystemSet({ name: 'transform-fixed' });
|
|
22
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Optional, test-owned counters for the parent-first executor. The counters
|
|
34
|
+
* are disabled unless a caller brackets a run with begin/end; production
|
|
35
|
+
* propagation therefore pays only one predictable branch per instrumented
|
|
36
|
+
* event. They are deliberately not a second execution state or a public
|
|
37
|
+
* dirty/cache contract.
|
|
38
|
+
*/
|
|
39
|
+
export interface TransformPropagationTrace {
|
|
40
|
+
hierarchyRootInvocations: number;
|
|
41
|
+
hierarchyRootCursorReuses: number;
|
|
42
|
+
hierarchyRootCursorAllocations: number;
|
|
43
|
+
hierarchyEntityLookups: number;
|
|
44
|
+
hierarchyRowsEvaluated: number;
|
|
45
|
+
hierarchyEdgesVisited: number;
|
|
46
|
+
hierarchyPublishedRows: number;
|
|
47
|
+
hierarchyPublishedRuns: number;
|
|
48
|
+
hierarchyResidualParentProbes: number;
|
|
49
|
+
flatStructuralRootRows: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let propagationTrace: TransformPropagationTrace | undefined;
|
|
53
|
+
let hierarchyRootCursorAllocationCount = 0;
|
|
54
|
+
let propagationTraceRootCursorAllocationStart = 0;
|
|
55
|
+
|
|
56
|
+
function createHierarchyRootCursor(): DerivedRangeCursor {
|
|
57
|
+
hierarchyRootCursorAllocationCount += 1;
|
|
58
|
+
return { bindingIndex: -1, row: -1 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function beginTransformPropagationTrace(): void {
|
|
62
|
+
propagationTraceRootCursorAllocationStart = hierarchyRootCursorAllocationCount;
|
|
63
|
+
propagationTrace = {
|
|
64
|
+
hierarchyRootInvocations: 0,
|
|
65
|
+
hierarchyRootCursorReuses: 0,
|
|
66
|
+
hierarchyRootCursorAllocations: 0,
|
|
67
|
+
hierarchyEntityLookups: 0,
|
|
68
|
+
hierarchyRowsEvaluated: 0,
|
|
69
|
+
hierarchyEdgesVisited: 0,
|
|
70
|
+
hierarchyPublishedRows: 0,
|
|
71
|
+
hierarchyPublishedRuns: 0,
|
|
72
|
+
hierarchyResidualParentProbes: 0,
|
|
73
|
+
flatStructuralRootRows: 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function endTransformPropagationTrace(): TransformPropagationTrace {
|
|
78
|
+
const trace = propagationTrace;
|
|
79
|
+
propagationTrace = undefined;
|
|
80
|
+
const rootCursorAllocations =
|
|
81
|
+
hierarchyRootCursorAllocationCount - propagationTraceRootCursorAllocationStart;
|
|
82
|
+
propagationTraceRootCursorAllocationStart = hierarchyRootCursorAllocationCount;
|
|
83
|
+
if (trace !== undefined) {
|
|
84
|
+
trace.hierarchyRootCursorAllocations = rootCursorAllocations;
|
|
85
|
+
return trace;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
hierarchyRootInvocations: 0,
|
|
89
|
+
hierarchyRootCursorReuses: 0,
|
|
90
|
+
hierarchyRootCursorAllocations: 0,
|
|
91
|
+
hierarchyEntityLookups: 0,
|
|
92
|
+
hierarchyRowsEvaluated: 0,
|
|
93
|
+
hierarchyEdgesVisited: 0,
|
|
94
|
+
hierarchyPublishedRows: 0,
|
|
95
|
+
hierarchyPublishedRuns: 0,
|
|
96
|
+
hierarchyResidualParentProbes: 0,
|
|
97
|
+
flatStructuralRootRows: 0,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function countPropagation(name: keyof TransformPropagationTrace): void {
|
|
102
|
+
const trace = propagationTrace;
|
|
103
|
+
if (trace !== undefined) trace[name] += 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
23
106
|
interface Scratch {
|
|
24
107
|
position: Float32Array;
|
|
25
108
|
rotation: Float32Array;
|
|
26
109
|
scale: Float32Array;
|
|
27
110
|
local: Mat4;
|
|
28
|
-
|
|
111
|
+
parent: Mat4;
|
|
112
|
+
candidate: Mat4;
|
|
113
|
+
hierarchyStackEntities: EntityHandle[];
|
|
114
|
+
hierarchyStackChildren: number[];
|
|
115
|
+
hierarchyProbeEntities: EntityHandle[];
|
|
116
|
+
hierarchyCurrentCursor: DerivedRangeCursor;
|
|
117
|
+
hierarchyParentCursor: DerivedRangeCursor;
|
|
118
|
+
hierarchyChildCursor: DerivedRangeCursor;
|
|
119
|
+
hierarchyResidualCursor: DerivedRangeCursor;
|
|
120
|
+
hierarchyResidualParentCursor: DerivedRangeCursor;
|
|
121
|
+
hierarchyRootCursor: DerivedRangeCursor;
|
|
122
|
+
hierarchyStates: Uint8Array[];
|
|
123
|
+
hierarchyChanged: Uint8Array[];
|
|
124
|
+
hierarchyBindingTables: number[];
|
|
125
|
+
hierarchyBindingRows: number[];
|
|
126
|
+
flatChanged: Uint8Array[];
|
|
127
|
+
flatBindingTables: number[];
|
|
128
|
+
flatBindingRows: number[];
|
|
129
|
+
flatStructureEpoch: number;
|
|
29
130
|
flatQuery?: FlatQuery;
|
|
30
|
-
outputQuery?: OutputQuery;
|
|
31
131
|
hierarchyQuery?: HierarchyQuery;
|
|
132
|
+
transformQuery?: TransformQuery;
|
|
133
|
+
hierarchyWriter?: HierarchyWriter;
|
|
134
|
+
transformWriter?: TransformWriter;
|
|
32
135
|
missingGlobalQuery?: MissingGlobalQuery;
|
|
33
136
|
missingTransformQuery?: MissingTransformQuery;
|
|
34
137
|
}
|
|
35
138
|
|
|
36
|
-
type OutputQuery = Query<readonly [], readonly [typeof GlobalTransform]>;
|
|
37
139
|
type FlatQuery = Query<readonly [typeof Transform], readonly [typeof GlobalTransform]>;
|
|
38
|
-
type HierarchyQuery = Query<
|
|
140
|
+
type HierarchyQuery = Query<
|
|
141
|
+
readonly [typeof Transform, typeof ChildOf],
|
|
142
|
+
readonly [typeof GlobalTransform]
|
|
143
|
+
>;
|
|
144
|
+
type TransformQuery = Query<readonly [typeof Transform], readonly [typeof GlobalTransform]>;
|
|
145
|
+
type HierarchyWriter = DerivedRangeWriter<
|
|
146
|
+
typeof Transform | typeof ChildOf,
|
|
147
|
+
typeof GlobalTransform
|
|
148
|
+
>;
|
|
149
|
+
type TransformWriter = DerivedRangeWriter<typeof Transform, typeof GlobalTransform>;
|
|
150
|
+
type HierarchyBinding = DerivedColumnBinding<
|
|
151
|
+
typeof Transform | typeof ChildOf,
|
|
152
|
+
typeof GlobalTransform
|
|
153
|
+
>;
|
|
154
|
+
type TransformBinding = DerivedColumnBinding<typeof Transform, typeof GlobalTransform>;
|
|
39
155
|
type MissingGlobalQuery = Query<readonly [], readonly [], readonly []>;
|
|
40
156
|
type MissingTransformQuery = Query<readonly [], readonly [], readonly []>;
|
|
41
157
|
|
|
@@ -60,8 +176,10 @@ function pairError(entity: EntityHandle, expected: string): Result<void, SceneEr
|
|
|
60
176
|
function ensureQueries(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
61
177
|
if (
|
|
62
178
|
scratch.flatQuery !== undefined &&
|
|
63
|
-
scratch.outputQuery !== undefined &&
|
|
64
179
|
scratch.hierarchyQuery !== undefined &&
|
|
180
|
+
scratch.transformQuery !== undefined &&
|
|
181
|
+
scratch.hierarchyWriter !== undefined &&
|
|
182
|
+
scratch.transformWriter !== undefined &&
|
|
65
183
|
scratch.missingGlobalQuery !== undefined &&
|
|
66
184
|
scratch.missingTransformQuery !== undefined
|
|
67
185
|
) {
|
|
@@ -73,16 +191,29 @@ function ensureQueries(world: World, scratch: Scratch): Result<void, SceneError>
|
|
|
73
191
|
without: [ChildOf],
|
|
74
192
|
changed: [Transform],
|
|
75
193
|
});
|
|
76
|
-
const
|
|
77
|
-
const
|
|
194
|
+
const hierarchy = world.query({ read: [Transform, ChildOf], write: [GlobalTransform] });
|
|
195
|
+
const transform = world.query({ read: [Transform], write: [GlobalTransform] });
|
|
78
196
|
const missingGlobal = world.query({ with: [Transform], without: [GlobalTransform] });
|
|
79
197
|
const missingTransform = world.query({ with: [GlobalTransform], without: [Transform] });
|
|
80
|
-
if (
|
|
198
|
+
if (
|
|
199
|
+
!flatOutput.ok ||
|
|
200
|
+
!hierarchy.ok ||
|
|
201
|
+
!transform.ok ||
|
|
202
|
+
!missingGlobal.ok ||
|
|
203
|
+
!missingTransform.ok
|
|
204
|
+
) {
|
|
81
205
|
return pairError(0 as EntityHandle, 'valid Transform and GlobalTransform pair queries');
|
|
82
206
|
}
|
|
207
|
+
const hierarchyWriter = getDerivedWriter(hierarchy.value, GlobalTransform);
|
|
208
|
+
const transformWriter = getDerivedWriter(transform.value, GlobalTransform);
|
|
209
|
+
if (!hierarchyWriter.ok || !transformWriter.ok) {
|
|
210
|
+
return pairError(0 as EntityHandle, 'dense Transform and ChildOf derived bindings');
|
|
211
|
+
}
|
|
83
212
|
scratch.flatQuery = flatOutput.value as FlatQuery;
|
|
84
|
-
scratch.outputQuery = output.value as OutputQuery;
|
|
85
213
|
scratch.hierarchyQuery = hierarchy.value as HierarchyQuery;
|
|
214
|
+
scratch.transformQuery = transform.value as TransformQuery;
|
|
215
|
+
scratch.hierarchyWriter = hierarchyWriter.value as HierarchyWriter;
|
|
216
|
+
scratch.transformWriter = transformWriter.value as TransformWriter;
|
|
86
217
|
scratch.missingGlobalQuery = missingGlobal.value as MissingGlobalQuery;
|
|
87
218
|
scratch.missingTransformQuery = missingTransform.value as MissingTransformQuery;
|
|
88
219
|
return ok(undefined);
|
|
@@ -113,7 +244,25 @@ function scratchFor(world: World): Scratch {
|
|
|
113
244
|
rotation: new Float32Array(4),
|
|
114
245
|
scale: new Float32Array(3),
|
|
115
246
|
local: mat4.create(),
|
|
116
|
-
|
|
247
|
+
parent: mat4.create(),
|
|
248
|
+
candidate: mat4.create(),
|
|
249
|
+
hierarchyStackEntities: [] as EntityHandle[],
|
|
250
|
+
hierarchyStackChildren: [] as number[],
|
|
251
|
+
hierarchyProbeEntities: [] as EntityHandle[],
|
|
252
|
+
hierarchyCurrentCursor: { bindingIndex: -1, row: -1 },
|
|
253
|
+
hierarchyParentCursor: { bindingIndex: -1, row: -1 },
|
|
254
|
+
hierarchyChildCursor: { bindingIndex: -1, row: -1 },
|
|
255
|
+
hierarchyResidualCursor: { bindingIndex: -1, row: -1 },
|
|
256
|
+
hierarchyResidualParentCursor: { bindingIndex: -1, row: -1 },
|
|
257
|
+
hierarchyRootCursor: createHierarchyRootCursor(),
|
|
258
|
+
hierarchyStates: [],
|
|
259
|
+
hierarchyChanged: [],
|
|
260
|
+
hierarchyBindingTables: [],
|
|
261
|
+
hierarchyBindingRows: [],
|
|
262
|
+
flatChanged: [],
|
|
263
|
+
flatBindingTables: [],
|
|
264
|
+
flatBindingRows: [],
|
|
265
|
+
flatStructureEpoch: -1,
|
|
117
266
|
};
|
|
118
267
|
SCRATCH.set(world, created);
|
|
119
268
|
return created;
|
|
@@ -191,46 +340,7 @@ function composeFlatColumns(
|
|
|
191
340
|
}
|
|
192
341
|
}
|
|
193
342
|
|
|
194
|
-
function
|
|
195
|
-
world: World,
|
|
196
|
-
output: ReturnType<World['query']> extends Result<infer Q, unknown> ? Q : never,
|
|
197
|
-
entity: EntityHandle,
|
|
198
|
-
value: Mat4,
|
|
199
|
-
): Result<void, SceneError> {
|
|
200
|
-
const row = output.at(entity);
|
|
201
|
-
if (row === undefined) {
|
|
202
|
-
return err(
|
|
203
|
-
new SceneError({
|
|
204
|
-
code: 'hierarchy-broken',
|
|
205
|
-
expected: 'each Transform entity to carry a GlobalTransform pair',
|
|
206
|
-
hint: 'attach GlobalTransform at scene authoring or import time, then retry propagation',
|
|
207
|
-
detail: { entity, parent: entity },
|
|
208
|
-
}),
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
const current = world.get(entity, GlobalTransform);
|
|
212
|
-
if (!current.ok)
|
|
213
|
-
return err(
|
|
214
|
-
new SceneError({
|
|
215
|
-
code: 'hierarchy-broken',
|
|
216
|
-
expected: 'each Transform entity to carry a GlobalTransform pair',
|
|
217
|
-
hint: 'attach GlobalTransform at scene authoring or import time, then retry propagation',
|
|
218
|
-
detail: { entity, parent: entity },
|
|
219
|
-
}),
|
|
220
|
-
);
|
|
221
|
-
let changed = false;
|
|
222
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
223
|
-
if (current.value.world[index] !== value[index]) {
|
|
224
|
-
changed = true;
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (!changed) return ok(undefined);
|
|
229
|
-
row.mut(GlobalTransform).world.set(value);
|
|
230
|
-
return ok(undefined);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function propagateFlat(scratch: Scratch): Result<void, SceneError> {
|
|
343
|
+
function propagateFlat(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
234
344
|
const query = scratch.flatQuery;
|
|
235
345
|
if (query === undefined)
|
|
236
346
|
return err(
|
|
@@ -252,182 +362,843 @@ function propagateFlat(scratch: Scratch): Result<void, SceneError> {
|
|
|
252
362
|
}
|
|
253
363
|
} catch (cause) {
|
|
254
364
|
const error = cause as EcsError;
|
|
255
|
-
return err(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
365
|
+
return err(derivedWriteError(error, bindingIndex));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// A structural relation change can turn an unchanged Transform row into a
|
|
369
|
+
// flat root (most importantly ChildOf removal). The changed Transform query
|
|
370
|
+
// above cannot observe that transition, so once per structure epoch scan the
|
|
371
|
+
// already-bound numeric Transform rows and publish only differing roots.
|
|
372
|
+
const transformWriter = scratch.transformWriter;
|
|
373
|
+
if (transformWriter === undefined) {
|
|
374
|
+
return pairError(0 as EntityHandle, 'dense Transform and GlobalTransform derived bindings');
|
|
375
|
+
}
|
|
376
|
+
const transformBindings = transformWriter.bindings as readonly TransformBinding[];
|
|
377
|
+
const structureEpoch = world[worldInternal].getStructureEpoch();
|
|
378
|
+
if (scratch.flatStructureEpoch === structureEpoch) return ok(undefined);
|
|
379
|
+
|
|
380
|
+
ensureFlatBuffers(scratch, transformBindings);
|
|
381
|
+
resetFlatBuffers(scratch);
|
|
382
|
+
for (let bindingIndex = 0; bindingIndex < transformBindings.length; bindingIndex += 1) {
|
|
383
|
+
const binding = transformBindings[bindingIndex];
|
|
384
|
+
const changed = scratch.flatChanged[bindingIndex];
|
|
385
|
+
if (binding === undefined || changed === undefined) continue;
|
|
386
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
387
|
+
const entity = (binding.entities[row] ?? 0) as EntityHandle;
|
|
388
|
+
const parentRaw = world[worldInternal].getFieldValue(entity, ChildOf, 'parent');
|
|
389
|
+
if (parentRaw !== undefined && parentRaw !== ENTITY_NULL_RAW) continue;
|
|
390
|
+
countPropagation('flatStructuralRootRows');
|
|
391
|
+
composeBindingRow(binding, row, undefined, 0, scratch, changed);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (let bindingIndex = 0; bindingIndex < transformBindings.length; bindingIndex += 1) {
|
|
395
|
+
const changed = scratch.flatChanged[bindingIndex];
|
|
396
|
+
if (changed === undefined) continue;
|
|
397
|
+
const published = transformWriter.publishChangedRows(bindingIndex, changed);
|
|
398
|
+
if (!published.ok) return err(derivedWriteError(published.error, bindingIndex));
|
|
272
399
|
}
|
|
400
|
+
scratch.flatStructureEpoch = structureEpoch;
|
|
273
401
|
return ok(undefined);
|
|
274
402
|
}
|
|
275
403
|
|
|
276
|
-
interface
|
|
277
|
-
readonly
|
|
278
|
-
|
|
279
|
-
readonly
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
404
|
+
interface TransformColumnShape {
|
|
405
|
+
readonly pos: ArrayLike<number>;
|
|
406
|
+
readonly quat: ArrayLike<number>;
|
|
407
|
+
readonly scale: ArrayLike<number>;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
interface OutputColumnShape {
|
|
411
|
+
readonly world: Float32Array;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface HierarchyColumnShape extends TransformColumnShape {
|
|
415
|
+
readonly parent: ArrayLike<number>;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function transformColumns(binding: TransformBinding | HierarchyBinding): TransformColumnShape {
|
|
419
|
+
return binding.read as unknown as TransformColumnShape;
|
|
284
420
|
}
|
|
285
421
|
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
422
|
+
function hierarchyColumns(binding: HierarchyBinding): HierarchyColumnShape {
|
|
423
|
+
return binding.read as unknown as HierarchyColumnShape;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function worldColumn(binding: TransformBinding | HierarchyBinding): Float32Array {
|
|
427
|
+
return (binding.write as unknown as OutputColumnShape).world;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function hierarchyError(
|
|
431
|
+
code: 'hierarchy-broken' | 'hierarchy-cycle',
|
|
432
|
+
entity: EntityHandle,
|
|
433
|
+
parent: EntityHandle,
|
|
434
|
+
expected: string,
|
|
435
|
+
hint: string,
|
|
436
|
+
): SceneError {
|
|
437
|
+
return new SceneError({ code, expected, hint, detail: { entity, parent } });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function writeCandidate(
|
|
441
|
+
binding: TransformBinding | HierarchyBinding,
|
|
442
|
+
row: number,
|
|
443
|
+
candidate: Mat4,
|
|
444
|
+
changed: Uint8Array,
|
|
445
|
+
): void {
|
|
446
|
+
const worlds = worldColumn(binding);
|
|
447
|
+
const base = row * 16;
|
|
448
|
+
for (let index = 0; index < 16; index += 1) {
|
|
449
|
+
if (worlds[base + index] !== candidate[index]) {
|
|
450
|
+
worlds.set(candidate, base);
|
|
451
|
+
changed[row] = 1;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
289
454
|
}
|
|
290
|
-
return -1;
|
|
291
455
|
}
|
|
292
456
|
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
457
|
+
function composeBindingRow(
|
|
458
|
+
binding: TransformBinding | HierarchyBinding,
|
|
459
|
+
row: number,
|
|
460
|
+
parentBinding: TransformBinding | undefined,
|
|
461
|
+
parentRow: number,
|
|
298
462
|
scratch: Scratch,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
463
|
+
changed: Uint8Array,
|
|
464
|
+
): void {
|
|
465
|
+
// Hierarchy bindings carry the parent column; flat structural-root repair
|
|
466
|
+
// uses the same numeric kernel but is intentionally counted separately.
|
|
467
|
+
if ('parent' in (binding.read as object)) countPropagation('hierarchyRowsEvaluated');
|
|
468
|
+
const local = transformColumns(binding);
|
|
469
|
+
const offset = row * 3;
|
|
470
|
+
composeColumns(local.pos, local.quat, local.scale, scratch.local, scratch, offset, row * 4);
|
|
471
|
+
if (parentBinding === undefined) {
|
|
472
|
+
scratch.candidate.set(scratch.local);
|
|
473
|
+
} else {
|
|
474
|
+
const parentWorld = worldColumn(parentBinding);
|
|
475
|
+
const parentOffset = parentRow * 16;
|
|
476
|
+
for (let index = 0; index < 16; index += 1) {
|
|
477
|
+
scratch.parent[index] = parentWorld[parentOffset + index] ?? 0;
|
|
314
478
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
expected: 'a parent-before-child Transform hierarchy',
|
|
319
|
-
hint: 'repair the ChildOf cycle and retry TransformPropagation',
|
|
320
|
-
detail: { entity: entry.entity, parent: cycleParent },
|
|
321
|
-
}),
|
|
322
|
-
);
|
|
479
|
+
// Column-major composition is intentionally identical to the previous
|
|
480
|
+
// matrix path: Global = Parent * Local, with no TRS decomposition.
|
|
481
|
+
mat4.multiply(scratch.candidate, scratch.parent, scratch.local);
|
|
323
482
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
483
|
+
writeCandidate(binding, row, scratch.candidate, changed);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function ensureHierarchyBuffers(scratch: Scratch, bindings: readonly HierarchyBinding[]): void {
|
|
487
|
+
let same = scratch.hierarchyBindingTables.length === bindings.length;
|
|
488
|
+
if (same) {
|
|
489
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
490
|
+
const binding = bindings[index];
|
|
491
|
+
if (
|
|
492
|
+
binding === undefined ||
|
|
493
|
+
scratch.hierarchyBindingTables[index] !== binding.tableId ||
|
|
494
|
+
scratch.hierarchyBindingRows[index] !== binding.rowCapacity
|
|
495
|
+
) {
|
|
496
|
+
same = false;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (same) return;
|
|
502
|
+
scratch.hierarchyBindingTables = bindings.map((binding) => binding.tableId);
|
|
503
|
+
scratch.hierarchyBindingRows = bindings.map((binding) => binding.rowCapacity);
|
|
504
|
+
scratch.hierarchyStates = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
|
|
505
|
+
scratch.hierarchyChanged = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function resetHierarchyBuffers(scratch: Scratch): void {
|
|
509
|
+
for (let index = 0; index < scratch.hierarchyStates.length; index += 1) {
|
|
510
|
+
scratch.hierarchyStates[index]?.fill(0);
|
|
511
|
+
scratch.hierarchyChanged[index]?.fill(0);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function ensureFlatBuffers(scratch: Scratch, bindings: readonly TransformBinding[]): void {
|
|
516
|
+
let same = scratch.flatBindingTables.length === bindings.length;
|
|
517
|
+
if (same) {
|
|
518
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
519
|
+
const binding = bindings[index];
|
|
520
|
+
if (
|
|
521
|
+
binding === undefined ||
|
|
522
|
+
scratch.flatBindingTables[index] !== binding.tableId ||
|
|
523
|
+
scratch.flatBindingRows[index] !== binding.rowCapacity
|
|
524
|
+
) {
|
|
525
|
+
same = false;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (same) return;
|
|
531
|
+
scratch.flatBindingTables = bindings.map((binding) => binding.tableId);
|
|
532
|
+
scratch.flatBindingRows = bindings.map((binding) => binding.rowCapacity);
|
|
533
|
+
scratch.flatChanged = bindings.map((binding) => new Uint8Array(binding.rowCapacity));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function resetFlatBuffers(scratch: Scratch): void {
|
|
537
|
+
for (const changed of scratch.flatChanged) changed.fill(0);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function derivedWriteError(cause: EcsError, bindingIndex: number): SceneError {
|
|
541
|
+
return new SceneError({
|
|
542
|
+
code: 'hierarchy-broken',
|
|
543
|
+
expected: 'derived GlobalTransform range publication to succeed',
|
|
544
|
+
hint: cause.hint ?? 'retry propagation on a healthy World',
|
|
545
|
+
detail: {
|
|
546
|
+
kind: 'derived-write',
|
|
547
|
+
entity: 0 as EntityHandle,
|
|
548
|
+
parent: 0 as EntityHandle,
|
|
549
|
+
bindingIndex,
|
|
550
|
+
base: 0,
|
|
551
|
+
start: 0,
|
|
552
|
+
count: 0,
|
|
553
|
+
cause,
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function findHierarchyLocation(
|
|
559
|
+
writer: HierarchyWriter,
|
|
560
|
+
bindings: readonly HierarchyBinding[],
|
|
561
|
+
entity: EntityHandle,
|
|
562
|
+
cursor: DerivedRangeCursor,
|
|
563
|
+
): HierarchyBinding | undefined {
|
|
564
|
+
countPropagation('hierarchyEntityLookups');
|
|
565
|
+
if (!writer.locateEntity(entity, cursor)) return undefined;
|
|
566
|
+
return bindings[cursor.bindingIndex];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function findTransformLocation(
|
|
570
|
+
writer: TransformWriter,
|
|
571
|
+
bindings: readonly TransformBinding[],
|
|
572
|
+
entity: EntityHandle,
|
|
573
|
+
cursor: DerivedRangeCursor,
|
|
574
|
+
): TransformBinding | undefined {
|
|
575
|
+
countPropagation('hierarchyEntityLookups');
|
|
576
|
+
if (!writer.locateEntity(entity, cursor)) return undefined;
|
|
577
|
+
return bindings[cursor.bindingIndex];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function countPublishedRows(changed: Uint8Array): void {
|
|
581
|
+
const trace = propagationTrace;
|
|
582
|
+
if (trace === undefined) return;
|
|
583
|
+
let runOpen = false;
|
|
584
|
+
for (let row = 0; row < changed.length; row += 1) {
|
|
585
|
+
if ((changed[row] ?? 0) !== 0) {
|
|
586
|
+
trace.hierarchyPublishedRows += 1;
|
|
587
|
+
if (!runOpen) {
|
|
588
|
+
trace.hierarchyPublishedRuns += 1;
|
|
589
|
+
runOpen = true;
|
|
590
|
+
}
|
|
344
591
|
} else {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
592
|
+
runOpen = false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function noteHierarchyError(current: SceneError | undefined, next: SceneError): SceneError {
|
|
598
|
+
if (current === undefined) return next;
|
|
599
|
+
const currentEntity = Number(current.detail?.entity ?? Number.MAX_SAFE_INTEGER);
|
|
600
|
+
const nextEntity = Number(next.detail?.entity ?? Number.MAX_SAFE_INTEGER);
|
|
601
|
+
return nextEntity < currentEntity ||
|
|
602
|
+
(nextEntity === currentEntity && next.code.localeCompare(current.code) < 0)
|
|
603
|
+
? next
|
|
604
|
+
: current;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function composeLocalHierarchyEntity(
|
|
608
|
+
writer: HierarchyWriter,
|
|
609
|
+
bindings: readonly HierarchyBinding[],
|
|
610
|
+
entity: EntityHandle,
|
|
611
|
+
scratch: Scratch,
|
|
612
|
+
changed: Uint8Array[],
|
|
613
|
+
): boolean {
|
|
614
|
+
const cursor = scratch.hierarchyCurrentCursor;
|
|
615
|
+
const binding = findHierarchyLocation(writer, bindings, entity, cursor);
|
|
616
|
+
if (binding === undefined) return false;
|
|
617
|
+
composeBindingRow(
|
|
618
|
+
binding,
|
|
619
|
+
cursor.row,
|
|
620
|
+
undefined,
|
|
621
|
+
0,
|
|
622
|
+
scratch,
|
|
623
|
+
changed[cursor.bindingIndex] as Uint8Array,
|
|
624
|
+
);
|
|
625
|
+
const states = scratch.hierarchyStates[cursor.bindingIndex];
|
|
626
|
+
if (states !== undefined) states[cursor.row] = 3;
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function handleActiveCycle(
|
|
631
|
+
repeated: EntityHandle,
|
|
632
|
+
stackEntities: EntityHandle[],
|
|
633
|
+
stackChildren: number[],
|
|
634
|
+
writer: HierarchyWriter,
|
|
635
|
+
bindings: readonly HierarchyBinding[],
|
|
636
|
+
scratch: Scratch,
|
|
637
|
+
changed: Uint8Array[],
|
|
638
|
+
report: (error: SceneError) => void,
|
|
639
|
+
): void {
|
|
640
|
+
let cycleStart = -1;
|
|
641
|
+
for (let index = 0; index < stackEntities.length; index += 1) {
|
|
642
|
+
if (stackEntities[index] === repeated) {
|
|
643
|
+
cycleStart = index;
|
|
644
|
+
break;
|
|
356
645
|
}
|
|
357
646
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
647
|
+
if (cycleStart < 0) return;
|
|
648
|
+
const repeatedCursor: DerivedRangeCursor = { bindingIndex: -1, row: -1 };
|
|
649
|
+
const repeatedBinding = findHierarchyLocation(writer, bindings, repeated, repeatedCursor);
|
|
650
|
+
const repeatedParent =
|
|
651
|
+
repeatedBinding === undefined
|
|
652
|
+
? repeated
|
|
653
|
+
: ((hierarchyColumns(repeatedBinding).parent[repeatedCursor.row] ??
|
|
654
|
+
ENTITY_NULL_RAW) as number as EntityHandle);
|
|
655
|
+
report(
|
|
656
|
+
hierarchyError(
|
|
657
|
+
'hierarchy-cycle',
|
|
658
|
+
repeated,
|
|
659
|
+
repeatedParent,
|
|
660
|
+
'a parent-before-child Transform hierarchy',
|
|
661
|
+
'repair the ChildOf cycle and retry TransformPropagation',
|
|
662
|
+
),
|
|
663
|
+
);
|
|
664
|
+
for (let index = cycleStart; index < stackEntities.length; index += 1) {
|
|
665
|
+
const cycleEntity = stackEntities[index];
|
|
666
|
+
if (cycleEntity === undefined) continue;
|
|
667
|
+
composeLocalHierarchyEntity(writer, bindings, cycleEntity, scratch, changed);
|
|
668
|
+
stackChildren[index] = 0;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function walkChildren(
|
|
673
|
+
world: World,
|
|
674
|
+
root: EntityHandle,
|
|
675
|
+
writer: HierarchyWriter,
|
|
676
|
+
bindings: readonly HierarchyBinding[],
|
|
677
|
+
transformWriter: TransformWriter,
|
|
678
|
+
transformBindings: readonly TransformBinding[],
|
|
679
|
+
scratch: Scratch,
|
|
680
|
+
report: (error: SceneError) => void,
|
|
681
|
+
allowCompletedRoot: boolean,
|
|
682
|
+
): void {
|
|
683
|
+
countPropagation('hierarchyRootInvocations');
|
|
684
|
+
const stackEntities = scratch.hierarchyStackEntities;
|
|
685
|
+
const stackChildren = scratch.hierarchyStackChildren;
|
|
686
|
+
stackEntities.length = 0;
|
|
687
|
+
stackChildren.length = 0;
|
|
688
|
+
|
|
689
|
+
if (allowCompletedRoot) {
|
|
690
|
+
// Residual recovery may be entered once for each member of a malformed
|
|
691
|
+
// parent path. State 4 means a previous fallback walk already expanded
|
|
692
|
+
// this root and all reachable Children edges, so do not repeat that
|
|
693
|
+
// subtree for every cycle member.
|
|
694
|
+
const rootCursor = scratch.hierarchyRootCursor;
|
|
695
|
+
const rootBinding = findHierarchyLocation(writer, bindings, root, rootCursor);
|
|
696
|
+
if (rootBinding !== undefined) {
|
|
697
|
+
const state = scratch.hierarchyStates[rootCursor.bindingIndex]?.[rootCursor.row] ?? 0;
|
|
698
|
+
if (state === 4) return;
|
|
699
|
+
}
|
|
371
700
|
} else {
|
|
372
|
-
|
|
701
|
+
// This cursor is scratch-owned and reused for every root. A root walk is
|
|
702
|
+
// a hot invocation; allocating a cursor here would scale object churn with
|
|
703
|
+
// the number of roots even after all bindings have warmed.
|
|
704
|
+
const rootCursor = scratch.hierarchyRootCursor;
|
|
705
|
+
countPropagation('hierarchyRootCursorReuses');
|
|
706
|
+
const rootBinding = findHierarchyLocation(writer, bindings, root, rootCursor);
|
|
707
|
+
if (rootBinding !== undefined) {
|
|
708
|
+
const state = scratch.hierarchyStates[rootCursor.bindingIndex]?.[rootCursor.row] ?? 0;
|
|
709
|
+
if (state !== 0) return;
|
|
710
|
+
}
|
|
373
711
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
712
|
+
stackEntities.push(root);
|
|
713
|
+
stackChildren.push(-1);
|
|
714
|
+
|
|
715
|
+
const currentCursor = scratch.hierarchyCurrentCursor;
|
|
716
|
+
const parentCursor = scratch.hierarchyParentCursor;
|
|
717
|
+
const childCursor = scratch.hierarchyChildCursor;
|
|
718
|
+
while (stackEntities.length > 0) {
|
|
719
|
+
const top = stackEntities.length - 1;
|
|
720
|
+
const current = stackEntities[top];
|
|
721
|
+
if (current === undefined) {
|
|
722
|
+
stackEntities.pop();
|
|
723
|
+
stackChildren.pop();
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const hierarchyBinding = findHierarchyLocation(writer, bindings, current, currentCursor);
|
|
727
|
+
const nextChild = stackChildren[top] ?? -1;
|
|
728
|
+
if (nextChild < 0) {
|
|
729
|
+
if (hierarchyBinding !== undefined) {
|
|
730
|
+
const state = scratch.hierarchyStates[currentCursor.bindingIndex]?.[currentCursor.row] ?? 0;
|
|
731
|
+
if (state === 0) {
|
|
732
|
+
const states = scratch.hierarchyStates[currentCursor.bindingIndex];
|
|
733
|
+
if (states !== undefined) states[currentCursor.row] = 1;
|
|
734
|
+
const parentRaw = (hierarchyColumns(hierarchyBinding).parent[currentCursor.row] ??
|
|
735
|
+
ENTITY_NULL_RAW) as number;
|
|
736
|
+
let completed = false;
|
|
737
|
+
if (parentRaw === ENTITY_NULL_RAW) {
|
|
738
|
+
composeBindingRow(
|
|
739
|
+
hierarchyBinding,
|
|
740
|
+
currentCursor.row,
|
|
741
|
+
undefined,
|
|
742
|
+
0,
|
|
743
|
+
scratch,
|
|
744
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
745
|
+
);
|
|
746
|
+
completed = true;
|
|
747
|
+
} else {
|
|
748
|
+
const parent = parentRaw as EntityHandle;
|
|
749
|
+
const parentBinding = findTransformLocation(
|
|
750
|
+
transformWriter,
|
|
751
|
+
transformBindings,
|
|
752
|
+
parent,
|
|
753
|
+
parentCursor,
|
|
754
|
+
);
|
|
755
|
+
const parentHierarchy = findHierarchyLocation(writer, bindings, parent, childCursor);
|
|
756
|
+
if (parentBinding === undefined) {
|
|
757
|
+
report(
|
|
758
|
+
hierarchyError(
|
|
759
|
+
'hierarchy-broken',
|
|
760
|
+
current,
|
|
761
|
+
parent,
|
|
762
|
+
'each ChildOf parent to carry Transform and GlobalTransform',
|
|
763
|
+
'repair the missing parent pair before retrying propagation',
|
|
764
|
+
),
|
|
765
|
+
);
|
|
766
|
+
composeBindingRow(
|
|
767
|
+
hierarchyBinding,
|
|
768
|
+
currentCursor.row,
|
|
769
|
+
undefined,
|
|
770
|
+
0,
|
|
771
|
+
scratch,
|
|
772
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
773
|
+
);
|
|
774
|
+
completed = true;
|
|
775
|
+
} else if (parentHierarchy !== undefined) {
|
|
776
|
+
const parentState =
|
|
777
|
+
scratch.hierarchyStates[childCursor.bindingIndex]?.[childCursor.row] ?? 0;
|
|
778
|
+
if (parentState === 1) {
|
|
779
|
+
handleActiveCycle(
|
|
780
|
+
parent,
|
|
781
|
+
stackEntities,
|
|
782
|
+
stackChildren,
|
|
783
|
+
writer,
|
|
784
|
+
bindings,
|
|
785
|
+
scratch,
|
|
786
|
+
scratch.hierarchyChanged,
|
|
787
|
+
report,
|
|
788
|
+
);
|
|
789
|
+
completed = true;
|
|
790
|
+
} else if (parentState === 0) {
|
|
791
|
+
report(
|
|
792
|
+
hierarchyError(
|
|
793
|
+
'hierarchy-broken',
|
|
794
|
+
current,
|
|
795
|
+
parent,
|
|
796
|
+
'Children to enumerate every parent-before-child edge',
|
|
797
|
+
'repair the Children mirror and retry TransformPropagation',
|
|
798
|
+
),
|
|
799
|
+
);
|
|
800
|
+
composeBindingRow(
|
|
801
|
+
hierarchyBinding,
|
|
802
|
+
currentCursor.row,
|
|
803
|
+
undefined,
|
|
804
|
+
0,
|
|
805
|
+
scratch,
|
|
806
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
807
|
+
);
|
|
808
|
+
completed = true;
|
|
809
|
+
} else {
|
|
810
|
+
composeBindingRow(
|
|
811
|
+
hierarchyBinding,
|
|
812
|
+
currentCursor.row,
|
|
813
|
+
parentBinding,
|
|
814
|
+
parentCursor.row,
|
|
815
|
+
scratch,
|
|
816
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
817
|
+
);
|
|
818
|
+
completed = true;
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
composeBindingRow(
|
|
822
|
+
hierarchyBinding,
|
|
823
|
+
currentCursor.row,
|
|
824
|
+
parentBinding,
|
|
825
|
+
parentCursor.row,
|
|
826
|
+
scratch,
|
|
827
|
+
scratch.hierarchyChanged[currentCursor.bindingIndex] as Uint8Array,
|
|
828
|
+
);
|
|
829
|
+
completed = true;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (completed && states !== undefined && states[currentCursor.row] === 1) {
|
|
833
|
+
states[currentCursor.row] = 2;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
stackChildren[top] = 0;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const childrenLength = world[worldInternal].getArrayLength(current, Children, 'entities') ?? 0;
|
|
842
|
+
if (nextChild >= childrenLength) {
|
|
843
|
+
stackEntities.pop();
|
|
844
|
+
stackChildren.pop();
|
|
845
|
+
if (allowCompletedRoot) {
|
|
846
|
+
const completedCursor = scratch.hierarchyResidualCursor;
|
|
847
|
+
const completedBinding = findHierarchyLocation(writer, bindings, current, completedCursor);
|
|
848
|
+
const completedStates =
|
|
849
|
+
completedBinding === undefined
|
|
850
|
+
? undefined
|
|
851
|
+
: scratch.hierarchyStates[completedCursor.bindingIndex];
|
|
852
|
+
if (completedStates !== undefined && completedStates[completedCursor.row] === 3) {
|
|
853
|
+
completedStates[completedCursor.row] = 4;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
stackChildren[top] = nextChild + 1;
|
|
859
|
+
countPropagation('hierarchyEdgesVisited');
|
|
860
|
+
const childRaw = world[worldInternal].getArrayElement(current, Children, 'entities', nextChild);
|
|
861
|
+
if (childRaw === undefined || childRaw === ENTITY_NULL_RAW) {
|
|
862
|
+
report(
|
|
863
|
+
hierarchyError(
|
|
864
|
+
'hierarchy-broken',
|
|
865
|
+
current,
|
|
866
|
+
current,
|
|
867
|
+
'Children.entities to contain live child handles',
|
|
868
|
+
'repair the Children mirror and retry TransformPropagation',
|
|
869
|
+
),
|
|
870
|
+
);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const child = childRaw as EntityHandle;
|
|
874
|
+
const childBinding = findHierarchyLocation(writer, bindings, child, childCursor);
|
|
875
|
+
if (childBinding === undefined) {
|
|
876
|
+
report(
|
|
877
|
+
hierarchyError(
|
|
878
|
+
'hierarchy-broken',
|
|
879
|
+
child,
|
|
880
|
+
current,
|
|
881
|
+
'each Children entry to carry Transform, GlobalTransform, and ChildOf',
|
|
882
|
+
'repair the child component pair before retrying propagation',
|
|
883
|
+
),
|
|
884
|
+
);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const childParent = (hierarchyColumns(childBinding).parent[childCursor.row] ??
|
|
888
|
+
ENTITY_NULL_RAW) as number;
|
|
889
|
+
const childState = scratch.hierarchyStates[childCursor.bindingIndex]?.[childCursor.row] ?? 0;
|
|
890
|
+
if (childParent !== (current as number)) {
|
|
891
|
+
// Leave an unvisited child for the residual row-state pass. That pass
|
|
892
|
+
// can classify the complete path (including a rootless cycle) without
|
|
893
|
+
// emitting a premature mirror error that would hide the cycle cause.
|
|
894
|
+
if (childState === 0) continue;
|
|
895
|
+
report(
|
|
896
|
+
hierarchyError(
|
|
897
|
+
'hierarchy-broken',
|
|
898
|
+
child,
|
|
899
|
+
current,
|
|
900
|
+
'Children and ChildOf to describe the same parent',
|
|
901
|
+
'repair the relationship mirror and retry TransformPropagation',
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (childState === 1) {
|
|
907
|
+
handleActiveCycle(
|
|
908
|
+
child,
|
|
909
|
+
stackEntities,
|
|
910
|
+
stackChildren,
|
|
911
|
+
writer,
|
|
912
|
+
bindings,
|
|
913
|
+
scratch,
|
|
914
|
+
scratch.hierarchyChanged,
|
|
915
|
+
report,
|
|
916
|
+
);
|
|
917
|
+
} else if (childState === 0) {
|
|
918
|
+
stackEntities.push(child);
|
|
919
|
+
stackChildren.push(-1);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function fallbackResidualPath(
|
|
925
|
+
world: World,
|
|
926
|
+
path: readonly EntityHandle[],
|
|
927
|
+
writer: HierarchyWriter,
|
|
928
|
+
bindings: readonly HierarchyBinding[],
|
|
929
|
+
transformWriter: TransformWriter,
|
|
930
|
+
transformBindings: readonly TransformBinding[],
|
|
931
|
+
scratch: Scratch,
|
|
932
|
+
report: (error: SceneError) => void,
|
|
933
|
+
): void {
|
|
934
|
+
// Root traversal owns the normal path. A residual path is necessarily a
|
|
935
|
+
// malformed Children projection (or a rootless cycle); cut the complete
|
|
936
|
+
// path to local roots in one pass so no stale GlobalTransform survives the
|
|
937
|
+
// diagnostic. Keeping the path as an explicit work list also makes this
|
|
938
|
+
// recovery O(path length), rather than repeatedly searching parent chains.
|
|
939
|
+
for (const entity of path) {
|
|
940
|
+
composeLocalHierarchyEntity(writer, bindings, entity, scratch, scratch.hierarchyChanged);
|
|
941
|
+
}
|
|
942
|
+
for (const entity of path) {
|
|
943
|
+
walkChildren(
|
|
944
|
+
world,
|
|
945
|
+
entity,
|
|
946
|
+
writer,
|
|
947
|
+
bindings,
|
|
948
|
+
transformWriter,
|
|
949
|
+
transformBindings,
|
|
950
|
+
scratch,
|
|
951
|
+
report,
|
|
952
|
+
true,
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function resolveResidualPath(
|
|
958
|
+
world: World,
|
|
959
|
+
entity: EntityHandle,
|
|
960
|
+
writer: HierarchyWriter,
|
|
961
|
+
bindings: readonly HierarchyBinding[],
|
|
962
|
+
transformWriter: TransformWriter,
|
|
963
|
+
transformBindings: readonly TransformBinding[],
|
|
964
|
+
scratch: Scratch,
|
|
965
|
+
report: (error: SceneError) => void,
|
|
966
|
+
): void {
|
|
967
|
+
const path = scratch.hierarchyProbeEntities;
|
|
968
|
+
path.length = 0;
|
|
969
|
+
const cursor = scratch.hierarchyResidualCursor;
|
|
970
|
+
const parentTransformCursor = scratch.hierarchyParentCursor;
|
|
971
|
+
const parentHierarchyCursor = scratch.hierarchyResidualParentCursor;
|
|
972
|
+
let current = entity;
|
|
973
|
+
while (true) {
|
|
974
|
+
countPropagation('hierarchyResidualParentProbes');
|
|
975
|
+
const binding = findHierarchyLocation(writer, bindings, current, cursor);
|
|
976
|
+
if (binding === undefined) break;
|
|
977
|
+
const states = scratch.hierarchyStates[cursor.bindingIndex];
|
|
978
|
+
const state = states?.[cursor.row] ?? 0;
|
|
979
|
+
if (state !== 0) {
|
|
980
|
+
const columns = hierarchyColumns(binding);
|
|
981
|
+
const parentRaw = (columns.parent[cursor.row] ?? ENTITY_NULL_RAW) as number;
|
|
982
|
+
report(
|
|
983
|
+
hierarchyError(
|
|
984
|
+
'hierarchy-cycle',
|
|
985
|
+
current,
|
|
986
|
+
parentRaw === ENTITY_NULL_RAW ? current : (parentRaw as EntityHandle),
|
|
987
|
+
'a parent-before-child Transform hierarchy',
|
|
988
|
+
'repair the ChildOf cycle and retry TransformPropagation',
|
|
989
|
+
),
|
|
990
|
+
);
|
|
991
|
+
fallbackResidualPath(
|
|
992
|
+
world,
|
|
993
|
+
path,
|
|
994
|
+
writer,
|
|
995
|
+
bindings,
|
|
996
|
+
transformWriter,
|
|
997
|
+
transformBindings,
|
|
998
|
+
scratch,
|
|
999
|
+
report,
|
|
1000
|
+
);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (states !== undefined) states[cursor.row] = 1;
|
|
1005
|
+
path.push(current);
|
|
1006
|
+
const parentRaw = (hierarchyColumns(binding).parent[cursor.row] ?? ENTITY_NULL_RAW) as number;
|
|
1007
|
+
if (parentRaw === ENTITY_NULL_RAW) {
|
|
1008
|
+
// Every null-parent row is enumerated as a root above. Reaching one
|
|
1009
|
+
// here means the materialized Children graph failed to expose the
|
|
1010
|
+
// parent-first edge; preserve the explicit malformed-graph error.
|
|
1011
|
+
report(
|
|
1012
|
+
hierarchyError(
|
|
1013
|
+
'hierarchy-broken',
|
|
1014
|
+
current,
|
|
1015
|
+
current,
|
|
1016
|
+
'Children to enumerate every parent-before-child edge',
|
|
1017
|
+
'repair the Children mirror and retry TransformPropagation',
|
|
1018
|
+
),
|
|
1019
|
+
);
|
|
1020
|
+
fallbackResidualPath(
|
|
1021
|
+
world,
|
|
1022
|
+
path,
|
|
1023
|
+
writer,
|
|
1024
|
+
bindings,
|
|
1025
|
+
transformWriter,
|
|
1026
|
+
transformBindings,
|
|
1027
|
+
scratch,
|
|
1028
|
+
report,
|
|
1029
|
+
);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const parent = parentRaw as EntityHandle;
|
|
1034
|
+
const parentTransform = findTransformLocation(
|
|
1035
|
+
transformWriter,
|
|
1036
|
+
transformBindings,
|
|
1037
|
+
parent,
|
|
1038
|
+
parentTransformCursor,
|
|
1039
|
+
);
|
|
1040
|
+
const parentHierarchy = findHierarchyLocation(writer, bindings, parent, parentHierarchyCursor);
|
|
1041
|
+
if (parentHierarchy === undefined) {
|
|
1042
|
+
report(
|
|
1043
|
+
hierarchyError(
|
|
1044
|
+
'hierarchy-broken',
|
|
1045
|
+
current,
|
|
1046
|
+
parent,
|
|
1047
|
+
parentTransform === undefined
|
|
1048
|
+
? 'each ChildOf parent to carry Transform and GlobalTransform'
|
|
1049
|
+
: 'Children to enumerate every parent-before-child edge',
|
|
1050
|
+
parentTransform === undefined
|
|
1051
|
+
? 'repair the missing parent pair before retrying propagation'
|
|
1052
|
+
: 'repair the Children mirror and retry TransformPropagation',
|
|
1053
|
+
),
|
|
1054
|
+
);
|
|
1055
|
+
fallbackResidualPath(
|
|
1056
|
+
world,
|
|
1057
|
+
path,
|
|
1058
|
+
writer,
|
|
1059
|
+
bindings,
|
|
1060
|
+
transformWriter,
|
|
1061
|
+
transformBindings,
|
|
1062
|
+
scratch,
|
|
1063
|
+
report,
|
|
1064
|
+
);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const parentState =
|
|
1069
|
+
scratch.hierarchyStates[parentHierarchyCursor.bindingIndex]?.[parentHierarchyCursor.row] ?? 0;
|
|
1070
|
+
if (parentState === 0) {
|
|
1071
|
+
current = parent;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (parentState === 1) {
|
|
1075
|
+
report(
|
|
1076
|
+
hierarchyError(
|
|
1077
|
+
'hierarchy-cycle',
|
|
1078
|
+
parent,
|
|
1079
|
+
current,
|
|
1080
|
+
'a parent-before-child Transform hierarchy',
|
|
1081
|
+
'repair the ChildOf cycle and retry TransformPropagation',
|
|
1082
|
+
),
|
|
1083
|
+
);
|
|
1084
|
+
} else {
|
|
1085
|
+
report(
|
|
1086
|
+
hierarchyError(
|
|
1087
|
+
'hierarchy-broken',
|
|
1088
|
+
current,
|
|
1089
|
+
parent,
|
|
1090
|
+
'Children to enumerate every parent-before-child edge',
|
|
1091
|
+
'repair the Children mirror and retry TransformPropagation',
|
|
1092
|
+
),
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
fallbackResidualPath(
|
|
1096
|
+
world,
|
|
1097
|
+
path,
|
|
1098
|
+
writer,
|
|
1099
|
+
bindings,
|
|
1100
|
+
transformWriter,
|
|
1101
|
+
transformBindings,
|
|
1102
|
+
scratch,
|
|
1103
|
+
report,
|
|
1104
|
+
);
|
|
1105
|
+
return;
|
|
378
1106
|
}
|
|
379
|
-
entry.state = 2;
|
|
380
|
-
scratch.hierarchyPath.pop();
|
|
381
|
-
return failure === undefined ? ok(undefined) : err(failure);
|
|
382
1107
|
}
|
|
383
1108
|
|
|
384
1109
|
function propagateHierarchy(world: World, scratch: Scratch): Result<void, SceneError> {
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
1110
|
+
const hierarchyWriter = scratch.hierarchyWriter;
|
|
1111
|
+
const transformWriter = scratch.transformWriter;
|
|
1112
|
+
if (hierarchyWriter === undefined || transformWriter === undefined) {
|
|
388
1113
|
return err(
|
|
389
1114
|
new SceneError({
|
|
390
1115
|
code: 'hierarchy-broken',
|
|
391
|
-
expected: 'paired Transform and GlobalTransform
|
|
1116
|
+
expected: 'paired Transform and GlobalTransform derived bindings',
|
|
392
1117
|
hint: 'register the scene components before running TransformPropagation',
|
|
393
1118
|
}),
|
|
394
1119
|
);
|
|
395
1120
|
}
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
scratch.hierarchyPath.length = 0;
|
|
1121
|
+
const hierarchyBindings = hierarchyWriter.bindings as readonly HierarchyBinding[];
|
|
1122
|
+
const transformBindings = transformWriter.bindings as readonly TransformBinding[];
|
|
1123
|
+
if (hierarchyBindings.length === 0) {
|
|
1124
|
+
// Keep the flat-only lane independent: it must not allocate hierarchy
|
|
1125
|
+
// markers or scan every Transform row merely to prove that no ChildOf
|
|
1126
|
+
// archetype exists.
|
|
1127
|
+
scratch.hierarchyBindingTables.length = 0;
|
|
1128
|
+
scratch.hierarchyBindingRows.length = 0;
|
|
1129
|
+
scratch.hierarchyStates.length = 0;
|
|
1130
|
+
scratch.hierarchyChanged.length = 0;
|
|
1131
|
+
return ok(undefined);
|
|
1132
|
+
}
|
|
1133
|
+
ensureHierarchyBuffers(scratch, hierarchyBindings);
|
|
1134
|
+
resetHierarchyBuffers(scratch);
|
|
413
1135
|
let firstError: SceneError | undefined;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1136
|
+
const report = (error: SceneError): void => {
|
|
1137
|
+
firstError = noteHierarchyError(firstError, error);
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
// Start only at actual roots (flat Transform rows and null-parent
|
|
1141
|
+
// ChildOf rows). Descendants are discovered exclusively through the ECS
|
|
1142
|
+
// materialized Children lists, so every normal frame is parent-first and
|
|
1143
|
+
// linear in rows plus relationship edges.
|
|
1144
|
+
for (const binding of transformBindings) {
|
|
1145
|
+
const entities = binding.entities;
|
|
1146
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
1147
|
+
const entity = (entities[row] ?? 0) as EntityHandle;
|
|
1148
|
+
const parentRaw = world[worldInternal].getFieldValue(entity, ChildOf, 'parent');
|
|
1149
|
+
if (parentRaw === undefined || parentRaw === ENTITY_NULL_RAW) {
|
|
1150
|
+
walkChildren(
|
|
1151
|
+
world,
|
|
1152
|
+
entity,
|
|
1153
|
+
hierarchyWriter,
|
|
1154
|
+
hierarchyBindings,
|
|
1155
|
+
transformWriter,
|
|
1156
|
+
transformBindings,
|
|
1157
|
+
scratch,
|
|
1158
|
+
report,
|
|
1159
|
+
false,
|
|
1160
|
+
);
|
|
428
1161
|
}
|
|
429
1162
|
}
|
|
430
1163
|
}
|
|
1164
|
+
|
|
1165
|
+
// Any remaining hierarchy row is either behind a malformed/missing mirror
|
|
1166
|
+
// edge or belongs to a rootless cycle. Follow each residual parent chain
|
|
1167
|
+
// once with the same row-state machine (not a per-node parent search), then
|
|
1168
|
+
// cut that complete residual path to local space. This keeps malformed
|
|
1169
|
+
// coverage linear in residual rows and edges while normal frames remain
|
|
1170
|
+
// exclusively Children-driven.
|
|
1171
|
+
for (let bindingIndex = 0; bindingIndex < hierarchyBindings.length; bindingIndex += 1) {
|
|
1172
|
+
const binding = hierarchyBindings[bindingIndex];
|
|
1173
|
+
if (binding === undefined) continue;
|
|
1174
|
+
const entities = binding.entities;
|
|
1175
|
+
const states = scratch.hierarchyStates[bindingIndex];
|
|
1176
|
+
for (let row = 0; row < binding.rowCapacity; row += 1) {
|
|
1177
|
+
if ((states?.[row] ?? 0) !== 0) continue;
|
|
1178
|
+
const entity = (entities[row] ?? 0) as EntityHandle;
|
|
1179
|
+
resolveResidualPath(
|
|
1180
|
+
world,
|
|
1181
|
+
entity,
|
|
1182
|
+
hierarchyWriter,
|
|
1183
|
+
hierarchyBindings,
|
|
1184
|
+
transformWriter,
|
|
1185
|
+
transformBindings,
|
|
1186
|
+
scratch,
|
|
1187
|
+
report,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
for (let bindingIndex = 0; bindingIndex < hierarchyBindings.length; bindingIndex += 1) {
|
|
1193
|
+
const changed = scratch.hierarchyChanged[bindingIndex];
|
|
1194
|
+
if (changed === undefined) continue;
|
|
1195
|
+
countPublishedRows(changed);
|
|
1196
|
+
const published = hierarchyWriter.publishChangedRows(bindingIndex, changed);
|
|
1197
|
+
if (!published.ok) {
|
|
1198
|
+
const cause = published.error as EcsError;
|
|
1199
|
+
report(derivedWriteError(cause, bindingIndex));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
431
1202
|
return firstError === undefined ? ok(undefined) : err(firstError);
|
|
432
1203
|
}
|
|
433
1204
|
|
|
@@ -435,7 +1206,7 @@ export function propagateTransforms(world: World): Result<void, SceneError> {
|
|
|
435
1206
|
const scratch = scratchFor(world);
|
|
436
1207
|
const pairs = validateTransformPairs(world, scratch);
|
|
437
1208
|
if (!pairs.ok) return pairs;
|
|
438
|
-
const flat = propagateFlat(scratch);
|
|
1209
|
+
const flat = propagateFlat(world, scratch);
|
|
439
1210
|
if (!flat.ok) return flat;
|
|
440
1211
|
return propagateHierarchy(world, scratch);
|
|
441
1212
|
}
|