@forgeax/engine-render 0.1.24 → 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 +100 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/surface-projection.d.ts +24 -0
- package/dist/assembly/material/surface-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +20 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
- package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
- package/dist/assets/asset-decoders.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
- package/dist/chunk-ADHHVYLW.mjs.map +1 -0
- package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
- package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
- package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
- package/dist/chunk-HKXTW355.mjs.map +1 -0
- package/dist/{chunk-YKB5DQV7.mjs → chunk-JSEDTZYS.mjs} +87 -19
- package/dist/chunk-JSEDTZYS.mjs.map +1 -0
- package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
- package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
- package/dist/{chunk-Z3YYH2MB.mjs → chunk-N3RT2EUR.mjs} +94 -41
- package/dist/chunk-N3RT2EUR.mjs.map +1 -0
- package/dist/{chunk-YRUYGA4A.mjs → chunk-TCLDX7SZ.mjs} +559 -138
- package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
- package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
- package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
- package/dist/components/instances.d.ts +13 -31
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +865 -142
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +6 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +4 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +27 -9
- package/dist/index.mjs.map +1 -1
- package/dist/instance-buffer-cache.d.ts +7 -1
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +101 -0
- package/dist/instances.d.ts.map +1 -0
- package/dist/internal.mjs +5 -5
- package/dist/materials.d.ts +63 -2
- package/dist/materials.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +13 -2
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +22 -3
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -1
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +11 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +4 -0
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +4 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +25 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +4 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +2 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +12 -1
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/systems/skin-palette-allocator.d.ts +6 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/package.json +20 -20
- package/src/__tests__/gpu-scene.unit.test.ts +38 -2
- package/src/__tests__/instances-store.unit.test.ts +46 -0
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +5 -13
- package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
- package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
- package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
- package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
- package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
- package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
- package/src/assembly/factory.ts +2 -0
- package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
- package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
- package/src/assembly/material/surface-projection.ts +90 -0
- package/src/assembly/material-shader-policy.ts +88 -10
- package/src/assembly/recovery/renderer-recover.ts +1 -1
- package/src/assembly/webgpu-ready.ts +3 -16
- package/src/assembly/webgpu-renderer.ts +90 -158
- package/src/assembly/webgpu-vertex-layouts.ts +132 -0
- package/src/assets/asset-decoders.ts +27 -4
- package/src/components/instances.ts +14 -197
- package/src/gpu-driven/production-raster.ts +34 -1
- package/src/gpu-scene.ts +84 -29
- package/src/index.ts +1 -0
- package/src/instance-buffer-cache.ts +14 -1
- package/src/instances.ts +288 -0
- package/src/materials.ts +107 -12
- package/src/pipeline-builder.ts +14 -10
- package/src/pipeline-spec.ts +6 -14
- package/src/record/frame-snapshot.ts +19 -2
- package/src/record/frame.ts +31 -2
- package/src/record/main-pass-geometry.ts +498 -59
- package/src/record/main-pass-sprite-draws.ts +1 -1
- package/src/record/render-context.ts +4 -0
- package/src/record/shadow-pass.ts +46 -99
- package/src/recovery/render-system-candidate.ts +41 -4
- package/src/render-contract.ts +4 -0
- package/src/render-system-extract-tail.ts +107 -51
- package/src/render-system-extract.ts +53 -26
- package/src/render-system.ts +40 -5
- package/src/scene/render-scene-types.ts +2 -0
- package/src/scene/render-scene.ts +579 -36
- package/src/systems/skin-palette-allocator.ts +129 -7
- package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
- package/dist/chunk-KZFTK6DO.mjs.map +0 -1
- package/dist/chunk-Q752SHI5.mjs.map +0 -1
- package/dist/chunk-YKB5DQV7.mjs.map +0 -1
- package/dist/chunk-YRUYGA4A.mjs.map +0 -1
- package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
|
@@ -1,206 +1,23 @@
|
|
|
1
|
-
// @forgeax/engine-render - Instances component (per-entity instanced-draw transforms).
|
|
2
|
-
//
|
|
3
|
-
// Two sources, one component (feat-20260622-chunk-gpu-instancing-sprite-tilemap
|
|
4
|
-
// / AC-09; charter P4 consistent abstraction + charter P1 progressive disclosure).
|
|
5
|
-
// The `Instances` ECS component has exactly TWO sources of `transforms` data, and
|
|
6
|
-
// AI users should hold the boundary explicitly to avoid misreading the ECS view:
|
|
7
|
-
//
|
|
8
|
-
// Source 1 (EXPLICIT, the only source that lands in ECS) — AI user opt-in.
|
|
9
|
-
// The AI user hand-attaches `Instances { transforms }` onto an entity at
|
|
10
|
-
// spawn / set time for high-performance instanced draw. `world.get(e,
|
|
11
|
-
// Instances)` returns the AI user's payload verbatim (a `Float32Array`
|
|
12
|
-
// snapshot of column-major mat4 instances; stride = 16). This is the sole
|
|
13
|
-
// code path through which `Instances` ever appears on an entity.
|
|
14
|
-
//
|
|
15
|
-
// Source 2 (TRANSPARENT, never written back into ECS) — engine record-stage
|
|
16
|
-
// fold. When the AI user spawns N independent sprite / tilemap-cell
|
|
17
|
-
// entities sharing the same (Layer.value, posZ, materialHandle) sort
|
|
18
|
-
// equivalence class, the engine record stage transparently folds the
|
|
19
|
-
// equivalent run into ONE `drawIndexed(indexCount, count)` using an
|
|
20
|
-
// INTERNAL transient buffer (see `render-system-record.ts` fold operator
|
|
21
|
-
// + `render.instancing.foldedDraws` metric, and the `RhiError
|
|
22
|
-
// 'instancing-exceeds-uniform-cap'` fallback). The fold operator does
|
|
23
|
-
// NOT auto-inject `Instances` onto those entities — `world.get(e,
|
|
24
|
-
// Instances)` on a folded entity returns `Result.err` exactly as before.
|
|
25
|
-
// The ECS view is preserved bit-for-bit (per-cell / per-sprite entity
|
|
26
|
-
// retained, query / despawn / picking unchanged); the optimisation is
|
|
27
|
-
// invisible at the component layer (charter P4: consistent abstraction —
|
|
28
|
-
// the high-performance path is opt-in via Source 1, the default spawn
|
|
29
|
-
// shape stays untouched).
|
|
30
|
-
//
|
|
31
|
-
// AI user reading this header: do NOT expect engine-side fold to materialise
|
|
32
|
-
// as an `Instances` row on your entity. Read the metric (foldedDraws) or the
|
|
33
|
-
// RhiError code to observe fold behaviour; never gate logic on a runtime
|
|
34
|
-
// `world.get(e, Instances)` lookup after spawn.
|
|
35
|
-
//
|
|
36
|
-
// Schema: 1 array<f32> field `transforms` carrying packed column-major mat4
|
|
37
|
-
// instance transforms (16 f32 per instance, stride = 16). Stride is enforced
|
|
38
|
-
// through a TWO-LAYER contract:
|
|
39
|
-
//
|
|
40
|
-
// 1. AI user set-site: spawn / `world.set` / `world.push` callers pass a
|
|
41
|
-
// `Float32Array` (or numeric array) whose `length` is a non-zero
|
|
42
|
-
// multiple of 16. The set site is the AI user's responsibility -- the
|
|
43
|
-
// engine cannot prove the column-major mat4 packing without the
|
|
44
|
-
// caller's intent.
|
|
45
|
-
// 2. RenderSystem extract entry: `render-system-extract.ts` performs a
|
|
46
|
-
// defensive `transforms.length % 16 === 0` check on every
|
|
47
|
-
// `world.get(e, Instances).transforms` snapshot at frame extract time;
|
|
48
|
-
// violations route a structured `InstanceTransformsStrideMismatchError`
|
|
49
|
-
// (`code: 'instance-transforms-stride-mismatch'`,
|
|
50
|
-
// `detail: { actualLength, expectedStride: 16 }`) through the World
|
|
51
|
-
// Layer-3 ErrorHandler and the renderable is skipped (fail-fast: the
|
|
52
|
-
// malformed length never reaches the GPU upload path).
|
|
53
|
-
//
|
|
54
|
-
// feat-20260515-buffer-array-vocab-collapse M3 / w16 (decision §2.3 stride
|
|
55
|
-
// responsibility migration): the legacy component-level
|
|
56
|
-
// stride-declaration option (`{ transforms: 16 }` keyed on the retired
|
|
57
|
-
// per-component stride defineComponent option key) was retired because:
|
|
58
|
-
// - The ECS layer no longer carries a per-component stride schema (M2 /
|
|
59
|
-
// w9 dropped the option from `DefineComponentOptions` -- the SSOT
|
|
60
|
-
// moved to the RenderSystem entry + AI user set site).
|
|
61
|
-
// - Centralising the check at the consumer (RenderSystem extract) rather
|
|
62
|
-
// than the producer (ECS write paths) lets the GPU upload path stay
|
|
63
|
-
// trust-the-snapshot; AI users get one structured error per offending
|
|
64
|
-
// frame rather than one per write.
|
|
65
|
-
//
|
|
66
|
-
// feat-20260514-ecs-children-instances-managed-buffer-array M3 / w14:
|
|
67
|
-
// migrated from the legacy `{ buffer: 'ref', count: 'u32' }` pair (which
|
|
68
|
-
// cross-coupled with the now-deleted `AssetRegistry.createInstancedBuffer`
|
|
69
|
-
// pipeline + the `InstancedBufferAsset` POD) to the ECS-managed array path.
|
|
70
|
-
// Resize is a `world.push(e, Instances, 'transforms', value)` away (one f32
|
|
71
|
-
// at a time) plus a future `world.grow` if a bulk-resize affordance is
|
|
72
|
-
// added.
|
|
73
|
-
//
|
|
74
|
-
// Storage buffer cap-gate (D-5 OOS-08 follow-up):
|
|
75
|
-
// - The runtime RenderSystem consumer (`render-system-record.ts`) checks
|
|
76
|
-
// `device.caps.storageBuffer` before binding the per-entity transforms;
|
|
77
|
-
// `caps.storageBuffer === false` (backend lacking storage buffer support,
|
|
78
|
-
// e.g. rhi-wgpu webgl backend) routes a `RhiError` ('feature-not-enabled').
|
|
79
|
-
// A future loop owns the per-draw fallback path.
|
|
80
|
-
//
|
|
81
|
-
// Group transform chained semantics (charter proposition 5 mental migration):
|
|
82
|
-
// - When the same entity carries both `Transform` (entity_world) and
|
|
83
|
-
// `Instances` (per-instance local transforms), the vertex shader
|
|
84
|
-
// composes per instance:
|
|
85
|
-
//
|
|
86
|
-
// world_position[i] = entity_world * instances_local[i] * vertex_position
|
|
87
|
-
//
|
|
88
|
-
// - `Instances.transforms[i*16..i*16+15]` is interpreted as a local-space
|
|
89
|
-
// transform under the entity, exactly like a child entity's `Transform`
|
|
90
|
-
// under `ChildOf { parent }` in the hierarchy system. AI users reuse
|
|
91
|
-
// the `parent x local` intuition without a new concept. Set the
|
|
92
|
-
// entity's `Transform` to identity to make `instances_local[i]`
|
|
93
|
-
// directly world-space.
|
|
94
|
-
//
|
|
95
|
-
// 4-segment minimum contract (read this header before reaching for
|
|
96
|
-
// the source body):
|
|
97
|
-
//
|
|
98
|
-
// ===== (a) single-component import + spawn example =====
|
|
99
|
-
//
|
|
100
|
-
// import {
|
|
101
|
-
// createRenderer, HANDLE_CUBE,
|
|
102
|
-
// MeshFilter, MeshRenderer, Transform,
|
|
103
|
-
// Instances, type InstancesData,
|
|
104
|
-
// } from '@forgeax/engine-render';
|
|
105
|
-
//
|
|
106
|
-
// // 1 entity rendering N instanced cubes (16N packed f32, column-major mat4):
|
|
107
|
-
// const transforms = new Float32Array(N * 16);
|
|
108
|
-
// // ... fill column-major mat4 columns per instance ...
|
|
109
|
-
// world.spawn(
|
|
110
|
-
// { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
111
|
-
// { component: MeshRenderer, data: {} },
|
|
112
|
-
// { component: Instances, data: { transforms } },
|
|
113
|
-
// );
|
|
114
|
-
//
|
|
115
|
-
// ===== (b) packed mat4 layout =====
|
|
116
|
-
//
|
|
117
|
-
// const transforms = new Float32Array(N * 16); // column-major mat4 per instance
|
|
118
|
-
// // instance i occupies floats [i*16 .. i*16+15]:
|
|
119
|
-
// // [m00 m10 m20 m30 <- column 0
|
|
120
|
-
// // m01 m11 m21 m31 <- column 1
|
|
121
|
-
// // m02 m12 m22 m32 <- column 2
|
|
122
|
-
// // m03 m13 m23 m33] <- column 3 (translation in m03/m13/m23)
|
|
123
|
-
//
|
|
124
|
-
// `transforms.length` MUST be a non-zero multiple of 16. AI users gate at
|
|
125
|
-
// the set / push site; the RenderSystem extract entry holds the second
|
|
126
|
-
// defensive (AC-06: detail carries `{ expectedStride: 16, actualLength }`).
|
|
127
|
-
// The instance count is the live snapshot length / 16.
|
|
128
|
-
//
|
|
129
|
-
// ===== (c) error code consumption (typed property access, no message regex) =====
|
|
130
|
-
//
|
|
131
|
-
// // Stride violation surfaces through the engine-ecs Layer-3 ErrorHandler
|
|
132
|
-
// // when extract reads a malformed snapshot:
|
|
133
|
-
// // on('error', (err) => {
|
|
134
|
-
// // if (err.code === 'instance-transforms-stride-mismatch') {
|
|
135
|
-
// // // err.detail.expectedStride === 16
|
|
136
|
-
// // // err.detail.actualLength === <user-supplied length>
|
|
137
|
-
// // }
|
|
138
|
-
// // });
|
|
139
|
-
// //
|
|
140
|
-
// // RenderSystem cap-gate (D-5) -> RhiError 'feature-not-enabled' on the
|
|
141
|
-
// // RhiErrorListenerRegistry channel:
|
|
142
|
-
// // on('error', (err) => {
|
|
143
|
-
// // if (err.code === 'feature-not-enabled') {
|
|
144
|
-
// // console.warn(err.expected, err.hint);
|
|
145
|
-
// // }
|
|
146
|
-
// // });
|
|
147
|
-
//
|
|
148
|
-
// charter mapping: proposition 1 (single import surface
|
|
149
|
-
// `import { Instances, type InstancesData } from '@forgeax/engine-render'`);
|
|
150
|
-
// proposition 3 (machine-readable schema > prose:
|
|
151
|
-
// `{ transforms: 'array<f32>' }` is the SSOT, stride 16 documented here +
|
|
152
|
-
// enforced at the RenderSystem entry);
|
|
153
|
-
// proposition 4 (explicit failure: stride violation routes a structured
|
|
154
|
-
// EcsError, not a silent half-row); proposition 5 (consistent abstraction:
|
|
155
|
-
// Instances mirrors Children -- both ride the array-vocab path).
|
|
156
|
-
//
|
|
157
|
-
// Anchors: requirements §AC-06 (stride 16 + RenderSystem upload path);
|
|
158
|
-
// plan-strategy §2.3 (stride responsibility migration to RenderSystem entry
|
|
159
|
-
// defensive + AI user set site) + §3.3 stride error path; plan-decisions
|
|
160
|
-
// D-1 / D-5.
|
|
161
|
-
// OOS-09 (no addChild Commands API) does not apply here; OOS-01 (no
|
|
162
|
-
// dangling-entity sweep) does not apply (Instances stores f32 not entity).
|
|
163
|
-
|
|
164
1
|
import { defineComponent } from '@forgeax/engine-ecs';
|
|
165
2
|
|
|
166
3
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* The runtime RenderSystem consumer materialises a fresh `Float32Array`
|
|
174
|
-
* snapshot on every `world.get(e, Instances).transforms` access (D-4
|
|
175
|
-
* no-cache), reads the live count from the snapshot's `length`, and
|
|
176
|
-
* uploads the bytes to a per-entity GPU storage buffer in the record stage
|
|
177
|
-
* (`render-system-record.ts`).
|
|
4
|
+
* World-owned local transforms for one explicit instanced renderable.
|
|
5
|
+
* Packed column-major mat4 values compose as entityWorld * instanceLocal.
|
|
6
|
+
* ECS owns copying, mutation and entity lifetime, including large arrays.
|
|
7
|
+
* Renderer extraction derives its own snapshot and GPU residency; neither
|
|
8
|
+
* authoring nor glTF SceneAsset lowering requires a Renderer identity.
|
|
178
9
|
*
|
|
179
|
-
* @example
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
10
|
+
* @example
|
|
11
|
+
* const transforms = new Float32Array(20_000 * 16);
|
|
12
|
+
* for (let i = 0; i < 20_000; i++) {
|
|
13
|
+
* transforms[i * 16] = transforms[i * 16 + 5] =
|
|
14
|
+
* transforms[i * 16 + 10] = transforms[i * 16 + 15] = 1;
|
|
15
|
+
* }
|
|
16
|
+
* const entity = world.spawn({ component: Instances, data: { transforms } }).unwrap();
|
|
17
|
+
* world.set(entity, Instances, { transforms }).unwrap();
|
|
187
18
|
*/
|
|
188
19
|
export const Instances = defineComponent('Instances', {
|
|
189
20
|
transforms: { type: 'array<f32>' },
|
|
190
21
|
});
|
|
191
22
|
|
|
192
|
-
|
|
193
|
-
* Type-level hint for `data` at the `Instances` spawn site.
|
|
194
|
-
*
|
|
195
|
-
* The runtime ECS column shape for `array<f32>` accepts a `Float32Array`
|
|
196
|
-
* payload at spawn / set time (the bytes are copied into the BufferPool
|
|
197
|
-
* slot). The `transforms` field is documented as `Float32Array` here so
|
|
198
|
-
* AI-user IDE autocomplete picks up the typed-array shape.
|
|
199
|
-
*
|
|
200
|
-
* @example
|
|
201
|
-
* import type { InstancesData } from '@forgeax/engine-render';
|
|
202
|
-
* const data: InstancesData = { transforms: new Float32Array(N * 16) };
|
|
203
|
-
*/
|
|
204
|
-
export type InstancesData = {
|
|
205
|
-
readonly transforms: Float32Array;
|
|
206
|
-
};
|
|
23
|
+
export type InstancesData = { readonly transforms: Float32Array };
|
|
@@ -49,7 +49,12 @@ import {
|
|
|
49
49
|
projectedHeightForCandidate,
|
|
50
50
|
type SubmissionPlan,
|
|
51
51
|
} from './batch-topology';
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
GPU_DRIVEN_VIEW_WGSL,
|
|
54
|
+
type GpuDrivenLodSelectionInspection,
|
|
55
|
+
GpuDrivenView,
|
|
56
|
+
selectGpuLodLane,
|
|
57
|
+
} from './view-gpu';
|
|
53
58
|
|
|
54
59
|
const VERTEX_STAGE = 0x1;
|
|
55
60
|
const FRAGMENT_STAGE = 0x2;
|
|
@@ -1038,6 +1043,34 @@ export class GpuDrivenProduction {
|
|
|
1038
1043
|
return new GpuDrivenProduction(device, shaderModuleFactory);
|
|
1039
1044
|
}
|
|
1040
1045
|
|
|
1046
|
+
/** Recovery has no frame retry: await generated programs before preparing the graph. */
|
|
1047
|
+
static async forRecoveryDevice(
|
|
1048
|
+
device: RhiDevice,
|
|
1049
|
+
factory: PipelineBuilderShaderModuleFactory,
|
|
1050
|
+
createShaderModule: (descriptor: {
|
|
1051
|
+
code: string;
|
|
1052
|
+
label?: string;
|
|
1053
|
+
}) => Promise<Result<ShaderModule, RhiError>>,
|
|
1054
|
+
): Promise<Result<GpuDrivenProduction, RhiError>> {
|
|
1055
|
+
const modules = new Map<string, ShaderModule>();
|
|
1056
|
+
for (const descriptor of [
|
|
1057
|
+
{ label: 'gpu-driven-view', code: GPU_DRIVEN_VIEW_WGSL },
|
|
1058
|
+
{ label: 'gpu-driven-rigid-unlit', code: GPU_DRIVEN_RIGID_UNLIT_WGSL },
|
|
1059
|
+
]) {
|
|
1060
|
+
const module = await createShaderModule(descriptor);
|
|
1061
|
+
if (!module.ok) return module;
|
|
1062
|
+
modules.set(descriptor.code, module.value);
|
|
1063
|
+
}
|
|
1064
|
+
return ok(
|
|
1065
|
+
new GpuDrivenProduction(device, {
|
|
1066
|
+
createShaderModule: (descriptor) => {
|
|
1067
|
+
const module = modules.get(descriptor.code);
|
|
1068
|
+
return module === undefined ? factory.createShaderModule(descriptor) : ok(module);
|
|
1069
|
+
},
|
|
1070
|
+
}),
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1041
1074
|
createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
|
|
1042
1075
|
return {
|
|
1043
1076
|
kind: 'scene-table',
|
package/src/gpu-scene.ts
CHANGED
|
@@ -225,6 +225,10 @@ export class GpuScene {
|
|
|
225
225
|
drawTemplate: [],
|
|
226
226
|
material: [],
|
|
227
227
|
};
|
|
228
|
+
/** Transform rows whose previous value advances only after submit. */
|
|
229
|
+
private readonly pendingTemporalTransforms = new Set<number>();
|
|
230
|
+
/** Previous rows whose post-submit upload must be retried before the next draw. */
|
|
231
|
+
private readonly pendingTemporalUploads = new Set<number>();
|
|
228
232
|
|
|
229
233
|
private constructor(
|
|
230
234
|
private readonly device: RhiDevice,
|
|
@@ -273,31 +277,47 @@ export class GpuScene {
|
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
sync(delta: RenderSceneApplyResult): Result<GpuSceneSyncResult, RhiError> {
|
|
280
|
+
for (const transformIndex of this.pendingTemporalUploads) {
|
|
281
|
+
this.writesByTable.transform.push(transformIndex);
|
|
282
|
+
}
|
|
283
|
+
const pendingWrites = TABLE_NAMES.some((name) => this.writesByTable[name].length > 0);
|
|
276
284
|
if (
|
|
277
285
|
delta.createdSlots.length === 0 &&
|
|
278
286
|
delta.updatedSlots.length === 0 &&
|
|
279
287
|
delta.recreatedSlots.length === 0 &&
|
|
280
|
-
delta.removedSlots.length === 0
|
|
288
|
+
delta.removedSlots.length === 0 &&
|
|
289
|
+
!pendingWrites
|
|
281
290
|
) {
|
|
282
291
|
this.noChangeFrames += 1;
|
|
283
292
|
return ok({ ranges: 0, bytes: 0, grew: false, cleared: 0 });
|
|
284
293
|
}
|
|
285
294
|
const writesByTable = this.writesByTable;
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
if (!this.identityUploaded && !writesByTable.transform.includes(0)) {
|
|
296
|
+
writesByTable.transform.push(0);
|
|
297
|
+
}
|
|
288
298
|
let highestChangedSlot = -1;
|
|
289
|
-
|
|
299
|
+
const contentUpdatedSlots = delta.contentUpdatedSlots ?? [];
|
|
300
|
+
const contentUpdatedSlotIds = new Set(contentUpdatedSlots.map((record) => record.slot));
|
|
301
|
+
const allocationResets = new Set<number>();
|
|
302
|
+
// RenderScene may recycle a released CPU slot for a different identity in
|
|
303
|
+
// the same delta. Clear the old GPU allocation before ensuring the new
|
|
304
|
+
// record; otherwise the later removal pass clears the newly allocated
|
|
305
|
+
// record and the next transform-only update sees no allocation.
|
|
306
|
+
for (const record of delta.removedSlots) {
|
|
307
|
+
if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
|
|
308
|
+
this.clearSlot(record.slot, writesByTable);
|
|
309
|
+
}
|
|
310
|
+
for (const records of [delta.createdSlots, contentUpdatedSlots, delta.recreatedSlots]) {
|
|
290
311
|
for (const record of records) {
|
|
312
|
+
const previous = this.allocations[record.slot];
|
|
291
313
|
this.ensureAllocation(record, writesByTable);
|
|
314
|
+
if (previous !== this.allocations[record.slot]) allocationResets.add(record.slot);
|
|
292
315
|
if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
for (const record of delta.updatedSlots) {
|
|
296
319
|
if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
|
|
297
320
|
}
|
|
298
|
-
for (const record of delta.removedSlots) {
|
|
299
|
-
if (record.slot > highestChangedSlot) highestChangedSlot = record.slot;
|
|
300
|
-
}
|
|
301
321
|
const requiredCapacity = Math.max(
|
|
302
322
|
highestChangedSlot + 1,
|
|
303
323
|
this.instances.requiredCapacity(),
|
|
@@ -312,11 +332,18 @@ export class GpuScene {
|
|
|
312
332
|
}
|
|
313
333
|
|
|
314
334
|
for (const record of delta.createdSlots) this.writeSlot(record, true, writesByTable);
|
|
315
|
-
for (const record of
|
|
335
|
+
for (const record of contentUpdatedSlots) {
|
|
336
|
+
this.writeSlot(record, allocationResets.has(record.slot), writesByTable);
|
|
337
|
+
}
|
|
338
|
+
for (const record of delta.updatedSlots) {
|
|
339
|
+
if (contentUpdatedSlotIds.has(record.slot)) continue;
|
|
340
|
+
this.writeRootTransform(record, false, writesByTable);
|
|
341
|
+
}
|
|
316
342
|
for (const record of delta.recreatedSlots) this.writeSlot(record, true, writesByTable);
|
|
317
|
-
for (const record of delta.removedSlots) this.clearSlot(record.slot, writesByTable);
|
|
318
343
|
const uploaded = this.uploadRows(writesByTable);
|
|
319
344
|
if (!uploaded.ok) return uploaded;
|
|
345
|
+
this.pendingTemporalUploads.clear();
|
|
346
|
+
for (const name of TABLE_NAMES) writesByTable[name].length = 0;
|
|
320
347
|
this.identityUploaded = true;
|
|
321
348
|
this.clearedSlots += delta.removedSlots.length;
|
|
322
349
|
return ok({
|
|
@@ -329,27 +356,47 @@ export class GpuScene {
|
|
|
329
356
|
|
|
330
357
|
/** Publish current transforms as previous only after a successful submit. */
|
|
331
358
|
commitTemporalFrame(enabled = true): Result<void, RhiError> {
|
|
332
|
-
if (!enabled)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
359
|
+
if (!enabled) {
|
|
360
|
+
this.pendingTemporalTransforms.clear();
|
|
361
|
+
this.pendingTemporalUploads.clear();
|
|
362
|
+
return ok(undefined);
|
|
363
|
+
}
|
|
364
|
+
if (this.pendingTemporalTransforms.size === 0) return ok(undefined);
|
|
336
365
|
const currentOffset = offset(TRANSFORM, 'currentWorld');
|
|
337
366
|
const previousOffset = offset(TRANSFORM, 'previousWorld');
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
367
|
+
const ranges = coalesceSlots([...this.pendingTemporalTransforms]);
|
|
368
|
+
for (const range of ranges) {
|
|
369
|
+
for (let index = range.start; index < range.end; index += 1) {
|
|
370
|
+
this.pendingTemporalUploads.add(index);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const range of ranges) {
|
|
374
|
+
for (let index = range.start; index < range.end; index += 1) {
|
|
375
|
+
this.transformBytes.copyWithin(
|
|
376
|
+
index * TRANSFORM.stride + previousOffset,
|
|
341
377
|
index * TRANSFORM.stride + currentOffset,
|
|
342
378
|
index * TRANSFORM.stride + currentOffset + 64,
|
|
343
|
-
)
|
|
344
|
-
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const range of ranges) {
|
|
383
|
+
const firstOffset = range.start * TRANSFORM.stride;
|
|
384
|
+
const lastOffset = range.end * TRANSFORM.stride;
|
|
385
|
+
const uploaded = this.device.queue.writeBuffer(
|
|
386
|
+
this.buffers.transform,
|
|
387
|
+
firstOffset,
|
|
388
|
+
this.transformBytes.subarray(firstOffset, lastOffset),
|
|
345
389
|
);
|
|
390
|
+
if (!uploaded.ok) return uploaded;
|
|
346
391
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
392
|
+
this.pendingTemporalTransforms.clear();
|
|
393
|
+
this.pendingTemporalUploads.clear();
|
|
394
|
+
const bytes = ranges.reduce(
|
|
395
|
+
(total, range) => total + (range.end - range.start) * TRANSFORM.stride,
|
|
396
|
+
0,
|
|
397
|
+
);
|
|
398
|
+
this.uploadRanges += ranges.length;
|
|
399
|
+
this.uploadBytes += bytes;
|
|
353
400
|
return ok(undefined);
|
|
354
401
|
}
|
|
355
402
|
|
|
@@ -359,6 +406,9 @@ export class GpuScene {
|
|
|
359
406
|
this.transforms.reset(1);
|
|
360
407
|
this.draws.reset();
|
|
361
408
|
this.materials.reset();
|
|
409
|
+
this.pendingTemporalTransforms.clear();
|
|
410
|
+
this.pendingTemporalUploads.clear();
|
|
411
|
+
for (const name of TABLE_NAMES) this.writesByTable[name].length = 0;
|
|
362
412
|
const writesByTable: Record<GpuSceneTableName, number[]> = {
|
|
363
413
|
primitive: [],
|
|
364
414
|
instance: [],
|
|
@@ -520,7 +570,8 @@ export class GpuScene {
|
|
|
520
570
|
const localCurrentOffset = localOffset + offset(TRANSFORM, 'currentWorld');
|
|
521
571
|
const localPreviousOffset = localOffset + offset(TRANSFORM, 'previousWorld');
|
|
522
572
|
writeMat4(transform, localCurrentOffset, localWorld);
|
|
523
|
-
writeMat4(transform, localPreviousOffset, localWorld);
|
|
573
|
+
if (resetPrevious) writeMat4(transform, localPreviousOffset, localWorld);
|
|
574
|
+
else this.pendingTemporalTransforms.add(transformIndex);
|
|
524
575
|
writes.transform.push(transformIndex);
|
|
525
576
|
}
|
|
526
577
|
writes.instance.push(instanceIndex);
|
|
@@ -631,14 +682,16 @@ export class GpuScene {
|
|
|
631
682
|
const allocation = this.allocations[record.slot];
|
|
632
683
|
if (allocation === undefined) throw new RangeError('GPU Scene allocation unavailable');
|
|
633
684
|
const transformOffset = allocation.transformStart * TRANSFORM.stride;
|
|
634
|
-
const currentOffset = transformOffset + offset(TRANSFORM, 'currentWorld');
|
|
635
685
|
const previousOffset = transformOffset + offset(TRANSFORM, 'previousWorld');
|
|
636
686
|
if (resetPrevious) {
|
|
637
687
|
writeMat4(this.transformView, previousOffset, record.snapshot.transform.world);
|
|
638
|
-
} else {
|
|
639
|
-
this.transformBytes.copyWithin(previousOffset, currentOffset, currentOffset + 64);
|
|
640
688
|
}
|
|
641
|
-
writeMat4(
|
|
689
|
+
writeMat4(
|
|
690
|
+
this.transformView,
|
|
691
|
+
transformOffset + offset(TRANSFORM, 'currentWorld'),
|
|
692
|
+
record.snapshot.transform.world,
|
|
693
|
+
);
|
|
694
|
+
if (!resetPrevious) this.pendingTemporalTransforms.add(allocation.transformStart);
|
|
642
695
|
writes.transform.push(allocation.transformStart);
|
|
643
696
|
}
|
|
644
697
|
|
|
@@ -743,6 +796,8 @@ export class GpuScene {
|
|
|
743
796
|
TRANSFORM.stride,
|
|
744
797
|
).fill(0);
|
|
745
798
|
writes.transform.push(transformIndex);
|
|
799
|
+
this.pendingTemporalTransforms.delete(transformIndex);
|
|
800
|
+
this.pendingTemporalUploads.delete(transformIndex);
|
|
746
801
|
}
|
|
747
802
|
for (let ordinal = 0; ordinal < allocation.drawCount; ordinal += 1) {
|
|
748
803
|
const drawIndex = allocation.drawStart + ordinal;
|
package/src/index.ts
CHANGED
|
@@ -196,6 +196,7 @@ export type {
|
|
|
196
196
|
ReflectionProbeSelectionInspection,
|
|
197
197
|
TransmissionInspection,
|
|
198
198
|
} from './inspection-types';
|
|
199
|
+
export type { InstanceCollectionInspection } from './instances';
|
|
199
200
|
export {
|
|
200
201
|
type MaterialColorInput3,
|
|
201
202
|
type MaterialColorInput4,
|
|
@@ -23,12 +23,16 @@ import type { GpuBuffer } from './gpu-resource';
|
|
|
23
23
|
* and routes dispose through the RHI shim's destroy bookkeeping SSOT. The
|
|
24
24
|
* two `uploaded*` fields are the cache invalidation fingerprint -- when the
|
|
25
25
|
* archetype version bumps or the byte length changes, the record stage
|
|
26
|
-
* allocates a fresh GpuBuffer and replaces the entry.
|
|
26
|
+
* allocates a fresh GpuBuffer and replaces the entry. Explicit collection
|
|
27
|
+
* entries are keyed by renderer-owned collection id at the outer map; the
|
|
28
|
+
* legacy entity key remains only for sprite/fold snapshots.
|
|
27
29
|
*/
|
|
28
30
|
export interface InstanceBufferCacheEntry {
|
|
29
31
|
readonly buffer: GpuBuffer;
|
|
30
32
|
readonly uploadedByteLength: number;
|
|
31
33
|
readonly uploadedArchVersion: number;
|
|
34
|
+
/** Renderer-owned collection revision uploaded into this buffer. */
|
|
35
|
+
readonly uploadedRevision?: number;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function destroyInstanceBufferEntries(
|
|
@@ -85,6 +89,15 @@ export function disposeInstanceBuffers(
|
|
|
85
89
|
map.clear();
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/** Destroy the renderer-owned buffers used by internal large-instance chunks. */
|
|
93
|
+
export function disposeInstanceBufferChunks(
|
|
94
|
+
map: Map<string, InstanceBufferCacheEntry>,
|
|
95
|
+
errorRegistry?: InstanceBufferCacheErrorSink,
|
|
96
|
+
): void {
|
|
97
|
+
destroyInstanceBufferEntries(map.values(), errorRegistry);
|
|
98
|
+
map.clear();
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
/** Destroy transient uniform-fallback chunks allocated for the previous frame. */
|
|
89
102
|
export function disposeTransientInstanceBuffers(
|
|
90
103
|
entries: InstanceBufferCacheEntry[],
|