@forgeax/engine-render 0.1.32 → 0.1.33
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 +379 -33
- package/dist/assembly/analytic-fog-registration.d.ts +3 -0
- package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
- package/dist/assembly/fxaa-registration.d.ts +4 -0
- package/dist/assembly/fxaa-registration.d.ts.map +1 -0
- package/dist/assembly/lod-observation.d.ts +4 -0
- package/dist/assembly/lod-observation.d.ts.map +1 -0
- package/dist/assembly/motion-blur-registration.d.ts +3 -0
- package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
- package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/bloom-admission.d.ts +13 -0
- package/dist/bloom-admission.d.ts.map +1 -0
- package/dist/{chunk-DRR7D3KX.mjs → chunk-3RTCJTH4.mjs} +12 -9
- package/dist/chunk-3RTCJTH4.mjs.map +1 -0
- package/dist/{chunk-S665NJNF.mjs → chunk-LEAXOMYJ.mjs} +3 -3
- package/dist/{chunk-S665NJNF.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
- package/dist/{chunk-6TFBXLRJ.mjs → chunk-NB2S7ZRX.mjs} +155 -11
- package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
- package/dist/{chunk-LULCA3OF.mjs → chunk-NNQL7CB3.mjs} +4 -4
- package/dist/{chunk-LULCA3OF.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
- package/dist/{chunk-4NNZQCL3.mjs → chunk-OO3DRYK7.mjs} +66 -200
- package/dist/chunk-OO3DRYK7.mjs.map +1 -0
- package/dist/{chunk-GWCEMDVR.mjs → chunk-PLAXOOX5.mjs} +232 -7
- package/dist/chunk-PLAXOOX5.mjs.map +1 -0
- package/dist/{chunk-JRIPCMYC.mjs → chunk-VOZWDM3L.mjs} +283 -193
- package/dist/chunk-VOZWDM3L.mjs.map +1 -0
- package/dist/{chunk-JCIRYQ4V.mjs → chunk-ZDQWGQW7.mjs} +10 -4
- package/dist/chunk-ZDQWGQW7.mjs.map +1 -0
- package/dist/components/atmosphere.d.ts +2 -0
- package/dist/components/atmosphere.d.ts.map +1 -1
- package/dist/components/camera.d.ts +20 -4
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/mesh-filter.d.ts +6 -5
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/motion-blur.d.ts +1 -0
- package/dist/components/motion-blur.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +2865 -526
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +6 -0
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/analytic-fog.d.ts +6 -0
- package/dist/environment/analytic-fog.d.ts.map +1 -0
- package/dist/errors/render.d.ts +6 -2
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +7 -0
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/extract/environment.d.ts +2 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/features/error-reporting.d.ts +13 -0
- package/dist/features/error-reporting.d.ts.map +1 -0
- package/dist/features/host.d.ts +5 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
- package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
- package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
- package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
- package/dist/features/plan.d.ts +7 -4
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +9 -5
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +7 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +3 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/view-gpu.d.ts +9 -0
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +6 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/inspection-types.d.ts +43 -2
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances.d.ts +8 -1
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +6 -6
- package/dist/pipeline/standard-post.d.ts +28 -19
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +2 -2
- package/dist/record/frame-snapshot.d.ts +3 -0
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +5 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +6 -5
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +12 -28
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +5 -3
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +24 -4
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +3 -1
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +8 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +7 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +2 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +7 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/temporal/view.d.ts +6 -1
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/typed-render-graph-primitives.d.ts +10 -7
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +0 -1
- package/dist/volume/component.d.ts.map +1 -1
- package/dist/volume/passes.d.ts.map +1 -1
- package/dist/volume/temporal.d.ts +3 -1
- package/dist/volume/temporal.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
- package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
- package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
- package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
- package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
- package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
- package/src/__tests__/environment-selection.unit.test.ts +12 -0
- package/src/__tests__/factory-contract.integration.test.ts +331 -68
- package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
- package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
- package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
- package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
- package/src/__tests__/gpu-scene.unit.test.ts +91 -0
- package/src/__tests__/instances-store.unit.test.ts +75 -0
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
- package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
- package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
- package/src/__tests__/public-surface.test-d.ts +8 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
- package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
- package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
- package/src/__tests__/shader-manifest-fixture.ts +4 -4
- package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
- package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
- package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
- package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
- package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
- package/src/__tests__/temporal-view.unit.test.ts +2 -1
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
- package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
- package/src/assembly/analytic-fog-registration.ts +14 -0
- package/src/assembly/fxaa-registration.ts +17 -0
- package/src/assembly/lod-observation.ts +12 -0
- package/src/assembly/motion-blur-registration.ts +17 -0
- package/src/assembly/renderer-facade.ts +2 -0
- package/src/assembly/temporal-post-process-prewarm.ts +40 -0
- package/src/assembly/webgpu-ready-contract.ts +8 -3
- package/src/assembly/webgpu-ready.ts +181 -245
- package/src/assembly/webgpu-renderer.ts +16 -31
- package/src/bloom-admission.ts +21 -0
- package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
- package/src/components/atmosphere.ts +4 -0
- package/src/components/camera.ts +66 -10
- package/src/components/mesh-filter.ts +11 -9
- package/src/components/motion-blur.ts +1 -0
- package/src/device/gpu-residency.ts +56 -12
- package/src/environment/analytic-fog.ts +44 -0
- package/src/environment/background.ts +2 -2
- package/src/errors/render.ts +14 -1
- package/src/extract/camera.ts +20 -7
- package/src/extract/contracts.ts +7 -0
- package/src/extract/environment.ts +4 -0
- package/src/features/error-reporting.ts +78 -0
- package/src/features/host.ts +25 -12
- package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
- package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
- package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
- package/src/features/motion-blur/motion-blur-params.ts +70 -2
- package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
- package/src/features/plan.ts +25 -8
- package/src/features/prepared-gpu-work.ts +86 -35
- package/src/features/render-graph-compute.ts +13 -5
- package/src/features/render-graph-contribution.ts +2 -0
- package/src/gpu-driven/material-artifacts.ts +2 -7
- package/src/gpu-driven/prepared-draw.ts +15 -10
- package/src/gpu-driven/production-raster.ts +83 -7
- package/src/gpu-driven/view-gpu.ts +17 -0
- package/src/gpu-scene.ts +188 -21
- package/src/index.ts +32 -0
- package/src/inspection-types.ts +60 -2
- package/src/instances.ts +80 -1
- package/src/pipeline/standard-post.ts +307 -78
- package/src/pipeline-spec.ts +5 -5
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
- package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
- package/src/record/frame-snapshot.ts +5 -1
- package/src/record/frame-targets.ts +10 -6
- package/src/record/frame.ts +163 -22
- package/src/record/main-pass-geometry.ts +30 -1
- package/src/record/main-pass-sprite-draws.ts +6 -1
- package/src/record/mesh-ssbo.ts +43 -6
- package/src/record/render-context.ts +12 -28
- package/src/record/shadow-pass.ts +6 -1
- package/src/record/skybox-post-pass.ts +122 -196
- package/src/record/typed-frame-graph.ts +153 -9
- package/src/recovery/render-system-candidate.ts +22 -10
- package/src/render-contract.ts +23 -4
- package/src/render-pipeline.ts +6 -1
- package/src/render-system-extract-tail.ts +16 -0
- package/src/render-system-extract.ts +45 -13
- package/src/render-system.ts +255 -120
- package/src/scene/render-scene.ts +85 -4
- package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
- package/src/temporal/frame-coordinator.ts +8 -1
- package/src/temporal/frame.ts +20 -0
- package/src/temporal/view.ts +13 -2
- package/src/typed-render-graph-primitives.ts +121 -48
- package/src/volume/component.ts +0 -1
- package/src/volume/passes.ts +8 -4
- package/src/volume/resources.ts +1 -1
- package/src/volume/temporal.ts +11 -0
- package/dist/chunk-4NNZQCL3.mjs.map +0 -1
- package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
- package/dist/chunk-DRR7D3KX.mjs.map +0 -1
- package/dist/chunk-GWCEMDVR.mjs.map +0 -1
- package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
- package/dist/chunk-JRIPCMYC.mjs.map +0 -1
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from '../components';
|
|
27
27
|
import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
|
|
28
28
|
import { gpuDrivenShadowDrawKey } from '../extract/gpu-driven';
|
|
29
|
+
import { motionBlurTemporalDemand } from '../features/motion-blur/motion-blur-params';
|
|
29
30
|
import {
|
|
30
31
|
BatchTopology,
|
|
31
32
|
type BatchTopologyInspection,
|
|
@@ -115,6 +116,41 @@ interface WorldTransformSpan {
|
|
|
115
116
|
|
|
116
117
|
type GlobalTransformChangeQuery = Query<readonly [typeof GlobalTransform]>;
|
|
117
118
|
|
|
119
|
+
function sameInstanceIdentitySet(
|
|
120
|
+
current: Uint32Array | undefined,
|
|
121
|
+
previous: Uint32Array | undefined,
|
|
122
|
+
): boolean {
|
|
123
|
+
// An instanced producer without generation tokens cannot prove that a
|
|
124
|
+
// compacted/reordered row is the same logical object. Keep that legacy
|
|
125
|
+
// snapshot conservative: the record stage will seed its previous matrix
|
|
126
|
+
// instead of borrowing a velocity from an unknown ordinal.
|
|
127
|
+
if (current === undefined || previous === undefined) return false;
|
|
128
|
+
const currentSet = new Set(current);
|
|
129
|
+
const previousSet = new Set(previous);
|
|
130
|
+
if (
|
|
131
|
+
currentSet.size !== current.length ||
|
|
132
|
+
previousSet.size !== previous.length ||
|
|
133
|
+
current.some((identity) => identity === 0) ||
|
|
134
|
+
previous.some((identity) => identity === 0)
|
|
135
|
+
) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (current.length === previous.length) {
|
|
139
|
+
let sameOrder = true;
|
|
140
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
141
|
+
if (current[index] !== previous[index]) {
|
|
142
|
+
sameOrder = false;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (sameOrder) return true;
|
|
147
|
+
}
|
|
148
|
+
// Reorder and retirement are valid when every current token is unique and
|
|
149
|
+
// survives. The set is built only on the topology-change path; steady pose
|
|
150
|
+
// updates stay on the linear same-order check and do not allocate.
|
|
151
|
+
return current.every((identity) => previousSet.has(identity));
|
|
152
|
+
}
|
|
153
|
+
|
|
118
154
|
/** The normal extract request used by the persistent scene owner. */
|
|
119
155
|
export type PersistentRenderCandidateRequest =
|
|
120
156
|
| 'full'
|
|
@@ -202,6 +238,9 @@ function ownSnapshot(snapshot: RenderableSnapshot): RenderableSnapshot {
|
|
|
202
238
|
instances: {
|
|
203
239
|
...snapshot.instances,
|
|
204
240
|
transforms: new Float32Array(snapshot.instances.transforms),
|
|
241
|
+
...(snapshot.instances.generations === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: { generations: new Uint32Array(snapshot.instances.generations) }),
|
|
205
244
|
},
|
|
206
245
|
}),
|
|
207
246
|
...(snapshot.spriteInstances === undefined
|
|
@@ -442,6 +481,9 @@ export class RenderScene {
|
|
|
442
481
|
instances: {
|
|
443
482
|
...operation.instances,
|
|
444
483
|
transforms: new Float32Array(operation.instances.transforms),
|
|
484
|
+
...(operation.instances.generations === undefined
|
|
485
|
+
? {}
|
|
486
|
+
: { generations: new Uint32Array(operation.instances.generations) }),
|
|
445
487
|
},
|
|
446
488
|
};
|
|
447
489
|
pending.instanceChanged = true;
|
|
@@ -922,16 +964,36 @@ export class RenderScene {
|
|
|
922
964
|
reasons.includes('material-revision');
|
|
923
965
|
const previous = seedCurrent || submitted === undefined ? slot.snapshot : submitted.snapshot;
|
|
924
966
|
const previousWorld = this.previousWorldBySlot[slot.slot];
|
|
967
|
+
const currentInstances = slot.snapshot.instances;
|
|
968
|
+
const submittedInstances = submitted?.snapshot.instances;
|
|
969
|
+
const instancesCompatible =
|
|
970
|
+
currentInstances === undefined && submittedInstances === undefined
|
|
971
|
+
? true
|
|
972
|
+
: currentInstances !== undefined &&
|
|
973
|
+
submittedInstances !== undefined &&
|
|
974
|
+
(currentInstances.collectionId === undefined ||
|
|
975
|
+
submittedInstances.collectionId === undefined ||
|
|
976
|
+
currentInstances.collectionId === submittedInstances.collectionId) &&
|
|
977
|
+
currentInstances.transforms.length >= currentInstances.instanceCount * 16 &&
|
|
978
|
+
submittedInstances.transforms.length >= submittedInstances.instanceCount * 16 &&
|
|
979
|
+
sameInstanceIdentitySet(currentInstances.generations, submittedInstances.generations);
|
|
980
|
+
const motionValid = !seedCurrent && previousWorld !== undefined && instancesCompatible;
|
|
925
981
|
return {
|
|
926
982
|
previousEpoch: seedCurrent ? undefined : this.submittedEpoch,
|
|
927
983
|
previousSource: seedCurrent ? 'current-seed' : 'last-submitted',
|
|
984
|
+
motionValid,
|
|
928
985
|
reactive: reasons.length > 0,
|
|
929
986
|
reactiveReasons: reasons,
|
|
930
987
|
previousTransform:
|
|
931
988
|
seedCurrent || previousWorld === undefined
|
|
932
989
|
? previous.transform
|
|
933
990
|
: { ...previous.transform, world: previousWorld },
|
|
934
|
-
|
|
991
|
+
// A collection identity/count break is a temporal cut for that
|
|
992
|
+
// renderable. The record stage receives the current snapshot as its
|
|
993
|
+
// baseline, while stable collections retain the submitted column and
|
|
994
|
+
// use identity generations to map reordered entries and seed replaced
|
|
995
|
+
// entries.
|
|
996
|
+
previousInstances: seedCurrent ? slot.snapshot.instances : previous.instances,
|
|
935
997
|
previousSkin: previous.skin,
|
|
936
998
|
previousMorphWeights: previous.morph?.weights,
|
|
937
999
|
};
|
|
@@ -1273,7 +1335,17 @@ function topologyDeltaForAvailable(
|
|
|
1273
1335
|
|
|
1274
1336
|
function frameDemandsTemporal(frame: ExtractedFrame): boolean {
|
|
1275
1337
|
const camera = frame.cameras[0];
|
|
1276
|
-
|
|
1338
|
+
const motionBlur = camera?.motionBlur;
|
|
1339
|
+
return (
|
|
1340
|
+
camera?.antialias === 'taa' ||
|
|
1341
|
+
(motionBlur !== undefined &&
|
|
1342
|
+
motionBlurTemporalDemand({
|
|
1343
|
+
shutterAngle: motionBlur.shutterAngle,
|
|
1344
|
+
maxRadiusPixels: motionBlur.maxRadiusPixels,
|
|
1345
|
+
sampleCount: motionBlur.sampleCount,
|
|
1346
|
+
targetFps: motionBlur.targetFps ?? 60,
|
|
1347
|
+
}))
|
|
1348
|
+
);
|
|
1277
1349
|
}
|
|
1278
1350
|
|
|
1279
1351
|
function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array[] {
|
|
@@ -2510,13 +2582,17 @@ export class PersistentRenderScene {
|
|
|
2510
2582
|
// Previous transforms are consumed only by TAA and motion blur. Ordinary
|
|
2511
2583
|
// frames must not copy and upload the complete GPU transform table.
|
|
2512
2584
|
const gpuCommit = this.gpuScene?.commitTemporalFrame(this.temporalCapture !== undefined);
|
|
2513
|
-
if (gpuCommit !== undefined && !gpuCommit.ok)
|
|
2585
|
+
if (gpuCommit !== undefined && !gpuCommit.ok) {
|
|
2586
|
+
this.temporalCapture = undefined;
|
|
2587
|
+
return gpuCommit;
|
|
2588
|
+
}
|
|
2514
2589
|
const temporalCapture = this.temporalCapture;
|
|
2515
2590
|
if (temporalCapture !== undefined) {
|
|
2516
2591
|
try {
|
|
2517
2592
|
temporalCapture.projection.commitSubmission(temporalCapture.capture);
|
|
2518
2593
|
} catch (cause) {
|
|
2519
2594
|
void cause;
|
|
2595
|
+
this.temporalCapture = undefined;
|
|
2520
2596
|
return err(
|
|
2521
2597
|
new RhiError({
|
|
2522
2598
|
code: 'internal-error',
|
|
@@ -2530,6 +2606,11 @@ export class PersistentRenderScene {
|
|
|
2530
2606
|
return ok(undefined);
|
|
2531
2607
|
}
|
|
2532
2608
|
|
|
2609
|
+
/** Drop a staged temporal capture when the owning frame did not submit. */
|
|
2610
|
+
discardTemporalFrame(): void {
|
|
2611
|
+
this.temporalCapture = undefined;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2533
2614
|
/** Capture current CPU scene facts; commitTemporalFrame publishes them only after submit. */
|
|
2534
2615
|
prepareTemporalFrame(
|
|
2535
2616
|
visible: readonly Pick<RenderableSnapshot, 'worldId' | 'entityKey'>[],
|
|
@@ -2912,7 +2993,7 @@ export class PersistentRenderScene {
|
|
|
2912
2993
|
recreatedSlots: [],
|
|
2913
2994
|
resynced: 0,
|
|
2914
2995
|
})
|
|
2915
|
-
: acquired.scene.sync(delta);
|
|
2996
|
+
: acquired.scene.sync(delta, (slot) => projection.temporalSnapshotBySlot(slot.slot));
|
|
2916
2997
|
if (!result.ok) {
|
|
2917
2998
|
this.sceneTableUploadBytes = 0;
|
|
2918
2999
|
this.failGpu(result.error);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createTemporalFrameTransaction } from '../frame';
|
|
3
|
+
|
|
4
|
+
function frame(frameId: number, resetReason?: 'camera-cut') {
|
|
5
|
+
return {
|
|
6
|
+
frameId,
|
|
7
|
+
currentViewProjection: new Float32Array(16),
|
|
8
|
+
jitter: [0, 0] as [number, number],
|
|
9
|
+
viewport: { width: 64, height: 64 },
|
|
10
|
+
cameraPosition: [frameId, 0, 0] as [number, number, number],
|
|
11
|
+
...(resetReason === undefined ? {} : { resetReason }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('renderer temporal candidate lifecycle', () => {
|
|
16
|
+
it('projects a rejected submit while retaining the accepted baseline', () => {
|
|
17
|
+
const transaction = createTemporalFrameTransaction({ deviceEpoch: 4 });
|
|
18
|
+
transaction.stage(frame(1));
|
|
19
|
+
expect(transaction.commit({ accepted: true }).ok).toBe(true);
|
|
20
|
+
const baseline = transaction.snapshot();
|
|
21
|
+
|
|
22
|
+
transaction.stage(frame(2));
|
|
23
|
+
expect(transaction.commit({ accepted: false, reason: 'queue-submit-failed' }).ok).toBe(false);
|
|
24
|
+
expect(transaction.snapshot()).toEqual(baseline);
|
|
25
|
+
expect(transaction.inspect()).toEqual({
|
|
26
|
+
accepted: true,
|
|
27
|
+
staged: false,
|
|
28
|
+
lastFailure: 'queue-submit-failed',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
transaction.stage(frame(3));
|
|
32
|
+
expect(transaction.commit({ accepted: true }).ok).toBe(true);
|
|
33
|
+
expect(transaction.inspect()).toEqual({ accepted: true, staged: false });
|
|
34
|
+
expect(transaction.snapshot().frameId).toBe(3);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import { motionBlurTemporalDemand } from '../features/motion-blur/motion-blur-params';
|
|
2
3
|
import type { CameraSnapshot } from '../render-contract';
|
|
3
4
|
import { classifySceneDataCoverage, type SceneDataContributor } from './coverage';
|
|
4
5
|
import { type TaaHistoryAttempt, TaaHistoryStore } from './taa-history-store';
|
|
@@ -301,7 +302,13 @@ export class TemporalFrameCoordinator<T = unknown> {
|
|
|
301
302
|
): TemporalView | undefined {
|
|
302
303
|
const demanded =
|
|
303
304
|
camera.antialias === 'taa' ||
|
|
304
|
-
(camera.motionBlur
|
|
305
|
+
(camera.motionBlur !== undefined &&
|
|
306
|
+
motionBlurTemporalDemand({
|
|
307
|
+
shutterAngle: camera.motionBlur.shutterAngle,
|
|
308
|
+
maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
|
|
309
|
+
sampleCount: camera.motionBlur.sampleCount,
|
|
310
|
+
targetFps: camera.motionBlur.targetFps ?? 60,
|
|
311
|
+
}));
|
|
305
312
|
if (!demanded) return undefined;
|
|
306
313
|
return projectTemporalView({
|
|
307
314
|
camera,
|
package/src/temporal/frame.ts
CHANGED
|
@@ -40,10 +40,18 @@ export interface TemporalSubmitResult {
|
|
|
40
40
|
readonly resetReason?: Exclude<TemporalResetReason, 'none' | 'queue-submit-failed'>;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Detached candidate/lifecycle facts consumed by Renderer inspection. */
|
|
44
|
+
export interface TemporalFrameTransactionInspection {
|
|
45
|
+
readonly accepted: boolean;
|
|
46
|
+
readonly staged: boolean;
|
|
47
|
+
readonly lastFailure?: 'queue-submit-failed';
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
export interface TemporalFrameTransaction {
|
|
44
51
|
stage(input: TemporalFrameInput): void;
|
|
45
52
|
commit(submit: TemporalSubmitResult): Result<TemporalFrame, TemporalFrameSubmitError>;
|
|
46
53
|
snapshot(): TemporalFrame;
|
|
54
|
+
inspect(): TemporalFrameTransactionInspection;
|
|
47
55
|
/** Drop accepted/staged history after a producer or device generation cut. */
|
|
48
56
|
reset(reason?: Extract<TemporalResetReason, 'signature-change' | 'device-recovery'>): void;
|
|
49
57
|
}
|
|
@@ -85,6 +93,7 @@ export function createTemporalFrameTransaction(options: {
|
|
|
85
93
|
let staged: TemporalFrameInput | undefined;
|
|
86
94
|
let historyEpoch = 0;
|
|
87
95
|
let pendingResetReason: Exclude<TemporalResetReason, 'none' | 'queue-submit-failed'> | undefined;
|
|
96
|
+
let lastFailure: TemporalFrameTransactionInspection['lastFailure'];
|
|
88
97
|
|
|
89
98
|
return {
|
|
90
99
|
stage(input) {
|
|
@@ -93,9 +102,11 @@ export function createTemporalFrameTransaction(options: {
|
|
|
93
102
|
commit(submit) {
|
|
94
103
|
if (!submit.accepted) {
|
|
95
104
|
staged = undefined;
|
|
105
|
+
lastFailure = submit.reason ?? 'queue-submit-failed';
|
|
96
106
|
return err(new TemporalFrameSubmitError(submit.reason ?? 'queue-submit-failed'));
|
|
97
107
|
}
|
|
98
108
|
if (staged === undefined) {
|
|
109
|
+
lastFailure = 'queue-submit-failed';
|
|
99
110
|
return err(new TemporalFrameSubmitError('queue-submit-failed'));
|
|
100
111
|
}
|
|
101
112
|
|
|
@@ -121,6 +132,7 @@ export function createTemporalFrameTransaction(options: {
|
|
|
121
132
|
accepted = next;
|
|
122
133
|
staged = undefined;
|
|
123
134
|
pendingResetReason = undefined;
|
|
135
|
+
lastFailure = undefined;
|
|
124
136
|
return ok(copyFrame(next));
|
|
125
137
|
},
|
|
126
138
|
snapshot() {
|
|
@@ -128,6 +140,13 @@ export function createTemporalFrameTransaction(options: {
|
|
|
128
140
|
throw new Error('TemporalFrame snapshot is unavailable before commit');
|
|
129
141
|
return copyFrame(accepted);
|
|
130
142
|
},
|
|
143
|
+
inspect() {
|
|
144
|
+
return {
|
|
145
|
+
accepted: accepted !== undefined,
|
|
146
|
+
staged: staged !== undefined,
|
|
147
|
+
...(lastFailure === undefined ? {} : { lastFailure }),
|
|
148
|
+
};
|
|
149
|
+
},
|
|
131
150
|
reset(reason = 'signature-change') {
|
|
132
151
|
accepted = undefined;
|
|
133
152
|
staged = undefined;
|
|
@@ -136,6 +155,7 @@ export function createTemporalFrameTransaction(options: {
|
|
|
136
155
|
// committed frame prevents stale motion vectors from crossing the
|
|
137
156
|
// producer/device boundary.
|
|
138
157
|
pendingResetReason = reason;
|
|
158
|
+
lastFailure = undefined;
|
|
139
159
|
},
|
|
140
160
|
};
|
|
141
161
|
}
|
package/src/temporal/view.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type TemporalResetReason =
|
|
|
21
21
|
| 'view-switch'
|
|
22
22
|
| 'environment-change'
|
|
23
23
|
| 'fog-change'
|
|
24
|
+
| 'time-discontinuity'
|
|
24
25
|
| 'device-recover'
|
|
25
26
|
| 'submit-failure';
|
|
26
27
|
|
|
@@ -43,6 +44,8 @@ export interface TemporalViewInput {
|
|
|
43
44
|
readonly previousUnjitteredViewProjection?: ArrayLike<number> | undefined;
|
|
44
45
|
readonly currentCameraPosition?: readonly [number, number, number] | undefined;
|
|
45
46
|
readonly previousCameraPosition?: readonly [number, number, number] | undefined;
|
|
47
|
+
/** Monotonic accepted sample time used to bridge failed render attempts. */
|
|
48
|
+
readonly sampleTimeSeconds?: number | undefined;
|
|
46
49
|
readonly resetReason?: TemporalResetReason | undefined;
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -65,6 +68,7 @@ interface TemporalViewSnapshot {
|
|
|
65
68
|
readonly previousUnjitteredViewProjection: ArrayLike<number> | undefined;
|
|
66
69
|
readonly currentCameraPosition: readonly [number, number, number] | undefined;
|
|
67
70
|
readonly previousCameraPosition: readonly [number, number, number] | undefined;
|
|
71
|
+
readonly sampleTimeSeconds: number | undefined;
|
|
68
72
|
readonly resetReason: TemporalResetReason | undefined;
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -83,6 +87,8 @@ export interface TemporalView {
|
|
|
83
87
|
/** Camera origins follow the same accepted-submit boundary as the matrices. */
|
|
84
88
|
readonly currentCameraPosition: readonly [number, number, number] | undefined;
|
|
85
89
|
readonly previousCameraPosition: readonly [number, number, number] | undefined;
|
|
90
|
+
/** Monotonic accepted sample time used to bridge failed render attempts. */
|
|
91
|
+
readonly sampleTimeSeconds: number | undefined;
|
|
86
92
|
readonly currentProjection: 'jittered' | 'unjittered';
|
|
87
93
|
readonly previousProjection: 'unjittered';
|
|
88
94
|
readonly historyRequired: boolean;
|
|
@@ -131,6 +137,7 @@ export function createTemporalView(input: TemporalViewInput): TemporalView {
|
|
|
131
137
|
previousUnjitteredViewProjection: input.previousUnjitteredViewProjection,
|
|
132
138
|
currentCameraPosition: input.currentCameraPosition,
|
|
133
139
|
previousCameraPosition: input.previousCameraPosition,
|
|
140
|
+
sampleTimeSeconds: input.sampleTimeSeconds,
|
|
134
141
|
resetReason: input.resetReason,
|
|
135
142
|
});
|
|
136
143
|
const taa = input.antialias === 'taa';
|
|
@@ -156,6 +163,7 @@ export function createTemporalView(input: TemporalViewInput): TemporalView {
|
|
|
156
163
|
previousUnjitteredViewProjection: input.previousUnjitteredViewProjection,
|
|
157
164
|
currentCameraPosition: input.currentCameraPosition,
|
|
158
165
|
previousCameraPosition: input.previousCameraPosition,
|
|
166
|
+
sampleTimeSeconds: input.sampleTimeSeconds,
|
|
159
167
|
currentProjection: taa ? 'jittered' : 'unjittered',
|
|
160
168
|
previousProjection: 'unjittered',
|
|
161
169
|
historyRequired: taa,
|
|
@@ -167,8 +175,11 @@ export function resolveTemporalReset(
|
|
|
167
175
|
previous: TemporalView | undefined,
|
|
168
176
|
next: TemporalView,
|
|
169
177
|
): TemporalResetReason | undefined {
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
// Motion Blur uses the unjittered `off` temporal mode as well as TAA. Keep
|
|
179
|
+
// the reset matrix shared so a camera switch, resize, or recovery cannot
|
|
180
|
+
// leak a previous motion vector merely because antialiasing is disabled.
|
|
181
|
+
if (previous === undefined) return 'first-frame';
|
|
182
|
+
if (next.mode === 'taa' && previous.mode !== 'taa') return 'first-frame';
|
|
172
183
|
const before = previous.input;
|
|
173
184
|
const after = next.input;
|
|
174
185
|
if (before.width !== after.width || before.height !== after.height) return 'resize';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
GraphAccess,
|
|
3
|
-
GraphResourceResolver,
|
|
4
|
-
GraphTextureView,
|
|
5
|
-
RenderGraphBuilder,
|
|
1
|
+
import {
|
|
2
|
+
type GraphAccess,
|
|
3
|
+
type GraphResourceResolver,
|
|
4
|
+
type GraphTextureView,
|
|
5
|
+
type RenderGraphBuilder,
|
|
6
6
|
RenderGraphError,
|
|
7
|
-
ResolveContext,
|
|
7
|
+
type ResolveContext,
|
|
8
8
|
} from '@forgeax/engine-render-graph';
|
|
9
9
|
import {
|
|
10
10
|
type Buffer,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type TextureFormat,
|
|
14
14
|
type TextureView,
|
|
15
15
|
} from '@forgeax/engine-rhi';
|
|
16
|
-
import { ok, type PassKind, type PassSelector, type Result } from '@forgeax/engine-types';
|
|
16
|
+
import { err, ok, type PassKind, type PassSelector, type Result } from '@forgeax/engine-types';
|
|
17
17
|
import {
|
|
18
18
|
GPU_TEXTURE_USAGE_COPY_SRC,
|
|
19
19
|
GPU_TEXTURE_USAGE_RENDER_ATTACHMENT,
|
|
@@ -28,10 +28,9 @@ import type {
|
|
|
28
28
|
} from './record/render-context';
|
|
29
29
|
import {
|
|
30
30
|
encodeSkyboxPass,
|
|
31
|
-
recordBloomBlurHPass,
|
|
32
|
-
recordBloomBlurVPass,
|
|
33
|
-
recordBloomBrightPass,
|
|
34
31
|
recordBloomCompositePass,
|
|
32
|
+
recordBloomDownsamplePass,
|
|
33
|
+
recordBloomUpsamplePass,
|
|
35
34
|
} from './record/skybox-post-pass';
|
|
36
35
|
import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from './render-contract';
|
|
37
36
|
import {
|
|
@@ -502,57 +501,57 @@ export function addReflectionFallbackObservationPass(
|
|
|
502
501
|
export interface TypedBloomTargets {
|
|
503
502
|
readonly scene: RenderPipelineTarget;
|
|
504
503
|
readonly composited: RenderPipelineTarget;
|
|
505
|
-
readonly
|
|
506
|
-
readonly
|
|
507
|
-
readonly
|
|
504
|
+
readonly downsample: readonly RenderPipelineTarget[];
|
|
505
|
+
readonly upsample: readonly RenderPipelineTarget[];
|
|
506
|
+
readonly levelDimensions: readonly { readonly width: number; readonly height: number }[];
|
|
508
507
|
}
|
|
509
508
|
|
|
510
509
|
/** The semantic Bloom pass owner; graph inspection consumes these same identities. */
|
|
511
510
|
export const TYPED_BLOOM_PASS_NAMES = {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
blurV: 'bloom-blur-v',
|
|
511
|
+
downsample: 'bloom-downsample',
|
|
512
|
+
upsample: 'bloom-upsample',
|
|
515
513
|
composite: 'bloom-composite',
|
|
516
514
|
} as const;
|
|
517
515
|
|
|
516
|
+
export function typedBloomLevelPassName(kind: 'downsample' | 'upsample', level: number): string {
|
|
517
|
+
return `${TYPED_BLOOM_PASS_NAMES[kind]}-${level}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
518
520
|
export function addTypedBloomPasses(
|
|
519
521
|
graph: RenderGraphBuilder<RenderPipelineFrame>,
|
|
520
522
|
targets: TypedBloomTargets,
|
|
521
523
|
): Result<void, RenderGraphError> {
|
|
522
|
-
const named = {
|
|
524
|
+
const named: Record<string, RenderPipelineTarget> = {
|
|
523
525
|
hdrColor: targets.scene,
|
|
524
526
|
hdrComposited: targets.composited,
|
|
525
|
-
bloomBright: targets.bright,
|
|
526
|
-
bloomBlurH: targets.blurH,
|
|
527
|
-
bloomBlurV: targets.blurV,
|
|
528
527
|
};
|
|
529
|
-
|
|
530
|
-
{
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
528
|
+
targets.downsample.forEach((target, level) => {
|
|
529
|
+
named[`bloomDownsample${level}`] = target;
|
|
530
|
+
});
|
|
531
|
+
targets.upsample.forEach((target, level) => {
|
|
532
|
+
named[`bloomUpsample${level}`] = target;
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const add = (
|
|
536
|
+
name: string,
|
|
537
|
+
reads: readonly RenderPipelineTarget[],
|
|
538
|
+
write: RenderPipelineTarget,
|
|
539
|
+
encode: (
|
|
540
|
+
frame: _InternalRenderPipelineContext,
|
|
541
|
+
resolve: ResolveContext,
|
|
542
|
+
pass: import('@forgeax/engine-rhi').RhiRenderPassEncoder,
|
|
543
|
+
level?: number,
|
|
544
|
+
size?: { readonly width: number; readonly height: number },
|
|
545
|
+
) => void,
|
|
546
|
+
level?: number,
|
|
547
|
+
): Result<void, RenderGraphError> => {
|
|
548
|
+
const descriptor = {
|
|
549
|
+
name,
|
|
550
|
+
reads,
|
|
551
|
+
write,
|
|
552
|
+
encode,
|
|
553
|
+
level,
|
|
554
|
+
};
|
|
556
555
|
const added = graph.addRasterPass(descriptor.name, {
|
|
557
556
|
accesses: [
|
|
558
557
|
...descriptor.reads.map((target) => ({
|
|
@@ -574,11 +573,85 @@ export function addTypedBloomPasses(
|
|
|
574
573
|
frame as _InternalRenderPipelineContext,
|
|
575
574
|
legacyResolver(resources, named),
|
|
576
575
|
pass,
|
|
576
|
+
descriptor.level,
|
|
577
|
+
descriptor.level === undefined ? undefined : targets.levelDimensions[descriptor.level],
|
|
577
578
|
),
|
|
578
579
|
});
|
|
580
|
+
return added.ok ? ok(undefined) : added;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
for (const [level, write] of targets.downsample.entries()) {
|
|
584
|
+
const source = level === 0 ? targets.scene : targets.downsample[level - 1];
|
|
585
|
+
if (source === undefined) {
|
|
586
|
+
return err(
|
|
587
|
+
new RenderGraphError({
|
|
588
|
+
code: 'resource-resolution-failed',
|
|
589
|
+
expected: 'Bloom downsample levels form one adjacent pyramid',
|
|
590
|
+
hint: 'rebuild the Standard Bloom target declaration from one level list',
|
|
591
|
+
detail: { resourceLabel: `bloom-level-${level}` },
|
|
592
|
+
}),
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
const added = add(
|
|
596
|
+
typedBloomLevelPassName('downsample', level),
|
|
597
|
+
[source],
|
|
598
|
+
write,
|
|
599
|
+
(frame, resolve, pass, passLevel) =>
|
|
600
|
+
recordBloomDownsamplePass(
|
|
601
|
+
frame,
|
|
602
|
+
resolve,
|
|
603
|
+
pass,
|
|
604
|
+
passLevel ?? level,
|
|
605
|
+
targets.levelDimensions[passLevel ?? level],
|
|
606
|
+
),
|
|
607
|
+
level,
|
|
608
|
+
);
|
|
579
609
|
if (!added.ok) return added;
|
|
580
610
|
}
|
|
581
|
-
|
|
611
|
+
for (let level = targets.upsample.length - 1; level >= 0; level -= 1) {
|
|
612
|
+
const current = targets.downsample[level];
|
|
613
|
+
const coarse =
|
|
614
|
+
level === targets.downsample.length - 2
|
|
615
|
+
? targets.downsample[level + 1]
|
|
616
|
+
: targets.upsample[level + 1];
|
|
617
|
+
const write = targets.upsample[level];
|
|
618
|
+
if (current === undefined || coarse === undefined || write === undefined) {
|
|
619
|
+
return err(
|
|
620
|
+
new RenderGraphError({
|
|
621
|
+
code: 'resource-resolution-failed',
|
|
622
|
+
expected: 'Bloom downsample and upsample levels form one adjacent pyramid',
|
|
623
|
+
hint: 'rebuild the Standard Bloom target declaration from one level list',
|
|
624
|
+
detail: { resourceLabel: `bloom-level-${level}` },
|
|
625
|
+
}),
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const added = add(
|
|
629
|
+
typedBloomLevelPassName('upsample', level),
|
|
630
|
+
[current, coarse],
|
|
631
|
+
write,
|
|
632
|
+
(frame, resolve, pass, passLevel) =>
|
|
633
|
+
recordBloomUpsamplePass(frame, resolve, pass, passLevel ?? level),
|
|
634
|
+
level,
|
|
635
|
+
);
|
|
636
|
+
if (!added.ok) return added;
|
|
637
|
+
}
|
|
638
|
+
const finest = targets.upsample[0] ?? targets.downsample[0];
|
|
639
|
+
if (finest === undefined) {
|
|
640
|
+
return err(
|
|
641
|
+
new RenderGraphError({
|
|
642
|
+
code: 'resource-resolution-failed',
|
|
643
|
+
expected: 'Bloom has at least one downsample level',
|
|
644
|
+
hint: 'derive Bloom levels from a positive output extent',
|
|
645
|
+
detail: { resourceLabel: 'bloom-downsample-0' },
|
|
646
|
+
}),
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return add(
|
|
650
|
+
TYPED_BLOOM_PASS_NAMES.composite,
|
|
651
|
+
[targets.scene, finest],
|
|
652
|
+
targets.composited,
|
|
653
|
+
(frame, resolve, pass) => recordBloomCompositePass(frame, resolve, pass),
|
|
654
|
+
);
|
|
582
655
|
}
|
|
583
656
|
|
|
584
657
|
export interface TypedSsaoTargets {
|
package/src/volume/component.ts
CHANGED
|
@@ -35,7 +35,6 @@ export interface VolumetricFogAuthoring {
|
|
|
35
35
|
readonly light: EntityHandle;
|
|
36
36
|
/** The optional same-World SpotLight paired with a PointLight. */
|
|
37
37
|
readonly spotLight?: EntityHandle;
|
|
38
|
-
/** Linear scalar density over bounds; the renderer adds no noise or animation. */
|
|
39
38
|
readonly density: VolumeDensityBinding;
|
|
40
39
|
readonly bounds: VolumeBounds;
|
|
41
40
|
readonly extinction: readonly [number, number, number];
|
package/src/volume/passes.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
VOLUME_HISTORY_FORMAT,
|
|
33
33
|
VOLUME_PACKING_Z,
|
|
34
34
|
VOLUME_RESOLVED_FORMAT,
|
|
35
|
+
VOLUMETRIC_FOG_PARAMS_BYTES,
|
|
35
36
|
} from './resources';
|
|
36
37
|
import type { VolumeProjectorTuple } from './temporal';
|
|
37
38
|
|
|
@@ -214,7 +215,10 @@ function importVolumeInputs(
|
|
|
214
215
|
if (!density.ok) return density;
|
|
215
216
|
const params = graph.importBuffer(
|
|
216
217
|
'volume-params',
|
|
217
|
-
{
|
|
218
|
+
{
|
|
219
|
+
size: VOLUMETRIC_FOG_PARAMS_BYTES,
|
|
220
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
221
|
+
},
|
|
218
222
|
(frame) => {
|
|
219
223
|
const context = frame.volumetricFog as VolumetricFogFrameContext | undefined;
|
|
220
224
|
if (context === undefined) throw pipelineError('parameter residency');
|
|
@@ -481,9 +485,9 @@ export function addVolumetricFogPasses(
|
|
|
481
485
|
const densitySampler = frame.runtime.device.createSampler({
|
|
482
486
|
minFilter: 'linear',
|
|
483
487
|
magFilter: 'linear',
|
|
484
|
-
addressModeU: '
|
|
485
|
-
addressModeV: '
|
|
486
|
-
addressModeW: '
|
|
488
|
+
addressModeU: 'repeat',
|
|
489
|
+
addressModeV: 'repeat',
|
|
490
|
+
addressModeW: 'repeat',
|
|
487
491
|
});
|
|
488
492
|
if (!densitySampler.ok) throw densitySampler.error;
|
|
489
493
|
const bindings = volumeBindGroup(frame, integrate.layout, [
|
package/src/volume/resources.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const VOLUME_RESOLVED_FORMAT = 'rgba16float' as const;
|
|
|
15
15
|
export const VOLUME_DEPTH_FORMAT = 'r32float' as const;
|
|
16
16
|
export const VOLUME_PACKING_Z = 4;
|
|
17
17
|
/** Renderer-owned parameter allocation shared by staging and inspection. */
|
|
18
|
-
export const VOLUMETRIC_FOG_PARAMS_BYTES =
|
|
18
|
+
export const VOLUMETRIC_FOG_PARAMS_BYTES = 9 * 16;
|
|
19
19
|
|
|
20
20
|
export interface VolumetricFogExtent {
|
|
21
21
|
readonly width: number;
|
package/src/volume/temporal.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface VolumeTemporalSignature {
|
|
|
5
5
|
readonly densityGeneration: number;
|
|
6
6
|
readonly width: number;
|
|
7
7
|
readonly height: number;
|
|
8
|
+
/** World-owned authored-medium time; render frame identity never advances the medium. */
|
|
9
|
+
readonly worldTimeSeconds?: number;
|
|
8
10
|
readonly lightKind?: 'directional' | 'point' | 'spot';
|
|
9
11
|
readonly lightEntity?: number;
|
|
10
12
|
readonly pointLightEntity?: number;
|
|
@@ -28,6 +30,7 @@ export type VolumeTemporalResetReason =
|
|
|
28
30
|
| 'fog-revision'
|
|
29
31
|
| 'light-revision'
|
|
30
32
|
| 'density-generation'
|
|
33
|
+
| 'simulation-time'
|
|
31
34
|
| 'resize'
|
|
32
35
|
| 'reprojection-out-of-screen'
|
|
33
36
|
| 'reprojection-depth-discontinuity';
|
|
@@ -92,6 +95,14 @@ export function resolveVolumeTemporalReset(
|
|
|
92
95
|
if (previous.lightShadowRevision !== next.lightShadowRevision) {
|
|
93
96
|
return { reset: true, reason: 'light-revision' };
|
|
94
97
|
}
|
|
98
|
+
if (previous.worldTimeSeconds !== next.worldTimeSeconds) {
|
|
99
|
+
const delta = (next.worldTimeSeconds ?? NaN) - (previous.worldTimeSeconds ?? NaN);
|
|
100
|
+
// Small monotonic ticks animate the same medium. Reset only on a clock
|
|
101
|
+
// discontinuity; rejecting every tick prevents temporal reuse in live play.
|
|
102
|
+
if (!Number.isFinite(delta) || delta < 0 || delta > 0.1) {
|
|
103
|
+
return { reset: true, reason: 'simulation-time' };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
95
106
|
if (previous.densityGeneration !== next.densityGeneration) {
|
|
96
107
|
return { reset: true, reason: 'density-generation' };
|
|
97
108
|
}
|