@forgeax/engine-render 0.1.23 → 0.1.24
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 +131 -9
- package/dist/assembly/bundler-contract.d.ts +9 -0
- package/dist/assembly/bundler-contract.d.ts.map +1 -0
- package/dist/assembly/device-feature-admission.d.ts +8 -0
- package/dist/assembly/device-feature-admission.d.ts.map +1 -1
- package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
- package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +7 -243
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +3 -2
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
- package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +168 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -0
- package/dist/assembly/material-shader-prewarm.d.ts +21 -0
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
- package/dist/assembly/recovery/generation.d.ts +46 -0
- package/dist/assembly/recovery/generation.d.ts.map +1 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
- package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts +10 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -0
- package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-helpers.d.ts +19 -0
- package/dist/assembly/renderer-helpers.d.ts.map +1 -0
- package/dist/assembly/renderer-lifecycle.d.ts +34 -0
- package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
- package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
- package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +55 -0
- package/dist/assembly/webgpu-ready.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +152 -0
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
- package/dist/chunk-GE3SDD72.mjs.map +1 -0
- package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
- package/dist/chunk-KZFTK6DO.mjs.map +1 -0
- package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
- package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
- package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
- package/dist/chunk-RR5L4SRS.mjs.map +1 -0
- package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
- package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
- package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
- package/dist/chunk-UQYS7FRM.mjs.map +1 -0
- package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
- package/dist/chunk-YKB5DQV7.mjs.map +1 -0
- package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
- package/dist/chunk-YRUYGA4A.mjs.map +1 -0
- package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
- package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
- package/dist/construct-renderer.mjs +19841 -16190
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/device-scope.d.ts +2 -0
- package/dist/device/device-scope.d.ts.map +1 -1
- package/dist/device/gpu-residency.d.ts +24 -4
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/frame.d.ts +5 -0
- package/dist/environment/frame.d.ts.map +1 -1
- package/dist/environment/inspection.d.ts +7 -0
- package/dist/environment/inspection.d.ts.map +1 -1
- package/dist/environment/lifecycle.d.ts +20 -2
- package/dist/environment/lifecycle.d.ts.map +1 -1
- package/dist/errors/recover.d.ts +58 -2
- package/dist/errors/recover.d.ts.map +1 -1
- package/dist/errors/render.d.ts +13 -0
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +899 -0
- package/dist/extract/contracts.d.ts.map +1 -0
- package/dist/features/host.d.ts +2 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +3 -0
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/prepared-graphics-store.d.ts +2 -0
- package/dist/features/prepared-graphics-store.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +28 -0
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +1 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +7 -6
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +7 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/internal.mjs +6 -6
- package/dist/light-buffer-layout.d.ts +3 -0
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/materials.d.ts +34 -10
- package/dist/materials.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +56 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/occlusion-proxy.d.ts +4 -0
- package/dist/occlusion-proxy.d.ts.map +1 -0
- package/dist/pbr-pipeline.d.ts +32 -9
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/points-lines/prepare.d.ts +3 -0
- package/dist/points-lines/prepare.d.ts.map +1 -1
- package/dist/points-lines/standard-owner.d.ts +33 -0
- package/dist/points-lines/standard-owner.d.ts.map +1 -0
- package/dist/record/frame-snapshot.d.ts +5 -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 +14 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +14 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +7 -17
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +11 -3
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/recovery-pipeline.d.ts +30 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -0
- package/dist/record/render-context.d.ts +26 -4
- package/dist/record/render-context.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 +65 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -0
- package/dist/render-contract.d.ts +64 -7
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +12 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -0
- package/dist/render-system-extract.d.ts +156 -22
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +128 -4
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +19 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +6 -6
- package/dist/temporal/taa-history-store.d.ts +15 -1
- package/dist/temporal/taa-history-store.d.ts.map +1 -1
- package/dist/temporal/transaction.d.ts +5 -0
- package/dist/temporal/transaction.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
- package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
- package/src/__tests__/environment-generation.integration.test.ts +23 -0
- package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
- package/src/__tests__/factory-contract.integration.test.ts +122 -7
- package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
- package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
- package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
- package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
- package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
- package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
- package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
- package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +85 -18
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
- package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
- package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
- package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
- package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
- package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
- package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
- package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
- package/src/__tests__/shader-manifest-fixture.ts +13 -16
- package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
- package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
- package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
- package/src/assembly/bundler-contract.ts +9 -0
- package/src/assembly/device-feature-admission.ts +15 -5
- package/src/assembly/extended-lighting-fallback.ts +216 -0
- package/src/assembly/factory.ts +81 -10088
- package/src/assembly/material/assembly.ts +18 -2
- package/src/assembly/material/standard-layer-projection.ts +16 -0
- package/src/assembly/material-shader-policy.ts +854 -0
- package/src/assembly/material-shader-prewarm.ts +88 -0
- package/src/assembly/mesh-ssbo-grow.ts +270 -0
- package/src/assembly/recovery/device-loss-fanout.ts +89 -0
- package/src/assembly/recovery/generation.ts +141 -0
- package/src/assembly/recovery/recovery-attempt.ts +800 -0
- package/src/assembly/recovery/renderer-recover.ts +906 -0
- package/src/assembly/renderer-facade.ts +342 -0
- package/src/assembly/renderer-frame-transaction.ts +45 -0
- package/src/assembly/renderer-helpers.ts +99 -0
- package/src/assembly/renderer-lifecycle.ts +159 -0
- package/src/assembly/shader-prewarm-policy.ts +157 -0
- package/src/assembly/webgpu-ready-contract.ts +14 -0
- package/src/assembly/webgpu-ready.ts +4079 -0
- package/src/assembly/webgpu-renderer.ts +4095 -0
- package/src/components/camera.ts +1 -1
- package/src/debug-draw-glue.ts +1 -1
- package/src/device/device-scope.ts +35 -6
- package/src/device/gpu-residency.ts +194 -26
- package/src/environment/frame.ts +6 -0
- package/src/environment/inspection.ts +10 -0
- package/src/environment/lifecycle.ts +114 -8
- package/src/errors/recover.ts +76 -5
- package/src/errors/render.ts +17 -0
- package/src/extract/contracts.ts +974 -0
- package/src/features/host.ts +16 -0
- package/src/features/prepared-gpu-work.ts +20 -2
- package/src/features/prepared-graphics-store.ts +17 -0
- package/src/gpu-driven/production-raster.ts +97 -2
- package/src/gpu-scene.ts +3 -2
- package/src/ibl/IblPipelineCache.ts +134 -50
- package/src/ibl/skylight-bind-group.ts +26 -0
- package/src/index.ts +9 -1
- package/src/light-buffer-layout.ts +3 -0
- package/src/materials.ts +262 -45
- package/src/mesh-material-bindings.ts +59 -0
- package/src/occlusion-proxy.ts +64 -0
- package/src/pbr-pipeline.ts +139 -15
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
- package/src/points-lines/prepare.ts +9 -0
- package/src/points-lines/standard-owner.ts +403 -0
- package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
- package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
- package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
- package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
- package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
- package/src/record/frame-snapshot.ts +15 -0
- package/src/record/frame-targets.ts +11 -1
- package/src/record/frame.ts +6 -2
- package/src/record/helpers.ts +1 -1
- package/src/record/main-pass-geometry.ts +230 -5
- package/src/record/main-pass-material.ts +316 -209
- package/src/record/main-pass-sprite-draws.ts +6 -7
- package/src/record/main-pass.ts +94 -6
- package/src/record/mesh-ssbo.ts +32 -10
- package/src/record/recovery-pipeline.ts +695 -0
- package/src/record/render-context.ts +35 -4
- package/src/record/typed-frame-graph.ts +45 -1
- package/src/record/view-ubo.ts +3 -3
- package/src/recovery/render-system-candidate.ts +839 -0
- package/src/render-contract.ts +65 -7
- package/src/render-system-extract-tail.ts +2983 -0
- package/src/render-system-extract.ts +124 -2977
- package/src/render-system.ts +868 -659
- package/src/scene/render-scene.ts +124 -1
- package/src/temporal/frame-coordinator.ts +4 -1
- package/src/temporal/taa-history-store.ts +46 -2
- package/src/temporal/temporal-view.ts +1 -1
- package/src/temporal/transaction.ts +9 -0
- package/src/tilemap-chunk-extract-system.ts +1 -1
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
- package/dist/chunk-4L3P6DT5.mjs.map +0 -1
- package/dist/chunk-4NPUASZR.mjs.map +0 -1
- package/dist/chunk-DIYW3M5D.mjs.map +0 -1
- package/dist/chunk-FBC4R44J.mjs.map +0 -1
- package/dist/chunk-HZUPSYW7.mjs.map +0 -1
- package/dist/chunk-QODA5UKU.mjs.map +0 -1
- package/dist/chunk-YALPS44A.mjs.map +0 -1
- package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// Public renderer facade and profile validation.
|
|
2
|
+
// This module narrows the lifetime-heavy host implementation to the stable
|
|
3
|
+
// lease/receipt contract and owns the profile/error projection helpers.
|
|
4
|
+
|
|
5
|
+
import { err, ok, type RhiError } from '@forgeax/engine-rhi';
|
|
6
|
+
import { type RecoverFailure, RecoveryFailedError } from '../errors/recover';
|
|
7
|
+
import {
|
|
8
|
+
type RenderError,
|
|
9
|
+
RendererContractFailureError,
|
|
10
|
+
type RendererOperationCause,
|
|
11
|
+
RendererOperationError,
|
|
12
|
+
} from '../errors/render';
|
|
13
|
+
import {
|
|
14
|
+
STANDARD_LIGHT_COUNTS,
|
|
15
|
+
STANDARD_PIPELINE_ID,
|
|
16
|
+
STANDARD_POST_STAGE_NAMES,
|
|
17
|
+
} from '../pipeline/standard-profile';
|
|
18
|
+
import type {
|
|
19
|
+
Renderer,
|
|
20
|
+
RendererError,
|
|
21
|
+
RendererEventListener,
|
|
22
|
+
RendererState,
|
|
23
|
+
RenderInspection,
|
|
24
|
+
RenderProfile,
|
|
25
|
+
RenderResult,
|
|
26
|
+
} from '../render-contract';
|
|
27
|
+
import type { RendererHostImplementation } from './host-contract';
|
|
28
|
+
import { createRendererLifecycle, isRendererStateOperational } from './renderer-lifecycle';
|
|
29
|
+
|
|
30
|
+
/** Narrow the concrete host to the lease/receipt-only public contract. */
|
|
31
|
+
export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
32
|
+
const listeners = new Set<RendererEventListener>();
|
|
33
|
+
const lifecycle = createRendererLifecycle(renderer.inspect().state);
|
|
34
|
+
let disposed = lifecycle.state() === 'disposed';
|
|
35
|
+
let recoverInFlight: Promise<RenderResult<void, RenderError>> | undefined;
|
|
36
|
+
|
|
37
|
+
const emit = (event: Parameters<RendererEventListener>[0]): void => {
|
|
38
|
+
for (const listener of listeners) {
|
|
39
|
+
try {
|
|
40
|
+
listener(event);
|
|
41
|
+
} catch (cause) {
|
|
42
|
+
console.error('[Renderer.subscribe] listener threw:', cause);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const transition = (next: RendererState): void => {
|
|
47
|
+
const previous = lifecycle.state();
|
|
48
|
+
if (previous === next || !lifecycle.transition(next)) return;
|
|
49
|
+
emit(Object.freeze({ kind: 'state-changed', previous, current: next }));
|
|
50
|
+
};
|
|
51
|
+
const eventError = (cause: RendererError): RenderError =>
|
|
52
|
+
cause instanceof RendererOperationError
|
|
53
|
+
? cause
|
|
54
|
+
: new RendererOperationError('device-operation-failed', {
|
|
55
|
+
operation: 'renderer-event',
|
|
56
|
+
cause: structuredRendererCause(cause, 'renderer-event'),
|
|
57
|
+
});
|
|
58
|
+
const drawError = (cause: RhiError | RenderError): RenderError => {
|
|
59
|
+
if (cause instanceof RendererOperationError) return cause;
|
|
60
|
+
if (cause instanceof RendererContractFailureError) {
|
|
61
|
+
return new RendererOperationError('device-operation-failed', {
|
|
62
|
+
operation: 'draw',
|
|
63
|
+
cause: structuredRendererCause(cause, 'draw'),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return new RendererOperationError('device-operation-failed', {
|
|
67
|
+
operation: 'draw',
|
|
68
|
+
cause: structuredRendererCause(cause, 'draw'),
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const offHostEvents = renderer.subscribeHostEvents((event) => {
|
|
72
|
+
if (event.kind === 'error') {
|
|
73
|
+
emit(Object.freeze({ kind: 'error', error: eventError(event.error) }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
transition(
|
|
77
|
+
event.health.reason === 'device-lost'
|
|
78
|
+
? 'device-lost'
|
|
79
|
+
: event.health.reason === 'internal-fault'
|
|
80
|
+
? 'faulted'
|
|
81
|
+
: 'alive',
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
attach: (world) => {
|
|
87
|
+
const result = renderer.attach(world);
|
|
88
|
+
return result.ok
|
|
89
|
+
? result
|
|
90
|
+
: err(
|
|
91
|
+
new RendererOperationError('world-lease-invalid', {
|
|
92
|
+
operation: 'attach',
|
|
93
|
+
cause: result.error,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
createRenderTarget: (descriptor) => renderer.createRenderTarget(descriptor),
|
|
98
|
+
resizeRenderTarget: (target, descriptor) => renderer.resizeRenderTarget(target, descriptor),
|
|
99
|
+
createRenderTargetTextureSource: (target, options) =>
|
|
100
|
+
renderer.createRenderTargetTextureSource(target, options),
|
|
101
|
+
requestTargetReadback: (target, request) => renderer.requestTargetReadback(target, request),
|
|
102
|
+
destroyRenderTarget: (target) => renderer.destroyRenderTarget(target),
|
|
103
|
+
setProfile: (profile) => {
|
|
104
|
+
const state = lifecycle.state();
|
|
105
|
+
if (!isRendererStateOperational(state)) {
|
|
106
|
+
return err(
|
|
107
|
+
new RendererOperationError('renderer-state-invalid', {
|
|
108
|
+
operation: 'set-profile',
|
|
109
|
+
state,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return renderer.setProfile(profile);
|
|
114
|
+
},
|
|
115
|
+
state: () => lifecycle.state(),
|
|
116
|
+
draw: (request) => {
|
|
117
|
+
const state = lifecycle.state();
|
|
118
|
+
if (!isRendererStateOperational(state)) {
|
|
119
|
+
return err(
|
|
120
|
+
new RendererOperationError('renderer-state-invalid', {
|
|
121
|
+
operation: 'draw',
|
|
122
|
+
state,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const result = renderer.drawFrame(request);
|
|
127
|
+
if (!result.ok) return err(drawError(result.error));
|
|
128
|
+
emit(
|
|
129
|
+
Object.freeze({
|
|
130
|
+
kind: 'frame-submitted',
|
|
131
|
+
frameId: result.value.frameId,
|
|
132
|
+
deviceGeneration: result.value.deviceGeneration,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
return ok(result.value);
|
|
136
|
+
},
|
|
137
|
+
inspect: () => ({ ...renderer.inspect(), state: lifecycle.state() }),
|
|
138
|
+
observe: (receipt, request) => renderer.observe(receipt, request),
|
|
139
|
+
subscribe: (listener) => {
|
|
140
|
+
if (disposed) return () => undefined;
|
|
141
|
+
listeners.add(listener);
|
|
142
|
+
return () => listeners.delete(listener);
|
|
143
|
+
},
|
|
144
|
+
releaseSurface: () => {
|
|
145
|
+
const result = renderer.releaseSurface();
|
|
146
|
+
return result.ok
|
|
147
|
+
? result
|
|
148
|
+
: err(
|
|
149
|
+
new RendererOperationError('surface-unavailable', {
|
|
150
|
+
operation: 'release-surface',
|
|
151
|
+
cause: result.error,
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
restoreSurface: () => {
|
|
156
|
+
const result = renderer.restoreSurface();
|
|
157
|
+
return result.ok
|
|
158
|
+
? result
|
|
159
|
+
: err(
|
|
160
|
+
new RendererOperationError('surface-unavailable', {
|
|
161
|
+
operation: 'restore-surface',
|
|
162
|
+
cause: result.error,
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
recover: () => {
|
|
167
|
+
if (recoverInFlight !== undefined) return recoverInFlight;
|
|
168
|
+
const state = lifecycle.state();
|
|
169
|
+
if (state !== 'device-lost') {
|
|
170
|
+
return Promise.resolve(
|
|
171
|
+
err(
|
|
172
|
+
new RendererOperationError('renderer-state-invalid', {
|
|
173
|
+
operation: 'recover',
|
|
174
|
+
state,
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
transition('recovering');
|
|
180
|
+
recoverInFlight = renderer
|
|
181
|
+
.recover()
|
|
182
|
+
.then((result): RenderResult<void, RenderError> => {
|
|
183
|
+
if (!result.ok) {
|
|
184
|
+
const error = new RendererOperationError('recovery-failed', {
|
|
185
|
+
operation: 'recover',
|
|
186
|
+
...projectRecoveryFailureDetail(result.error, renderer.inspect().recovery),
|
|
187
|
+
cause: structuredRendererCause(result.error, 'recover'),
|
|
188
|
+
});
|
|
189
|
+
emit(Object.freeze({ kind: 'error', error }));
|
|
190
|
+
return err(error);
|
|
191
|
+
}
|
|
192
|
+
return ok(undefined);
|
|
193
|
+
})
|
|
194
|
+
.finally(() => {
|
|
195
|
+
transition(renderer.inspect().state);
|
|
196
|
+
recoverInFlight = undefined;
|
|
197
|
+
});
|
|
198
|
+
return recoverInFlight;
|
|
199
|
+
},
|
|
200
|
+
dispose: async () => {
|
|
201
|
+
if (disposed) return ok(undefined);
|
|
202
|
+
const result = renderer.dispose();
|
|
203
|
+
if (!result.ok) emit(Object.freeze({ kind: 'error', error: result.error }));
|
|
204
|
+
disposed = true;
|
|
205
|
+
transition('disposed');
|
|
206
|
+
offHostEvents();
|
|
207
|
+
listeners.clear();
|
|
208
|
+
return result;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── feat-20260520-directional-light-shadow-mapping M1c / w8 helpers ────────
|
|
214
|
+
|
|
215
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
export function toError(value: unknown): Error {
|
|
218
|
+
if (value instanceof Error) return value;
|
|
219
|
+
return new Error(String(value));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function structuredRendererCause(cause: unknown, operation: string): RendererOperationCause {
|
|
223
|
+
if (typeof cause === 'object' && cause !== null) {
|
|
224
|
+
const candidate = cause as Partial<RendererOperationCause>;
|
|
225
|
+
if (
|
|
226
|
+
typeof candidate.code === 'string' &&
|
|
227
|
+
typeof candidate.expected === 'string' &&
|
|
228
|
+
typeof candidate.hint === 'string'
|
|
229
|
+
) {
|
|
230
|
+
return {
|
|
231
|
+
code: candidate.code,
|
|
232
|
+
expected: candidate.expected,
|
|
233
|
+
hint: candidate.hint,
|
|
234
|
+
...(candidate.detail === undefined ? {} : { detail: candidate.detail }),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const contractFailure = new RendererContractFailureError(
|
|
239
|
+
'draw',
|
|
240
|
+
`${operation}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
241
|
+
);
|
|
242
|
+
return {
|
|
243
|
+
code: contractFailure.code,
|
|
244
|
+
expected: contractFailure.expected,
|
|
245
|
+
hint: contractFailure.hint,
|
|
246
|
+
detail: contractFailure.detail,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function projectRecoveryFailureDetail(
|
|
251
|
+
failure: RecoverFailure,
|
|
252
|
+
inspection: RenderInspection['recovery'],
|
|
253
|
+
): Omit<
|
|
254
|
+
import('../errors/render').RendererOperationDetailByCode['recovery-failed'],
|
|
255
|
+
'operation' | 'cause'
|
|
256
|
+
> {
|
|
257
|
+
if (failure instanceof RecoveryFailedError) {
|
|
258
|
+
return {
|
|
259
|
+
phase: failure.detail.phase,
|
|
260
|
+
oldGeneration: failure.detail.oldGeneration,
|
|
261
|
+
candidateGeneration: failure.detail.candidateGeneration,
|
|
262
|
+
attempt: failure.detail.attempt,
|
|
263
|
+
elapsedMs: failure.detail.elapsedMs,
|
|
264
|
+
retryable: failure.detail.retryable,
|
|
265
|
+
guidance: failure.detail.guidance,
|
|
266
|
+
owner: failure.detail.owner,
|
|
267
|
+
resourceKind: failure.detail.resourceKind,
|
|
268
|
+
lastOutcome: failure.detail.lastOutcome,
|
|
269
|
+
rehydratedRoots: failure.detail.rehydratedRoots,
|
|
270
|
+
staleLossEvents: failure.detail.staleLossEvents,
|
|
271
|
+
cleanupFailures: Object.freeze(
|
|
272
|
+
failure.detail.cleanupFailures.map((cause: unknown) =>
|
|
273
|
+
structuredRendererCause(cause, 'recovery-cleanup'),
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const lastOutcome = inspection.lastOutcome === 'disposed' ? 'disposed' : 'failed';
|
|
279
|
+
return {
|
|
280
|
+
phase: inspection.phase ?? 'cleanup',
|
|
281
|
+
oldGeneration: inspection.fromGeneration,
|
|
282
|
+
candidateGeneration: inspection.candidateGeneration,
|
|
283
|
+
attempt: inspection.attempt,
|
|
284
|
+
elapsedMs: inspection.elapsedMs,
|
|
285
|
+
retryable: false,
|
|
286
|
+
guidance: 'rebuild-renderer',
|
|
287
|
+
owner: inspection.failedOwner ?? 'renderer',
|
|
288
|
+
resourceKind: inspection.failedResourceKind ?? 'pipeline',
|
|
289
|
+
lastOutcome,
|
|
290
|
+
rehydratedRoots: inspection.rehydratedRoots,
|
|
291
|
+
staleLossEvents: inspection.staleLossEvents,
|
|
292
|
+
cleanupFailures: Object.freeze([]),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const RENDER_PROFILE_KEYS = new Set([
|
|
297
|
+
'pipelineId',
|
|
298
|
+
'lightCount',
|
|
299
|
+
'renderPath',
|
|
300
|
+
'shadows',
|
|
301
|
+
'pbr',
|
|
302
|
+
'ibl',
|
|
303
|
+
'ssao',
|
|
304
|
+
'postStages',
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
export function validateRenderProfile(profile: RenderProfile): string | undefined {
|
|
308
|
+
if (typeof profile !== 'object' || profile === null) return 'RenderProfile must be a POD object';
|
|
309
|
+
const unknownKey = Object.keys(profile).find((key) => !RENDER_PROFILE_KEYS.has(key));
|
|
310
|
+
if (unknownKey !== undefined) return `RenderProfile contains unsupported field '${unknownKey}'`;
|
|
311
|
+
if (profile.pipelineId !== STANDARD_PIPELINE_ID) {
|
|
312
|
+
return `RenderProfile.pipelineId must be '${STANDARD_PIPELINE_ID}'`;
|
|
313
|
+
}
|
|
314
|
+
if (!STANDARD_LIGHT_COUNTS.some((count) => count === profile.lightCount)) {
|
|
315
|
+
return 'RenderProfile.lightCount must be 1, 32, or 256';
|
|
316
|
+
}
|
|
317
|
+
if (profile.renderPath !== 'forward' && profile.renderPath !== 'deferred') {
|
|
318
|
+
return "RenderProfile.renderPath must be 'forward' or 'deferred'";
|
|
319
|
+
}
|
|
320
|
+
if (profile.shadows !== 'off' && profile.shadows !== 'hard' && profile.shadows !== 'filtered') {
|
|
321
|
+
return "RenderProfile.shadows must be 'off', 'hard', or 'filtered'";
|
|
322
|
+
}
|
|
323
|
+
for (const field of ['pbr', 'ibl', 'ssao'] as const) {
|
|
324
|
+
if (typeof profile[field] !== 'boolean') return `RenderProfile.${field} must be boolean`;
|
|
325
|
+
}
|
|
326
|
+
const stages = STANDARD_POST_STAGE_NAMES;
|
|
327
|
+
if (
|
|
328
|
+
!Array.isArray(profile.postStages) ||
|
|
329
|
+
profile.postStages.length !== stages.length ||
|
|
330
|
+
stages.some((stage, index) => profile.postStages[index] !== stage)
|
|
331
|
+
) {
|
|
332
|
+
return 'RenderProfile.postStages must preserve the Standard post stage order';
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function freezeRenderProfile(profile: RenderProfile): RenderProfile {
|
|
338
|
+
return Object.freeze({
|
|
339
|
+
...profile,
|
|
340
|
+
postStages: Object.freeze([...profile.postStages]) as RenderProfile['postStages'],
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -42,6 +42,45 @@ export interface RendererFrameTransactionSteps<T> {
|
|
|
42
42
|
readonly submit: (candidate: T) => RendererFrameStageResult<void>;
|
|
43
43
|
readonly commit: (candidate: T) => void;
|
|
44
44
|
readonly abort?: (failure: RendererFrameStageFailure) => void;
|
|
45
|
+
readonly generationFence?: RendererGenerationFence;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RendererGenerationFence {
|
|
49
|
+
readonly capturedGeneration: number;
|
|
50
|
+
readonly currentGeneration: () => number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ContinuationKind = 'readback' | 'query' | 'mapAsync' | 'queue-completion';
|
|
54
|
+
|
|
55
|
+
export interface ContinuationTerminationReason {
|
|
56
|
+
readonly code: 'device-lost' | 'disposed' | 'stale-generation';
|
|
57
|
+
readonly detail?: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ContinuationTerminator {
|
|
61
|
+
terminate(reason: ContinuationTerminationReason): boolean;
|
|
62
|
+
promise(): Promise<ContinuationTerminationReason>;
|
|
63
|
+
isTerminated(): boolean;
|
|
64
|
+
guard(kind: ContinuationKind): boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createContinuationTerminator(): ContinuationTerminator {
|
|
68
|
+
let termination: ContinuationTerminationReason | undefined;
|
|
69
|
+
let resolveTermination: ((reason: ContinuationTerminationReason) => void) | undefined;
|
|
70
|
+
const terminated = new Promise<ContinuationTerminationReason>((resolve) => {
|
|
71
|
+
resolveTermination = resolve;
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
terminate: (reason) => {
|
|
75
|
+
if (termination !== undefined) return false;
|
|
76
|
+
termination = Object.freeze({ ...reason });
|
|
77
|
+
resolveTermination?.(termination);
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
promise: () => terminated,
|
|
81
|
+
isTerminated: () => termination !== undefined,
|
|
82
|
+
guard: () => termination === undefined,
|
|
83
|
+
};
|
|
45
84
|
}
|
|
46
85
|
|
|
47
86
|
/**
|
|
@@ -68,6 +107,12 @@ export function executeRendererFrameTransaction<T>(
|
|
|
68
107
|
if (!executed.ok) return fail('execute');
|
|
69
108
|
const finished = steps.finish(built.value);
|
|
70
109
|
if (!finished.ok) return fail('finish');
|
|
110
|
+
if (
|
|
111
|
+
steps.generationFence !== undefined &&
|
|
112
|
+
steps.generationFence.currentGeneration() !== steps.generationFence.capturedGeneration
|
|
113
|
+
) {
|
|
114
|
+
return fail('submit');
|
|
115
|
+
}
|
|
71
116
|
const submitted = steps.submit(built.value);
|
|
72
117
|
if (!submitted.ok) return fail('submit');
|
|
73
118
|
steps.commit(built.value);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Result, RhiCanvasContext, RhiDevice } from '@forgeax/engine-rhi';
|
|
2
|
+
import { err, ok, RhiError } from '@forgeax/engine-rhi';
|
|
3
|
+
import type { RhiErrorListenerRegistry } from '../lifecycle';
|
|
4
|
+
import { configureSurface, type PipelineState } from '../render-system';
|
|
5
|
+
|
|
6
|
+
/** Minimum device-bound surface contract needed by lazy context configuration. */
|
|
7
|
+
export interface RendererSurfaceInternals {
|
|
8
|
+
readonly device: RhiDevice;
|
|
9
|
+
readonly context: RhiCanvasContext;
|
|
10
|
+
readonly pack: {
|
|
11
|
+
readonly instrumentation?: {
|
|
12
|
+
readonly resolveSurfaceDevice?: (device: RhiDevice) => Result<RhiDevice, RhiError>;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function wrapDisposeError(cause: unknown, step: string): RhiError {
|
|
18
|
+
if (cause instanceof RhiError) return cause;
|
|
19
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
20
|
+
return new RhiError({
|
|
21
|
+
code: 'webgpu-runtime-error',
|
|
22
|
+
expected: `${step} completes during Renderer.dispose() without throwing`,
|
|
23
|
+
hint: `dispose sub-step '${step}' threw: ${message}; cascade continues to subsequent steps`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ensureContextConfigured(
|
|
28
|
+
internals: RendererSurfaceInternals,
|
|
29
|
+
state: PipelineState,
|
|
30
|
+
errorRegistry: RhiErrorListenerRegistry,
|
|
31
|
+
context: RhiCanvasContext = internals.context,
|
|
32
|
+
device: RhiDevice = internals.device,
|
|
33
|
+
): Result<void, RhiError> {
|
|
34
|
+
if (state.perPassResources.configured) return ok(undefined);
|
|
35
|
+
const configuredDevice = internals.pack.instrumentation?.resolveSurfaceDevice?.(device);
|
|
36
|
+
if (configuredDevice !== undefined && !configuredDevice.ok) {
|
|
37
|
+
errorRegistry.fire(configuredDevice.error);
|
|
38
|
+
return err(configuredDevice.error);
|
|
39
|
+
}
|
|
40
|
+
const cfgResult = configureSurface(
|
|
41
|
+
context,
|
|
42
|
+
configuredDevice?.value ?? device,
|
|
43
|
+
state.format,
|
|
44
|
+
state.colorAttachmentFormat,
|
|
45
|
+
);
|
|
46
|
+
if (!cfgResult.ok) {
|
|
47
|
+
errorRegistry.fire(cfgResult.error);
|
|
48
|
+
return err(cfgResult.error);
|
|
49
|
+
}
|
|
50
|
+
state.perPassResources.configured = true;
|
|
51
|
+
(globalThis as { __forgeaxSwapChainFormat?: GPUTextureFormat }).__forgeaxSwapChainFormat =
|
|
52
|
+
state.format;
|
|
53
|
+
return ok(undefined);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function runShimSyncStep<T>(
|
|
57
|
+
fn: () => Result<T, RhiError>,
|
|
58
|
+
fallbackCode:
|
|
59
|
+
| 'shader-compile-failed'
|
|
60
|
+
| 'webgpu-runtime-error'
|
|
61
|
+
| 'queue-write-buffer-out-of-bounds',
|
|
62
|
+
expected: string,
|
|
63
|
+
hint: string,
|
|
64
|
+
): Result<T, RhiError> {
|
|
65
|
+
try {
|
|
66
|
+
return fn();
|
|
67
|
+
} catch (caught) {
|
|
68
|
+
if (caught instanceof RhiError) return err(caught);
|
|
69
|
+
const detail = caught instanceof Error ? caught.message : String(caught);
|
|
70
|
+
return err(
|
|
71
|
+
new RhiError({
|
|
72
|
+
code: fallbackCode,
|
|
73
|
+
expected,
|
|
74
|
+
hint: `${hint} (cause: ${detail})`,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runShimStep<T>(
|
|
81
|
+
fn: () => Promise<Result<T, RhiError>>,
|
|
82
|
+
fallbackCode: 'shader-compile-failed' | 'webgpu-runtime-error',
|
|
83
|
+
expected: string,
|
|
84
|
+
hint: string,
|
|
85
|
+
): Promise<Result<T, RhiError>> {
|
|
86
|
+
try {
|
|
87
|
+
return await fn();
|
|
88
|
+
} catch (caught) {
|
|
89
|
+
if (caught instanceof RhiError) return err(caught);
|
|
90
|
+
const detail = caught instanceof Error ? caught.message : String(caught);
|
|
91
|
+
return err(
|
|
92
|
+
new RhiError({
|
|
93
|
+
code: fallbackCode,
|
|
94
|
+
expected,
|
|
95
|
+
hint: `${hint} (cause: ${detail})`,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -4,3 +4,162 @@ import type { RendererState } from '../render-contract';
|
|
|
4
4
|
export function isRendererStateOperational(state: RendererState): boolean {
|
|
5
5
|
return state === 'alive';
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
export const RECOVERY_TOTAL_DEADLINE_MS = 20_000;
|
|
9
|
+
export const RECOVERY_ADAPTER_DEADLINE_MS = 10_000;
|
|
10
|
+
export const RECOVERY_DEVICE_DEADLINE_MS = 10_000;
|
|
11
|
+
|
|
12
|
+
export const RECOVERY_PHASES = [
|
|
13
|
+
'quiesce',
|
|
14
|
+
'acquire-adapter',
|
|
15
|
+
'acquire-device',
|
|
16
|
+
'rehydrate',
|
|
17
|
+
'compile-graph',
|
|
18
|
+
'publish',
|
|
19
|
+
'cleanup',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type RecoveryPhase = (typeof RECOVERY_PHASES)[number];
|
|
23
|
+
export type RecoveryGuidance = 'retry' | 'repair-owner' | 'rebuild-renderer';
|
|
24
|
+
|
|
25
|
+
const RENDERER_STATES = [
|
|
26
|
+
'alive',
|
|
27
|
+
'device-lost',
|
|
28
|
+
'recovering',
|
|
29
|
+
'faulted',
|
|
30
|
+
'disposed',
|
|
31
|
+
] as const satisfies readonly RendererState[];
|
|
32
|
+
|
|
33
|
+
const ALLOWED_TRANSITIONS: Readonly<Record<RendererState, readonly RendererState[]>> = {
|
|
34
|
+
alive: ['device-lost', 'disposed'],
|
|
35
|
+
'device-lost': ['recovering', 'faulted', 'disposed'],
|
|
36
|
+
recovering: ['alive', 'device-lost', 'faulted', 'disposed'],
|
|
37
|
+
faulted: ['disposed'],
|
|
38
|
+
disposed: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface RendererLifecycle {
|
|
42
|
+
state(): RendererState;
|
|
43
|
+
transition(next: RendererState): boolean;
|
|
44
|
+
states(): readonly RendererState[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createRendererLifecycle(initial: RendererState = 'alive'): RendererLifecycle {
|
|
48
|
+
if (!RENDERER_STATES.includes(initial)) {
|
|
49
|
+
throw new RangeError(`Unknown renderer lifecycle state: ${initial}`);
|
|
50
|
+
}
|
|
51
|
+
let current = initial;
|
|
52
|
+
return {
|
|
53
|
+
state: () => current,
|
|
54
|
+
transition: (next) => {
|
|
55
|
+
if (!ALLOWED_TRANSITIONS[current].includes(next)) return false;
|
|
56
|
+
current = next;
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
states: () => RENDERER_STATES,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface RecoveryDeadline {
|
|
64
|
+
readonly startedAt: number;
|
|
65
|
+
readonly totalDeadlineMs: number;
|
|
66
|
+
readonly adapterDeadlineMs: number;
|
|
67
|
+
readonly deviceDeadlineMs: number;
|
|
68
|
+
readonly deadlineAt: number;
|
|
69
|
+
beginDeviceAcquisition(now: number): void;
|
|
70
|
+
isValid(phase: RecoveryPhase, now: number): boolean;
|
|
71
|
+
elapsed(now: number): number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createRecoveryDeadline(startedAt: number): RecoveryDeadline {
|
|
75
|
+
if (!Number.isFinite(startedAt)) throw new RangeError('Recovery deadline requires finite time.');
|
|
76
|
+
const deadlineAt = startedAt + RECOVERY_TOTAL_DEADLINE_MS;
|
|
77
|
+
let deviceAcquisitionStartedAt: number | undefined;
|
|
78
|
+
return {
|
|
79
|
+
startedAt,
|
|
80
|
+
totalDeadlineMs: RECOVERY_TOTAL_DEADLINE_MS,
|
|
81
|
+
adapterDeadlineMs: RECOVERY_ADAPTER_DEADLINE_MS,
|
|
82
|
+
deviceDeadlineMs: RECOVERY_DEVICE_DEADLINE_MS,
|
|
83
|
+
deadlineAt,
|
|
84
|
+
beginDeviceAcquisition: (now) => {
|
|
85
|
+
if (!Number.isFinite(now)) throw new RangeError('Device acquisition requires finite time.');
|
|
86
|
+
deviceAcquisitionStartedAt ??= now;
|
|
87
|
+
},
|
|
88
|
+
isValid: (phase, now) => {
|
|
89
|
+
const phaseDeadline =
|
|
90
|
+
phase === 'acquire-adapter'
|
|
91
|
+
? startedAt + RECOVERY_ADAPTER_DEADLINE_MS
|
|
92
|
+
: phase === 'acquire-device'
|
|
93
|
+
? Math.min(
|
|
94
|
+
(deviceAcquisitionStartedAt ?? startedAt) + RECOVERY_DEVICE_DEADLINE_MS,
|
|
95
|
+
deadlineAt,
|
|
96
|
+
)
|
|
97
|
+
: deadlineAt;
|
|
98
|
+
return Number.isFinite(now) && now < phaseDeadline;
|
|
99
|
+
},
|
|
100
|
+
elapsed: (now) => Math.max(0, now - startedAt),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface RecoveryContinuation {
|
|
105
|
+
isValid(phase: RecoveryPhase, now: number): boolean;
|
|
106
|
+
abandon(now: number): void;
|
|
107
|
+
cleanupOnce(): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createRecoveryContinuation(
|
|
111
|
+
deadline: RecoveryDeadline,
|
|
112
|
+
cleanup: () => void,
|
|
113
|
+
): RecoveryContinuation {
|
|
114
|
+
let valid = true;
|
|
115
|
+
let cleaned = false;
|
|
116
|
+
return {
|
|
117
|
+
isValid: (phase, now) => {
|
|
118
|
+
if (!valid || !deadline.isValid(phase, now)) {
|
|
119
|
+
valid = false;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
},
|
|
124
|
+
abandon: (now) => {
|
|
125
|
+
if (!deadline.isValid('cleanup', now)) valid = false;
|
|
126
|
+
valid = false;
|
|
127
|
+
},
|
|
128
|
+
cleanupOnce: () => {
|
|
129
|
+
if (cleaned) return;
|
|
130
|
+
cleaned = true;
|
|
131
|
+
cleanup();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface SingleFlight<T> {
|
|
137
|
+
run(): Promise<T>;
|
|
138
|
+
inFlight(): Promise<T> | undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function createSingleFlight<T>(factory: () => Promise<T>): SingleFlight<T> {
|
|
142
|
+
let active: Promise<T> | undefined;
|
|
143
|
+
return {
|
|
144
|
+
run: () => {
|
|
145
|
+
if (active !== undefined) return active;
|
|
146
|
+
let promise: Promise<T>;
|
|
147
|
+
try {
|
|
148
|
+
promise = factory();
|
|
149
|
+
} catch (cause) {
|
|
150
|
+
promise = Promise.reject(cause);
|
|
151
|
+
}
|
|
152
|
+
active = promise;
|
|
153
|
+
promise.then(
|
|
154
|
+
() => {
|
|
155
|
+
if (active === promise) active = undefined;
|
|
156
|
+
},
|
|
157
|
+
() => {
|
|
158
|
+
if (active === promise) active = undefined;
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
return promise;
|
|
162
|
+
},
|
|
163
|
+
inFlight: () => active,
|
|
164
|
+
};
|
|
165
|
+
}
|