@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
package/src/extract/camera.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { mat4, vec3 } from '@forgeax/engine-math';
|
|
|
4
4
|
import { GlobalTransform } from '@forgeax/engine-scene';
|
|
5
5
|
import {
|
|
6
6
|
antialiasFromF32,
|
|
7
|
-
bloomEnabledFromF32,
|
|
8
7
|
Camera,
|
|
9
8
|
cameraExposureFromColumns,
|
|
10
9
|
cameraProjectionFromF32,
|
|
11
10
|
tonemapFromF32,
|
|
11
|
+
validateCameraBloom,
|
|
12
12
|
validateCameraColorGrading,
|
|
13
13
|
} from '../components/camera';
|
|
14
14
|
import { DepthOfField } from '../components/depth-of-field';
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
depthOfFieldRequestFailure,
|
|
23
23
|
resolveDepthOfFieldParams,
|
|
24
24
|
} from '../features/depth-of-field/depth-of-field-params';
|
|
25
|
+
import { resolveMotionBlurParams } from '../features/motion-blur/motion-blur-params';
|
|
25
26
|
import type { CameraSnapshot } from '../render-contract';
|
|
26
27
|
import { getActiveCamera, selectActiveCameraIndex } from '../systems/active-camera';
|
|
27
28
|
|
|
@@ -44,6 +45,9 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
44
45
|
for (const row of cameraQuery) {
|
|
45
46
|
const cam = row.get(Camera);
|
|
46
47
|
const motionBlur = row.get(MotionBlur);
|
|
48
|
+
const motionBlurResult = resolveMotionBlurParams(motionBlur);
|
|
49
|
+
if (!motionBlurResult.ok) throw motionBlurResult.error;
|
|
50
|
+
const motionBlurParams = motionBlurResult.value;
|
|
47
51
|
const depthOfField = row.get(DepthOfField);
|
|
48
52
|
validateCameraColorGrading(cam.temperature, cam.tint, cam.colorLutStrength);
|
|
49
53
|
const dynamicResolution = row.get(DynamicResolution);
|
|
@@ -58,6 +62,13 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
58
62
|
? undefined
|
|
59
63
|
: depthOfFieldRequestFailure(depthOfFieldResult.error);
|
|
60
64
|
const screenSpaceReflection = row.get(ScreenSpaceReflection);
|
|
65
|
+
const bloom = validateCameraBloom(
|
|
66
|
+
cam.bloom,
|
|
67
|
+
cam.bloomThreshold,
|
|
68
|
+
cam.bloomIntensity,
|
|
69
|
+
cam.bloomSoftKnee,
|
|
70
|
+
cam.bloomScatter,
|
|
71
|
+
);
|
|
61
72
|
const entity = row.entity as EntityHandle;
|
|
62
73
|
const worldMat = readWorldMatrix(world, entity);
|
|
63
74
|
if (worldMat === undefined) continue;
|
|
@@ -92,10 +103,11 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
92
103
|
...(dynamicResolutionResult.value === undefined
|
|
93
104
|
? {}
|
|
94
105
|
: { dynamicResolution: dynamicResolutionResult.value }),
|
|
95
|
-
bloom
|
|
106
|
+
bloom,
|
|
96
107
|
bloomThreshold: cam.bloomThreshold,
|
|
97
108
|
bloomIntensity: cam.bloomIntensity,
|
|
98
|
-
|
|
109
|
+
bloomSoftKnee: cam.bloomSoftKnee,
|
|
110
|
+
bloomScatter: cam.bloomScatter,
|
|
99
111
|
clearColor: [
|
|
100
112
|
cam.clearColor[0] ?? 0,
|
|
101
113
|
cam.clearColor[1] ?? 0,
|
|
@@ -106,13 +118,14 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
106
118
|
? { depthOfField: depthOfFieldResult.value }
|
|
107
119
|
: {}),
|
|
108
120
|
...(depthOfFieldError === undefined ? {} : { depthOfFieldError }),
|
|
109
|
-
...(
|
|
121
|
+
...(motionBlurParams === undefined
|
|
110
122
|
? {}
|
|
111
123
|
: {
|
|
112
124
|
motionBlur: {
|
|
113
|
-
shutterAngle:
|
|
114
|
-
maxRadiusPixels:
|
|
115
|
-
sampleCount:
|
|
125
|
+
shutterAngle: motionBlurParams.shutterAngle,
|
|
126
|
+
maxRadiusPixels: motionBlurParams.maxRadiusPixels,
|
|
127
|
+
sampleCount: motionBlurParams.sampleCount,
|
|
128
|
+
targetFps: motionBlurParams.targetFps,
|
|
116
129
|
},
|
|
117
130
|
}),
|
|
118
131
|
...(screenSpaceReflection === undefined
|
package/src/extract/contracts.ts
CHANGED
|
@@ -394,6 +394,8 @@ export interface ExtractedVolumetricFog {
|
|
|
394
394
|
readonly generation?: number;
|
|
395
395
|
readonly digest?: string;
|
|
396
396
|
readonly worldId?: number;
|
|
397
|
+
/** Canonical simulation time projected from the owning World. */
|
|
398
|
+
readonly worldTimeSeconds?: number;
|
|
397
399
|
readonly lightEntity?: EntityHandle;
|
|
398
400
|
readonly lightKind?: 'directional' | 'point' | 'spot';
|
|
399
401
|
readonly pointLightEntity?: EntityHandle;
|
|
@@ -530,6 +532,9 @@ export type RenderableReactiveReason =
|
|
|
530
532
|
export interface RenderableTemporalSnapshot {
|
|
531
533
|
readonly previousEpoch: number | undefined;
|
|
532
534
|
readonly previousSource: RenderablePreviousSource;
|
|
535
|
+
/** True only when current and previous geometry belong to one stable source identity. */
|
|
536
|
+
readonly motionValid: boolean;
|
|
537
|
+
/** Color-history invalidation is independent from motion validity. */
|
|
533
538
|
readonly reactive: boolean;
|
|
534
539
|
readonly reactiveReasons: readonly RenderableReactiveReason[];
|
|
535
540
|
readonly previousTransform: TransformSnapshot;
|
|
@@ -563,6 +568,8 @@ export interface GpuDrivenDrawSnapshot {
|
|
|
563
568
|
export interface InstancesSnapshot {
|
|
564
569
|
/** Packed column-major mat4 transforms (16 f32 per instance). */
|
|
565
570
|
readonly transforms: Float32Array;
|
|
571
|
+
/** Optional unique identity generations; tokens travel with instances through reorder. */
|
|
572
|
+
readonly generations?: Uint32Array;
|
|
566
573
|
/** Number of instances (transforms.length / 16). */
|
|
567
574
|
readonly instanceCount: number;
|
|
568
575
|
/** Stable per-entity GPU buffer cache key (the packed Entity u32). */
|
|
@@ -39,6 +39,8 @@ export interface AtmosphereParameters {
|
|
|
39
39
|
readonly mieCoefficient: number;
|
|
40
40
|
readonly mieDirectionalG: number;
|
|
41
41
|
readonly sunAngularRadius: number;
|
|
42
|
+
readonly circumsolarStrength: number;
|
|
43
|
+
readonly circumsolarWidth: number;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/** Shared CPU contract for values accepted by the atmosphere shaders. */
|
|
@@ -50,6 +52,8 @@ export const ATMOSPHERE_PARAMETER_RANGES: Readonly<
|
|
|
50
52
|
mieCoefficient: Object.freeze({ min: 0, max: Number.POSITIVE_INFINITY }),
|
|
51
53
|
mieDirectionalG: Object.freeze({ min: 0, max: 0.999 }),
|
|
52
54
|
sunAngularRadius: Object.freeze({ min: 0, max: Number.POSITIVE_INFINITY }),
|
|
55
|
+
circumsolarStrength: Object.freeze({ min: 0, max: 4 }),
|
|
56
|
+
circumsolarWidth: Object.freeze({ min: 0.25, max: 4 }),
|
|
53
57
|
});
|
|
54
58
|
|
|
55
59
|
export interface FogCandidate {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { RenderError } from '../errors/render';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recoverable feature preparation failures are still useful diagnostics, but
|
|
5
|
+
* they are not new failures on every render tick. Keep the reporting policy
|
|
6
|
+
* independent from the frame orchestration so it can be exercised without a
|
|
7
|
+
* renderer or device.
|
|
8
|
+
*/
|
|
9
|
+
export const PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES = 60;
|
|
10
|
+
|
|
11
|
+
export function shouldReportPendingRenderFeatureError(
|
|
12
|
+
lastReportedFrame: Map<string, number>,
|
|
13
|
+
key: string,
|
|
14
|
+
frameNumber: number,
|
|
15
|
+
intervalFrames = PENDING_RENDER_FEATURE_ERROR_REPORT_INTERVAL_FRAMES,
|
|
16
|
+
): boolean {
|
|
17
|
+
const lastReported = lastReportedFrame.get(key);
|
|
18
|
+
if (lastReported !== undefined && frameNumber - lastReported < intervalFrames) return false;
|
|
19
|
+
lastReportedFrame.set(key, frameNumber);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function reportPendingRenderFeatureErrors(
|
|
24
|
+
errors: readonly RenderError[],
|
|
25
|
+
frameNumber: number,
|
|
26
|
+
lastReportedFrame: Map<string, number>,
|
|
27
|
+
errorRegistry: { fire(error: RenderError): void },
|
|
28
|
+
): void {
|
|
29
|
+
for (const error of errors) {
|
|
30
|
+
if (!isPendingRenderFeaturePreparation(error)) {
|
|
31
|
+
errorRegistry.fire(error);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// A missing prepared resource during its normal asynchronous warm-up is
|
|
35
|
+
// an expected candidate state. Keep that path silent; other next-frame
|
|
36
|
+
// failures still receive first/periodic reporting.
|
|
37
|
+
if (isSilentRenderFeatureWarmup(error)) continue;
|
|
38
|
+
const key = pendingRenderFeatureErrorKey(error);
|
|
39
|
+
if (shouldReportPendingRenderFeatureError(lastReportedFrame, key, frameNumber)) {
|
|
40
|
+
errorRegistry.fire(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isPendingRenderFeaturePreparation(error: RenderError): boolean {
|
|
46
|
+
if (!('detail' in error)) return false;
|
|
47
|
+
switch (error.code) {
|
|
48
|
+
case 'render-feature-stage-failed':
|
|
49
|
+
case 'render-feature-preparation-failed':
|
|
50
|
+
case 'render-feature-prepared-state-mismatch':
|
|
51
|
+
case 'render-feature-draw-recording-failed':
|
|
52
|
+
return error.detail.recovery === 'next-frame';
|
|
53
|
+
default:
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isSilentRenderFeatureWarmup(error: RenderError): boolean {
|
|
59
|
+
return (
|
|
60
|
+
error.code === 'render-feature-preparation-failed' &&
|
|
61
|
+
error.detail.reason.startsWith('rhi-not-available:')
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pendingRenderFeatureErrorKey(error: RenderError): string {
|
|
66
|
+
const detail = 'detail' in error ? error.detail : undefined;
|
|
67
|
+
const featureIdentity =
|
|
68
|
+
detail !== undefined &&
|
|
69
|
+
'featureIdentity' in detail &&
|
|
70
|
+
typeof detail.featureIdentity === 'string'
|
|
71
|
+
? detail.featureIdentity
|
|
72
|
+
: '';
|
|
73
|
+
const order =
|
|
74
|
+
detail !== undefined && 'order' in detail && typeof detail.order === 'number'
|
|
75
|
+
? detail.order
|
|
76
|
+
: -1;
|
|
77
|
+
return `${error.code}\0${featureIdentity}\0${order}\0${error.expected}\0${error.hint}`;
|
|
78
|
+
}
|
package/src/features/host.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
PreparedGraphicsResolver,
|
|
18
18
|
PreparedGraphicsResourceLease,
|
|
19
19
|
} from '../prepare/prepared-graphics-resolver';
|
|
20
|
-
import type
|
|
20
|
+
import { isSceneDataTarget, type SceneDataTarget } from '../temporal/scene-data';
|
|
21
21
|
import { createSceneDataCatalog, type SceneDataCatalog } from '../temporal/scene-data-catalog';
|
|
22
22
|
import {
|
|
23
23
|
cloneRenderFeaturePlanSignatureSnapshot,
|
|
@@ -134,6 +134,8 @@ export interface RenderFeatureFrameInput {
|
|
|
134
134
|
readonly worlds: readonly import('@forgeax/engine-ecs').World[];
|
|
135
135
|
readonly owner: number;
|
|
136
136
|
readonly frameNumber: number;
|
|
137
|
+
/** Physical render extent for bounded feature dispatch sizing. */
|
|
138
|
+
readonly frameSize?: { readonly width: number; readonly height: number };
|
|
137
139
|
readonly visibilitySnapshots?: readonly RenderFeatureWorldVisibilitySnapshot[];
|
|
138
140
|
readonly hiddenEntityReports?: readonly RenderFeatureHiddenEntityReport[];
|
|
139
141
|
/** Active-pipeline logical targets available to producer-owned features. */
|
|
@@ -412,7 +414,7 @@ function asFeatureError(
|
|
|
412
414
|
if (error instanceof Error && typeof (error as Partial<RenderError>).code === 'string') {
|
|
413
415
|
return error as RenderError;
|
|
414
416
|
}
|
|
415
|
-
return new RenderFeatureStageFailedError(identity, order, stage, 'next-frame');
|
|
417
|
+
return new RenderFeatureStageFailedError(identity, order, stage, 'next-frame', error);
|
|
416
418
|
}
|
|
417
419
|
|
|
418
420
|
function featureErrorForSlot(slot: FeatureSlot, error: RenderError): RenderError {
|
|
@@ -431,6 +433,7 @@ function featureErrorForSlot(slot: FeatureSlot, error: RenderError): RenderError
|
|
|
431
433
|
slot.order,
|
|
432
434
|
'plan',
|
|
433
435
|
'next-frame',
|
|
436
|
+
error,
|
|
434
437
|
);
|
|
435
438
|
default:
|
|
436
439
|
return new RenderFeatureStageFailedError(
|
|
@@ -438,6 +441,7 @@ function featureErrorForSlot(slot: FeatureSlot, error: RenderError): RenderError
|
|
|
438
441
|
slot.order,
|
|
439
442
|
'plan',
|
|
440
443
|
'next-frame',
|
|
444
|
+
error,
|
|
441
445
|
);
|
|
442
446
|
}
|
|
443
447
|
}
|
|
@@ -467,6 +471,7 @@ function errorDescriptor(error: RenderError): RenderFeatureErrorDescriptor {
|
|
|
467
471
|
order: -1,
|
|
468
472
|
stage: 'plan',
|
|
469
473
|
recovery: 'next-frame',
|
|
474
|
+
cause: error,
|
|
470
475
|
},
|
|
471
476
|
};
|
|
472
477
|
}
|
|
@@ -606,14 +611,17 @@ function preparePlanResources(
|
|
|
606
611
|
: targetHandle(resource.logicalTarget, targets);
|
|
607
612
|
if (
|
|
608
613
|
resource.logicalTarget !== undefined &&
|
|
609
|
-
(logical === undefined ||
|
|
614
|
+
(logical === undefined ||
|
|
615
|
+
(typeof logical !== 'string' &&
|
|
616
|
+
!isRenderFeatureTargetHandle(logical) &&
|
|
617
|
+
!isSceneDataTarget(logical)))
|
|
610
618
|
) {
|
|
611
619
|
return err(planFailure(slot));
|
|
612
620
|
}
|
|
613
621
|
const prepared = gpu.prepareTextureView(
|
|
614
622
|
resource.name,
|
|
615
|
-
resource.resource.value as TextureView,
|
|
616
|
-
logical === undefined
|
|
623
|
+
resource.resource.value as TextureView | undefined,
|
|
624
|
+
logical === undefined ? undefined : logical,
|
|
617
625
|
);
|
|
618
626
|
if (!prepared.ok) return prepared;
|
|
619
627
|
preparedGpuResources.set(resource.name, {
|
|
@@ -1429,7 +1437,10 @@ export function runRenderFeatureFrame(
|
|
|
1429
1437
|
() =>
|
|
1430
1438
|
feature.plan(extracted.value, {
|
|
1431
1439
|
caps: input.caps,
|
|
1432
|
-
frame: {
|
|
1440
|
+
frame: {
|
|
1441
|
+
frameNumber: input.frameNumber,
|
|
1442
|
+
...(input.frameSize === undefined ? {} : input.frameSize),
|
|
1443
|
+
},
|
|
1433
1444
|
generation: transaction.generation,
|
|
1434
1445
|
targets,
|
|
1435
1446
|
sceneData,
|
|
@@ -1499,9 +1510,9 @@ export function runRenderFeatureFrame(
|
|
|
1499
1510
|
resolver === undefined
|
|
1500
1511
|
? undefined
|
|
1501
1512
|
: (descriptor: import('./prepared-graphics').RenderFeatureGraphicsPassDescriptor) => {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
return
|
|
1513
|
+
// A pending pass can be skipped while earlier passes remain usable.
|
|
1514
|
+
// Keep their shared resolver leases alive until submission retires them.
|
|
1515
|
+
return resolveGraphicsSnapshot(resolver, descriptor, transaction.generation);
|
|
1505
1516
|
};
|
|
1506
1517
|
const releaseResolver = (): void => {
|
|
1507
1518
|
if (resolver !== undefined) resolver.release();
|
|
@@ -1529,9 +1540,11 @@ export function runRenderFeatureFrame(
|
|
|
1529
1540
|
host.setStatus(identity, 'failed', slot.latestError);
|
|
1530
1541
|
continue;
|
|
1531
1542
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1543
|
+
// Include partial allocations from skipped pending passes, even when no pass
|
|
1544
|
+
// completed. Fatal projection failures release the resolver above instead.
|
|
1545
|
+
const graphicsLeases =
|
|
1546
|
+
resolver?.leases ??
|
|
1547
|
+
projected.value.passes.flatMap((pass) => pass.resolvedGraphics?.leases ?? []);
|
|
1535
1548
|
if (graphicsLeases.length > 0) requiresPreparedResourceKey = true;
|
|
1536
1549
|
const leases = [...new Set(graphicsLeases), ...(gpu?.retireUntouched() ?? [])];
|
|
1537
1550
|
const retained = host.retainPreparedGraphics(identity, leases);
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { SCENE_DATA_TEMPORAL_V1_SCHEMA } from '../../../temporal/scene-data';
|
|
3
3
|
import { createSceneDataCatalog } from '../../../temporal/scene-data-catalog';
|
|
4
4
|
import { freezeRenderFeaturePlan } from '../../plan';
|
|
5
|
-
import { planMotionBlur } from '../motion-blur-feature';
|
|
5
|
+
import { MOTION_BLUR_COMPUTE_WGSL, planMotionBlur } from '../motion-blur-feature';
|
|
6
6
|
import { DEFAULT_MOTION_BLUR_PARAMS } from '../motion-blur-params';
|
|
7
7
|
|
|
8
8
|
const caps = {
|
|
@@ -64,4 +64,114 @@ describe('Motion Blur feature plan', () => {
|
|
|
64
64
|
expect(pass.sampledTargets).not.toContain('motion-depth');
|
|
65
65
|
expect(catalog.schema).toBe(SCENE_DATA_TEMPORAL_V1_SCHEMA);
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
it('selects one tile summary and one fused reconstruction on compute-capable targets', () => {
|
|
69
|
+
const computeCaps = {
|
|
70
|
+
rgba16floatRenderable: true,
|
|
71
|
+
compute: true,
|
|
72
|
+
storageBuffer: true,
|
|
73
|
+
storageTexture: true,
|
|
74
|
+
} as never;
|
|
75
|
+
const catalog = createSceneDataCatalog({
|
|
76
|
+
featureIdentity: 'forgeax.motion-blur',
|
|
77
|
+
generation: 4,
|
|
78
|
+
planIdentity: 'forgeax.motion-blur:4',
|
|
79
|
+
rgba16floatRenderable: true,
|
|
80
|
+
});
|
|
81
|
+
const targets = [
|
|
82
|
+
{
|
|
83
|
+
name: 'motion-input',
|
|
84
|
+
kind: 'color' as const,
|
|
85
|
+
format: 'rgba16float' as const,
|
|
86
|
+
sampleCount: 1 as const,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'motion-output',
|
|
90
|
+
kind: 'color' as const,
|
|
91
|
+
format: 'rgba16float' as const,
|
|
92
|
+
sampleCount: 1 as const,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
const planned = planMotionBlur(
|
|
96
|
+
DEFAULT_MOTION_BLUR_PARAMS,
|
|
97
|
+
{
|
|
98
|
+
caps: computeCaps,
|
|
99
|
+
frame: { frameNumber: 7, width: 1920, height: 1080 },
|
|
100
|
+
generation: 4,
|
|
101
|
+
targets,
|
|
102
|
+
sceneData: catalog,
|
|
103
|
+
},
|
|
104
|
+
{ frameDeltaSeconds: 1 / 60, reset: false },
|
|
105
|
+
);
|
|
106
|
+
expect(planned.ok).toBe(true);
|
|
107
|
+
if (!planned.ok) return;
|
|
108
|
+
const frozen = freezeRenderFeaturePlan('forgeax.motion-blur', planned.value, targets);
|
|
109
|
+
expect(frozen.ok).toBe(true);
|
|
110
|
+
if (!frozen.ok) return;
|
|
111
|
+
|
|
112
|
+
const computeProgram = frozen.value.resources.find(
|
|
113
|
+
(resource) => resource.kind === 'compute-program',
|
|
114
|
+
);
|
|
115
|
+
const tileSummary = frozen.value.resources.find(
|
|
116
|
+
(resource) => resource.kind === 'buffer' && resource.name === 'motion-blur-tile-summary',
|
|
117
|
+
);
|
|
118
|
+
expect(computeProgram?.kind).toBe('compute-program');
|
|
119
|
+
expect(tileSummary).toMatchObject({
|
|
120
|
+
kind: 'buffer',
|
|
121
|
+
size: Math.ceil(1920 / 16) * Math.ceil(1080 / 16) * 32,
|
|
122
|
+
usage: ['storage'],
|
|
123
|
+
});
|
|
124
|
+
expect(frozen.value.passes.map((pass) => pass.name)).toEqual([
|
|
125
|
+
'motion-blur-tile-summary',
|
|
126
|
+
'motion-blur',
|
|
127
|
+
]);
|
|
128
|
+
expect(frozen.value.passes).toHaveLength(2);
|
|
129
|
+
expect(frozen.value.passes[0]).toMatchObject({
|
|
130
|
+
kind: 'compute',
|
|
131
|
+
dispatches: [{ entryPoint: 'tile_summary', workgroups: [120, 68, 1] }],
|
|
132
|
+
});
|
|
133
|
+
expect(frozen.value.passes[1]).toMatchObject({
|
|
134
|
+
kind: 'compute',
|
|
135
|
+
dispatches: [{ entryPoint: 'reconstruct', workgroups: [240, 135, 1] }],
|
|
136
|
+
});
|
|
137
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('2.0 * (f32(index) + 0.5) / denominator - 1.0');
|
|
138
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let sourcePixel = workgroupBase');
|
|
139
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('halfFilteredColor');
|
|
140
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('workgroupBarrier()');
|
|
141
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('if (!inBounds) {');
|
|
142
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('depthReject');
|
|
143
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('sourceVelocitySegmentCovers');
|
|
144
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('TILE_NEIGHBORHOOD_COUNT');
|
|
145
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('tileIntersectsWorkgroupSupport');
|
|
146
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('tileNeighborhoodOffset');
|
|
147
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('const EDGE_FILL_FACTOR : f32 = 0.99;');
|
|
148
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let acceptedColor');
|
|
149
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let acceptedAlpha');
|
|
150
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let emptyReceiver');
|
|
151
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let sourceScale');
|
|
152
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let sourceContribution');
|
|
153
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let trailScale');
|
|
154
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).toContain('let outputAlpha');
|
|
155
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).not.toContain('TILE_CANDIDATE_OFFSETS');
|
|
156
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).not.toContain('atomic');
|
|
157
|
+
expect(MOTION_BLUR_COMPUTE_WGSL).not.toContain('textureSampleLevel');
|
|
158
|
+
if (computeProgram?.kind === 'compute-program') {
|
|
159
|
+
const entries = computeProgram.program.bindings?.[0]?.entries ?? [];
|
|
160
|
+
expect(entries.find((entry) => entry.binding === 2)?.buffer?.type).toBe('storage');
|
|
161
|
+
expect(entries.find((entry) => entry.binding === 3)?.storageTexture?.access).toBe(
|
|
162
|
+
'write-only',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('compiles the compute lane WGSL before it reaches a backend', async () => {
|
|
168
|
+
const { compileShader } = await import(
|
|
169
|
+
/* @vite-ignore */
|
|
170
|
+
new URL('../../../../../shader-compiler/dist/index.mjs', import.meta.url).href
|
|
171
|
+
);
|
|
172
|
+
const compiled = await compileShader(MOTION_BLUR_COMPUTE_WGSL, {
|
|
173
|
+
id: 'forgeax::motion-blur-compute',
|
|
174
|
+
});
|
|
175
|
+
expect(compiled.ok, compiled.ok ? undefined : String(compiled.error)).toBe(true);
|
|
176
|
+
});
|
|
67
177
|
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_MOTION_BLUR_PARAMS,
|
|
4
|
+
effectiveMotionBlurSampleCount,
|
|
5
|
+
isMotionBlurIntervalValid,
|
|
6
|
+
motionBlurExposureScale,
|
|
7
|
+
motionBlurSampleDelta,
|
|
4
8
|
motionBlurTemporalDemand,
|
|
5
9
|
validateMotionBlurParams,
|
|
6
10
|
} from '../motion-blur-params';
|
|
@@ -12,6 +16,7 @@ describe('Motion Blur parameter validation', () => {
|
|
|
12
16
|
shutterAngle: 180,
|
|
13
17
|
maxRadiusPixels: 32,
|
|
14
18
|
sampleCount: 8,
|
|
19
|
+
targetFps: 60,
|
|
15
20
|
});
|
|
16
21
|
});
|
|
17
22
|
|
|
@@ -32,6 +37,9 @@ describe('Motion Blur parameter validation', () => {
|
|
|
32
37
|
['sampleCount', 3],
|
|
33
38
|
['sampleCount', 17],
|
|
34
39
|
['sampleCount', 8.5],
|
|
40
|
+
['targetFps', -1],
|
|
41
|
+
['targetFps', 240.5],
|
|
42
|
+
['targetFps', 59.5],
|
|
35
43
|
] as const)('returns structured failure for %s=%s', (field, value) => {
|
|
36
44
|
const result = validateMotionBlurParams({ [field]: value });
|
|
37
45
|
expect(result.ok).toBe(false);
|
|
@@ -45,9 +53,56 @@ describe('Motion Blur parameter validation', () => {
|
|
|
45
53
|
|
|
46
54
|
it('does no temporal work when omitted or shutter is zero', () => {
|
|
47
55
|
expect(motionBlurTemporalDemand(undefined)).toBe(false);
|
|
48
|
-
expect(
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
expect(
|
|
57
|
+
motionBlurTemporalDemand({
|
|
58
|
+
shutterAngle: 0,
|
|
59
|
+
maxRadiusPixels: 32,
|
|
60
|
+
sampleCount: 8,
|
|
61
|
+
targetFps: 60,
|
|
62
|
+
}),
|
|
63
|
+
).toBe(false);
|
|
64
|
+
expect(
|
|
65
|
+
motionBlurTemporalDemand({
|
|
66
|
+
shutterAngle: 180,
|
|
67
|
+
maxRadiusPixels: 0,
|
|
68
|
+
sampleCount: 8,
|
|
69
|
+
targetFps: 60,
|
|
70
|
+
}),
|
|
71
|
+
).toBe(false);
|
|
51
72
|
expect(motionBlurTemporalDemand(DEFAULT_MOTION_BLUR_PARAMS)).toBe(true);
|
|
52
73
|
});
|
|
74
|
+
|
|
75
|
+
it('rounds authored sample budgets down to the fixed shader tiers', () => {
|
|
76
|
+
expect(effectiveMotionBlurSampleCount(3)).toBe(0);
|
|
77
|
+
expect(effectiveMotionBlurSampleCount(4)).toBe(4);
|
|
78
|
+
expect(effectiveMotionBlurSampleCount(7)).toBe(4);
|
|
79
|
+
expect(effectiveMotionBlurSampleCount(8)).toBe(8);
|
|
80
|
+
expect(effectiveMotionBlurSampleCount(15)).toBe(8);
|
|
81
|
+
expect(effectiveMotionBlurSampleCount(16)).toBe(16);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects unstable intervals and scales displacement to the target rate', () => {
|
|
85
|
+
expect(isMotionBlurIntervalValid(1 / 60)).toBe(true);
|
|
86
|
+
expect(isMotionBlurIntervalValid(0)).toBe(false);
|
|
87
|
+
expect(isMotionBlurIntervalValid(0.101)).toBe(false);
|
|
88
|
+
expect(motionBlurExposureScale(1 / 60, 60)).toBeCloseTo(1);
|
|
89
|
+
expect(motionBlurExposureScale(1 / 120, 60)).toBeCloseTo(2);
|
|
90
|
+
expect(motionBlurExposureScale(1 / 60, 0)).toBe(1);
|
|
91
|
+
expect(motionBlurExposureScale(0, 60)).toBe(0);
|
|
92
|
+
expect(motionBlurExposureScale(1 / 60, 60.5)).toBe(0);
|
|
93
|
+
expect(motionBlurExposureScale(1 / 60, 241)).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('keeps raw render hitches visible before ECS clamp policy', () => {
|
|
97
|
+
expect(motionBlurSampleDelta(1.2, 1, 0.1)).toBeCloseTo(0.2);
|
|
98
|
+
expect(motionBlurSampleDelta(1 / 30, 0, 0.1)).toBeCloseTo(1 / 30);
|
|
99
|
+
expect(isMotionBlurIntervalValid(motionBlurSampleDelta(1.2, 1, 0.1))).toBe(false);
|
|
100
|
+
expect(isMotionBlurIntervalValid(motionBlurSampleDelta(1 / 30, 0, 0.1))).toBe(true);
|
|
101
|
+
expect(motionBlurSampleDelta(undefined, undefined, 0.1)).toBe(0.1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('fails closed for an explicitly invalid host/replay sample', () => {
|
|
105
|
+
expect(Number.isNaN(motionBlurSampleDelta(Number.NaN, 1, 1 / 60))).toBe(true);
|
|
106
|
+
expect(Number.isNaN(motionBlurSampleDelta(1, Number.POSITIVE_INFINITY, 1 / 60))).toBe(true);
|
|
107
|
+
});
|
|
53
108
|
});
|