@forgeax/engine-render 0.1.26 → 0.1.28
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 +215 -0
- package/dist/assembly/dynamic-geometry-host.d.ts +31 -0
- package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
- package/dist/assembly/host-contract.d.ts +11 -0
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/authoring.mjs +2 -2
- package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
- package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
- package/dist/{chunk-HKXTW355.mjs → chunk-IQMKJLNU.mjs} +3 -3
- package/dist/{chunk-HKXTW355.mjs.map → chunk-IQMKJLNU.mjs.map} +1 -1
- package/dist/{chunk-KM2NOX2I.mjs → chunk-J52NMQKR.mjs} +462 -6
- package/dist/chunk-J52NMQKR.mjs.map +1 -0
- package/dist/{chunk-ZZ4YQ474.mjs → chunk-J6KACNOH.mjs} +2 -2
- package/dist/{chunk-ZZ4YQ474.mjs.map → chunk-J6KACNOH.mjs.map} +1 -1
- package/dist/{chunk-ADHHVYLW.mjs → chunk-JAASCMXA.mjs} +3 -3
- package/dist/{chunk-ADHHVYLW.mjs.map → chunk-JAASCMXA.mjs.map} +1 -1
- package/dist/{chunk-TMIASV2N.mjs → chunk-MBU4ZVE7.mjs} +20 -12
- package/dist/chunk-MBU4ZVE7.mjs.map +1 -0
- package/dist/{chunk-E34VL5VI.mjs → chunk-MYJ5RPJO.mjs} +3 -3
- package/dist/{chunk-E34VL5VI.mjs.map → chunk-MYJ5RPJO.mjs.map} +1 -1
- package/dist/{chunk-JDWARUOI.mjs → chunk-RBDBZ5R7.mjs} +17 -3
- package/dist/chunk-RBDBZ5R7.mjs.map +1 -0
- package/dist/{chunk-YUQXSEHG.mjs → chunk-VSL23LZE.mjs} +26 -12
- package/dist/chunk-VSL23LZE.mjs.map +1 -0
- package/dist/construct-renderer.mjs +862 -106
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +10 -5
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/device/mesh-residency-lifetime.d.ts +21 -0
- package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
- package/dist/dynamic-geometry.d.ts +125 -0
- package/dist/dynamic-geometry.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +7 -6
- package/dist/index.mjs.map +1 -1
- package/dist/internal.mjs +7 -7
- package/dist/record/frame.d.ts +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -0
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/render-contract.d.ts +12 -0
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +6 -0
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +7 -0
- package/dist/render-system.d.ts.map +1 -1
- package/dist/temporal/index.mjs +4 -4
- package/package.json +20 -20
- package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
- package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
- package/src/__tests__/factory-contract.integration.test.ts +6 -14
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +1 -1
- package/src/assembly/dynamic-geometry-host.ts +839 -0
- package/src/assembly/host-contract.ts +26 -0
- package/src/assembly/renderer-facade.ts +17 -0
- package/src/assembly/webgpu-renderer.ts +49 -2
- package/src/components/sprite-animation.ts +1 -1
- package/src/device/gpu-residency.ts +62 -62
- package/src/device/mesh-residency-lifetime.ts +73 -0
- package/src/dynamic-geometry.ts +736 -0
- package/src/index.ts +16 -0
- package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +68 -1
- package/src/record/frame.ts +2 -0
- package/src/record/main-pass-geometry.ts +13 -4
- package/src/record/main-pass-sprite-draws.ts +2 -0
- package/src/record/render-context.ts +2 -0
- package/src/record/typed-frame-graph.ts +2 -9
- package/src/render-contract.ts +24 -0
- package/src/render-system-extract-tail.ts +3 -2
- package/src/render-system-extract.ts +30 -1
- package/src/render-system.ts +42 -0
- package/dist/chunk-FJ6P52EE.mjs.map +0 -1
- package/dist/chunk-JDWARUOI.mjs.map +0 -1
- package/dist/chunk-KM2NOX2I.mjs.map +0 -1
- package/dist/chunk-TMIASV2N.mjs.map +0 -1
- package/dist/chunk-YUQXSEHG.mjs.map +0 -1
|
@@ -2,6 +2,13 @@ import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
|
2
2
|
import type { World } from '@forgeax/engine-ecs';
|
|
3
3
|
import type { Result, RhiDevice, RhiError, ShaderModule } from '@forgeax/engine-rhi';
|
|
4
4
|
import type { RenderPipelineAsset } from '@forgeax/engine-types';
|
|
5
|
+
import type {
|
|
6
|
+
DynamicGeometryCandidate,
|
|
7
|
+
DynamicGeometryError,
|
|
8
|
+
DynamicGeometryOrdering,
|
|
9
|
+
DynamicGeometryPrepareInput,
|
|
10
|
+
DynamicGeometryReceipt,
|
|
11
|
+
} from '../dynamic-geometry';
|
|
5
12
|
import type { RecoverFailure } from '../errors/recover';
|
|
6
13
|
import type { ObservationUnavailableError, RenderError } from '../errors/render';
|
|
7
14
|
import type { RenderFeature, RenderFeatureDiagnostics } from '../features/types';
|
|
@@ -54,6 +61,25 @@ export type RendererHostEventListener = (event: RendererHostEvent) => void;
|
|
|
54
61
|
*/
|
|
55
62
|
export interface RendererHostImplementation {
|
|
56
63
|
attach(world: World): RenderResult<RenderWorldLease, RenderError>;
|
|
64
|
+
prepareDynamicGeometry(
|
|
65
|
+
input: DynamicGeometryPrepareInput,
|
|
66
|
+
): Result<DynamicGeometryCandidate, DynamicGeometryError>;
|
|
67
|
+
acceptDynamicGeometry(
|
|
68
|
+
candidate: DynamicGeometryCandidate,
|
|
69
|
+
ordering: DynamicGeometryOrdering,
|
|
70
|
+
): Result<DynamicGeometryCandidate, DynamicGeometryError>;
|
|
71
|
+
dynamicGeometryReceipt(candidate: DynamicGeometryCandidate): DynamicGeometryReceipt | undefined;
|
|
72
|
+
cancelDynamicGeometry(candidate: DynamicGeometryCandidate): Result<void, DynamicGeometryError>;
|
|
73
|
+
retireDynamicGeometry(candidate: DynamicGeometryCandidate): Result<void, DynamicGeometryError>;
|
|
74
|
+
publishDynamicGeometry(
|
|
75
|
+
frame: {
|
|
76
|
+
readonly frameId: number;
|
|
77
|
+
readonly deviceGeneration: number;
|
|
78
|
+
readonly completed?: Promise<unknown>;
|
|
79
|
+
},
|
|
80
|
+
worlds?: readonly object[],
|
|
81
|
+
fixedStep?: number,
|
|
82
|
+
): readonly DynamicGeometryReceipt[];
|
|
57
83
|
createRenderTarget(descriptor: RenderTargetDescriptor): RenderResult<RenderTarget, RenderError>;
|
|
58
84
|
resizeRenderTarget(
|
|
59
85
|
target: RenderTarget,
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
// lease/receipt contract and owns the profile/error projection helpers.
|
|
4
4
|
|
|
5
5
|
import { err, ok, type RhiError } from '@forgeax/engine-rhi';
|
|
6
|
+
import type {
|
|
7
|
+
DynamicGeometryCandidate,
|
|
8
|
+
DynamicGeometryOrdering,
|
|
9
|
+
DynamicGeometryPrepareInput,
|
|
10
|
+
} from '../dynamic-geometry';
|
|
6
11
|
import { type RecoverFailure, RecoveryFailedError } from '../errors/recover';
|
|
7
12
|
import {
|
|
8
13
|
type RenderError,
|
|
@@ -94,6 +99,18 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
94
99
|
}),
|
|
95
100
|
);
|
|
96
101
|
},
|
|
102
|
+
prepareDynamicGeometry: (input: DynamicGeometryPrepareInput) =>
|
|
103
|
+
renderer.prepareDynamicGeometry(input),
|
|
104
|
+
acceptDynamicGeometry: (
|
|
105
|
+
candidate: DynamicGeometryCandidate,
|
|
106
|
+
ordering: DynamicGeometryOrdering,
|
|
107
|
+
) => renderer.acceptDynamicGeometry(candidate, ordering),
|
|
108
|
+
dynamicGeometryReceipt: (candidate: DynamicGeometryCandidate) =>
|
|
109
|
+
renderer.dynamicGeometryReceipt(candidate),
|
|
110
|
+
cancelDynamicGeometry: (candidate: DynamicGeometryCandidate) =>
|
|
111
|
+
renderer.cancelDynamicGeometry(candidate),
|
|
112
|
+
retireDynamicGeometry: (candidate: DynamicGeometryCandidate) =>
|
|
113
|
+
renderer.retireDynamicGeometry(candidate),
|
|
97
114
|
createRenderTarget: (descriptor) => renderer.createRenderTarget(descriptor),
|
|
98
115
|
resizeRenderTarget: (target, descriptor) => renderer.resizeRenderTarget(target, descriptor),
|
|
99
116
|
createRenderTargetTextureSource: (target, options) =>
|
|
@@ -57,6 +57,7 @@ import type {
|
|
|
57
57
|
PrimitiveTopology,
|
|
58
58
|
VertexAttributeMap,
|
|
59
59
|
} from '@forgeax/engine-types';
|
|
60
|
+
import { createDynamicGeometryLifecycle } from '../dynamic-geometry';
|
|
60
61
|
|
|
61
62
|
export type { MaterialShaderManifestEntry } from '@forgeax/engine-shader';
|
|
62
63
|
|
|
@@ -165,6 +166,7 @@ import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
|
|
|
165
166
|
import type { RhiBackendPack } from './backend-contract';
|
|
166
167
|
import type { BundlerOptions } from './bundler-contract';
|
|
167
168
|
import { deviceOptionsForAdapter } from './device-feature-admission';
|
|
169
|
+
import { createDynamicGeometryHost } from './dynamic-geometry-host';
|
|
168
170
|
import type { RendererAssemblyImplementation } from './host-contract';
|
|
169
171
|
import {
|
|
170
172
|
allowsUnlitPreparedFallback,
|
|
@@ -1109,6 +1111,7 @@ async function makeWebGPURenderer(
|
|
|
1109
1111
|
const publishRendererGeneration = (candidate: RendererGeneration): void => {
|
|
1110
1112
|
publishGeneration(generationPublication, candidate, (value) => value.scope.isAlive());
|
|
1111
1113
|
activeDeviceScope = candidate.scope;
|
|
1114
|
+
dynamicGeometry.invalidateGeneration(activeDeviceScope.generation);
|
|
1112
1115
|
internals.generationState.current = activeDeviceScope.generation;
|
|
1113
1116
|
pipelineState = candidate.pipeline;
|
|
1114
1117
|
const bindings = candidate.producerBindings;
|
|
@@ -2023,6 +2026,7 @@ async function makeWebGPURenderer(
|
|
|
2023
2026
|
const fragmentEntry =
|
|
2024
2027
|
authoredFragmentEntry ??
|
|
2025
2028
|
(isHdr &&
|
|
2029
|
+
passKind === 'forward' &&
|
|
2026
2030
|
(materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
|
|
2027
2031
|
? 'fs_main_hdr'
|
|
2028
2032
|
: undefined);
|
|
@@ -2805,6 +2809,20 @@ async function makeWebGPURenderer(
|
|
|
2805
2809
|
const attachedWorlds = new Set<World>();
|
|
2806
2810
|
const attachedLeases = new Map<RenderReadLease, World>();
|
|
2807
2811
|
const leasesByWorld = new Map<World, RenderReadLease>();
|
|
2812
|
+
const dynamicGeometry = createDynamicGeometryLifecycle();
|
|
2813
|
+
const dynamicGeometryHost = createDynamicGeometryHost({
|
|
2814
|
+
lifecycle: dynamicGeometry,
|
|
2815
|
+
attachedWorlds,
|
|
2816
|
+
getGpuStore: () => gpuStore,
|
|
2817
|
+
currentGeneration: () => activeDeviceScope.generation,
|
|
2818
|
+
onTopologyChanged: () => renderSystem.invalidateGeometryHistory(),
|
|
2819
|
+
isConsumedByRenderFrame: (candidate) =>
|
|
2820
|
+
renderSystem.isDynamicGeometryConsumed(
|
|
2821
|
+
candidate.world as World,
|
|
2822
|
+
candidate.entity ?? -1,
|
|
2823
|
+
candidate.meshHandle,
|
|
2824
|
+
),
|
|
2825
|
+
});
|
|
2808
2826
|
const derivedSystemNames = new Map<World, string>();
|
|
2809
2827
|
const transformReleases = new Map<World, () => void>();
|
|
2810
2828
|
const attachmentOwner = {};
|
|
@@ -2817,7 +2835,6 @@ async function makeWebGPURenderer(
|
|
|
2817
2835
|
const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
|
|
2818
2836
|
let latestReceipt: FrameReceipt | undefined;
|
|
2819
2837
|
let surfaceReleased = false;
|
|
2820
|
-
|
|
2821
2838
|
const renderer: RendererAssemblyImplementation = {
|
|
2822
2839
|
attach(world: World): RenderResult<RenderWorldLease, RenderError> {
|
|
2823
2840
|
const attached = this.attachScene(world);
|
|
@@ -2829,6 +2846,7 @@ async function makeWebGPURenderer(
|
|
|
2829
2846
|
leasesByWorld.set(world, lease);
|
|
2830
2847
|
return ok(lease);
|
|
2831
2848
|
},
|
|
2849
|
+
...dynamicGeometryHost,
|
|
2832
2850
|
createRenderTarget: (descriptor) => renderTargetHost.createRenderTarget(descriptor),
|
|
2833
2851
|
resizeRenderTarget: (target, descriptor) =>
|
|
2834
2852
|
renderTargetHost.resizeRenderTarget(target, descriptor),
|
|
@@ -2949,6 +2967,7 @@ async function makeWebGPURenderer(
|
|
|
2949
2967
|
}),
|
|
2950
2968
|
),
|
|
2951
2969
|
),
|
|
2970
|
+
dynamicGeometry: dynamicGeometry.inspect(),
|
|
2952
2971
|
renderScene: renderSystem.renderScene,
|
|
2953
2972
|
reflectionProbes: renderSystem.reflectionProbes,
|
|
2954
2973
|
ssrDependencies: renderSystem.ssrDependencies,
|
|
@@ -3092,6 +3111,7 @@ async function makeWebGPURenderer(
|
|
|
3092
3111
|
},
|
|
3093
3112
|
detachScene(world: World): void {
|
|
3094
3113
|
if (!attachedWorlds.delete(world)) return;
|
|
3114
|
+
dynamicGeometryHost.invalidateDynamicGeometryWorld(world);
|
|
3095
3115
|
for (const [lease, leaseWorld] of attachedLeases) {
|
|
3096
3116
|
if (leaseWorld === world) {
|
|
3097
3117
|
lease.dispose();
|
|
@@ -3293,6 +3313,27 @@ async function makeWebGPURenderer(
|
|
|
3293
3313
|
: worlds.map((world) => leasesByWorld.get(world)).every((lease) => lease !== undefined)
|
|
3294
3314
|
? worlds.map((world) => leasesByWorld.get(world) as RenderReadLease)
|
|
3295
3315
|
: undefined;
|
|
3316
|
+
for (const world of worlds) {
|
|
3317
|
+
if (!world.hasResource('PhysicsWorld')) continue;
|
|
3318
|
+
const physics = world.getResource<{
|
|
3319
|
+
readonly getDerivedAdmission?: () => unknown;
|
|
3320
|
+
readonly getDerivedRecoveryState?: () => string;
|
|
3321
|
+
}>('PhysicsWorld');
|
|
3322
|
+
if (
|
|
3323
|
+
physics?.getDerivedAdmission?.() !== undefined ||
|
|
3324
|
+
physics?.getDerivedRecoveryState?.() === 'rebuild-required'
|
|
3325
|
+
) {
|
|
3326
|
+
return err(
|
|
3327
|
+
new RendererOperationError('frame-input-invalid', {
|
|
3328
|
+
operation: 'draw',
|
|
3329
|
+
cause: new RendererContractFailureError(
|
|
3330
|
+
'draw',
|
|
3331
|
+
'paired geometry admission must finish physics step and writeback in a healthy World before draw',
|
|
3332
|
+
),
|
|
3333
|
+
}),
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3296
3337
|
if (frameRequest !== undefined && worlds.length !== frameRequest.leases.length) {
|
|
3297
3338
|
return err(
|
|
3298
3339
|
new RendererOperationError('world-lease-invalid', {
|
|
@@ -3479,6 +3520,8 @@ async function makeWebGPURenderer(
|
|
|
3479
3520
|
const timingCompletion = timingHost.gpuPassTimingSubmittedWork;
|
|
3480
3521
|
timingHost.gpuPassTimingSubmittedWork = undefined;
|
|
3481
3522
|
const reflectionFallbackCompletion = renderSystem.reflectionFallbackCompletion;
|
|
3523
|
+
const queueCompletion = timingCompletion ?? internals.device.queue.onSubmittedWorkDone();
|
|
3524
|
+
gpuStore.trackMeshSubmission(queueCompletion);
|
|
3482
3525
|
internals.pack.instrumentation?.onFrameBoundary?.();
|
|
3483
3526
|
if (!isFrameRequest) {
|
|
3484
3527
|
renderTargetHost.onFrameSubmitted();
|
|
@@ -3486,7 +3529,6 @@ async function makeWebGPURenderer(
|
|
|
3486
3529
|
}
|
|
3487
3530
|
const receiptFrameId = ++frameId;
|
|
3488
3531
|
const receiptGeneration = activeDeviceScope.generation;
|
|
3489
|
-
const queueCompletion = timingCompletion ?? internals.device.queue.onSubmittedWorkDone();
|
|
3490
3532
|
const completion: FrameReceipt['completed'] = Promise.all([
|
|
3491
3533
|
queueCompletion,
|
|
3492
3534
|
reflectionFallbackCompletion ?? Promise.resolve(),
|
|
@@ -3608,6 +3650,7 @@ async function makeWebGPURenderer(
|
|
|
3608
3650
|
);
|
|
3609
3651
|
}
|
|
3610
3652
|
issuedReceipts.add(receipt);
|
|
3653
|
+
dynamicGeometryHost.publishDynamicGeometry(receipt, worlds, frameRequest?.fixedStep);
|
|
3611
3654
|
return ok(receipt);
|
|
3612
3655
|
} catch (cause) {
|
|
3613
3656
|
const error =
|
|
@@ -3756,6 +3799,10 @@ async function makeWebGPURenderer(
|
|
|
3756
3799
|
dispose(): RenderResult<void, RenderError> {
|
|
3757
3800
|
if (disposed) return ok(undefined);
|
|
3758
3801
|
disposed = true;
|
|
3802
|
+
for (const world of attachedWorlds) {
|
|
3803
|
+
dynamicGeometryHost.invalidateDynamicGeometryWorld(world);
|
|
3804
|
+
}
|
|
3805
|
+
dynamicGeometry.dispose();
|
|
3759
3806
|
for (const continuation of frameContinuations) {
|
|
3760
3807
|
continuation.terminate({ code: 'disposed' });
|
|
3761
3808
|
}
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
//
|
|
54
54
|
// 4-step recipe (charter F1 progressive disclosure — minimum walk-cycle
|
|
55
55
|
// host code; full demo lands in M6 hello-sprite-atlas):
|
|
56
|
-
// 1. Build atlas via `forgeax
|
|
56
|
+
// 1. Build atlas via `forgeax asset atlas --input <glob> --name <prefix> --output <dir> --root <project>`
|
|
57
57
|
// (M5 build-time hook); load the emitted `<name>.atlas.png` plus
|
|
58
58
|
// `<name>.atlas.meta.json` sidecar.
|
|
59
59
|
// 2. Register the sprite material referencing the atlas
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MeshResidencyLease, MeshResidencyLifetime } from './mesh-residency-lifetime';
|
|
1
2
|
// @forgeax/engine-runtime - GpuResidencyCache
|
|
2
3
|
// (feat-20260601-device/gpu-residency-extraction M1; M2 added the deriveRenderData
|
|
3
4
|
// projection seam — see render-data.ts).
|
|
@@ -557,6 +558,36 @@ export class GpuResidencyCache {
|
|
|
557
558
|
// equirect source always resolves to the same cubemap (idempotent, A2).
|
|
558
559
|
private readonly cubemapIdempotentMap: Map<number, Handle<'EquirectAsset', 'shared'>> = new Map();
|
|
559
560
|
private readonly meshGpuHandles: Map<string | number, MeshGpuHandles> = new Map();
|
|
561
|
+
private readonly meshLifetimes = new WeakMap<MeshGpuHandles, MeshResidencyLifetime>();
|
|
562
|
+
private readonly retiredMeshes = new Set<MeshGpuHandles>();
|
|
563
|
+
|
|
564
|
+
private meshLifetime(entry: MeshGpuHandles): MeshResidencyLifetime {
|
|
565
|
+
let lifetime = this.meshLifetimes.get(entry);
|
|
566
|
+
if (lifetime === undefined) {
|
|
567
|
+
lifetime = new MeshResidencyLifetime(() => {
|
|
568
|
+
if (!entry.vertexBuffer.isDestroyed) entry.vertexBuffer.destroy();
|
|
569
|
+
if (entry.indexBuffer !== null && !entry.indexBuffer.isDestroyed)
|
|
570
|
+
entry.indexBuffer.destroy();
|
|
571
|
+
this.retiredMeshes.delete(entry);
|
|
572
|
+
});
|
|
573
|
+
this.meshLifetimes.set(entry, lifetime);
|
|
574
|
+
}
|
|
575
|
+
return lifetime;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** All resident entries conservatively cover main, shadow and cached GPU-driven draws. */
|
|
579
|
+
trackMeshSubmission(completed: Promise<unknown>): void {
|
|
580
|
+
for (const entry of this.meshGpuHandles.values()) this.meshLifetime(entry).track(completed);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private retireMeshEntry(key: string | number, entry: MeshGpuHandles): void {
|
|
584
|
+
if (this.meshGpuHandles.get(key) === entry) {
|
|
585
|
+
this.meshGpuHandles.delete(key);
|
|
586
|
+
this.meshResidencyEpoch += 1;
|
|
587
|
+
}
|
|
588
|
+
this.retiredMeshes.add(entry);
|
|
589
|
+
this.meshLifetime(entry).retire();
|
|
590
|
+
}
|
|
560
591
|
|
|
561
592
|
/**
|
|
562
593
|
* Compose a cache key from the raw handle slot and its owning World. The
|
|
@@ -711,11 +742,12 @@ export class GpuResidencyCache {
|
|
|
711
742
|
this.cubemapGpuHandles.clear();
|
|
712
743
|
this.cubemapIdempotentMap.clear();
|
|
713
744
|
|
|
714
|
-
for (const entry of this.meshGpuHandles.values()) {
|
|
745
|
+
for (const entry of [...this.meshGpuHandles.values(), ...this.retiredMeshes]) {
|
|
715
746
|
destroyBuf(entry.vertexBuffer);
|
|
716
747
|
if (entry.indexBuffer !== null) destroyBuf(entry.indexBuffer);
|
|
717
748
|
}
|
|
718
749
|
this.meshGpuHandles.clear();
|
|
750
|
+
this.retiredMeshes.clear();
|
|
719
751
|
}
|
|
720
752
|
|
|
721
753
|
/** Number of candidate-owned GPU resources currently held by this cache. */
|
|
@@ -730,6 +762,7 @@ export class GpuResidencyCache {
|
|
|
730
762
|
}
|
|
731
763
|
return (
|
|
732
764
|
this.meshGpuHandles.size +
|
|
765
|
+
this.retiredMeshes.size +
|
|
733
766
|
this.textureGpuHandles.size +
|
|
734
767
|
this.samplerGpuHandles.size +
|
|
735
768
|
cubemapResources
|
|
@@ -802,27 +835,13 @@ export class GpuResidencyCache {
|
|
|
802
835
|
const entry = this.meshGpuHandles.get(id);
|
|
803
836
|
if (entry === undefined) return { freed: 0, errors: [] };
|
|
804
837
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
if (r.ok) {
|
|
812
|
-
freed += 1;
|
|
813
|
-
} else {
|
|
814
|
-
errors.push(r.error);
|
|
815
|
-
if (this.errorRegistry) this.errorRegistry.fire(r.error);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
838
|
+
if (this.meshLifetime(entry).owners > 0) return { freed: 0, errors: [] };
|
|
839
|
+
this.retireMeshEntry(id, entry);
|
|
840
|
+
return {
|
|
841
|
+
freed:
|
|
842
|
+
Number(entry.vertexBuffer.isDestroyed) + Number(entry.indexBuffer?.isDestroyed ?? false),
|
|
843
|
+
errors: [],
|
|
818
844
|
};
|
|
819
|
-
|
|
820
|
-
destroyBuf(entry.vertexBuffer);
|
|
821
|
-
if (entry.indexBuffer !== null) destroyBuf(entry.indexBuffer);
|
|
822
|
-
|
|
823
|
-
this.meshGpuHandles.delete(id);
|
|
824
|
-
this.meshResidencyEpoch += 1;
|
|
825
|
-
return { freed: freed > 0 ? 1 : 0, errors };
|
|
826
845
|
}
|
|
827
846
|
|
|
828
847
|
evictCubemap(id: number): { freed: number; errors: RhiError[] } {
|
|
@@ -862,7 +881,6 @@ export class GpuResidencyCache {
|
|
|
862
881
|
releaseUnreferenced(liveSet: Set<number>): { freed: number; errors: RhiError[] } {
|
|
863
882
|
let freed = 0;
|
|
864
883
|
const errors: RhiError[] = [];
|
|
865
|
-
let meshResidencyChanged = false;
|
|
866
884
|
let materialResidencyChanged = false;
|
|
867
885
|
const liveSlots = new Set([...liveSet].map((slot) => String(slot)));
|
|
868
886
|
const isLiveKey = (key: string | number): boolean => {
|
|
@@ -916,35 +934,14 @@ export class GpuResidencyCache {
|
|
|
916
934
|
}
|
|
917
935
|
}
|
|
918
936
|
|
|
919
|
-
for (const key of this.meshGpuHandles
|
|
920
|
-
if (!isLiveKey(key)) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const r = entry.vertexBuffer.destroy();
|
|
925
|
-
if (r.ok) {
|
|
926
|
-
freed += 1;
|
|
927
|
-
} else {
|
|
928
|
-
errors.push(r.error);
|
|
929
|
-
if (this.errorRegistry) this.errorRegistry.fire(r.error);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
if (entry.indexBuffer !== null && !entry.indexBuffer.isDestroyed) {
|
|
933
|
-
const r = entry.indexBuffer.destroy();
|
|
934
|
-
if (r.ok) {
|
|
935
|
-
freed += 1;
|
|
936
|
-
} else {
|
|
937
|
-
errors.push(r.error);
|
|
938
|
-
if (this.errorRegistry) this.errorRegistry.fire(r.error);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
this.meshGpuHandles.delete(key);
|
|
942
|
-
meshResidencyChanged = true;
|
|
943
|
-
}
|
|
937
|
+
for (const [key, entry] of this.meshGpuHandles) {
|
|
938
|
+
if (!isLiveKey(key) && this.meshLifetime(entry).owners === 0) {
|
|
939
|
+
this.retireMeshEntry(key, entry);
|
|
940
|
+
freed +=
|
|
941
|
+
Number(entry.vertexBuffer.isDestroyed) + Number(entry.indexBuffer?.isDestroyed ?? false);
|
|
944
942
|
}
|
|
945
943
|
}
|
|
946
944
|
|
|
947
|
-
if (meshResidencyChanged) this.meshResidencyEpoch += 1;
|
|
948
945
|
if (materialResidencyChanged) this.materialResourceEpoch += 1;
|
|
949
946
|
|
|
950
947
|
return { freed, errors };
|
|
@@ -1096,23 +1093,26 @@ export class GpuResidencyCache {
|
|
|
1096
1093
|
return this.meshGpuHandles.get(this.worldKey(handleSlot(handle), worldId));
|
|
1097
1094
|
}
|
|
1098
1095
|
|
|
1099
|
-
/**
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1096
|
+
/** Candidate leases retain the concrete allocation even after cache invalidation. */
|
|
1097
|
+
retainMeshResidency(
|
|
1098
|
+
handle: Handle<'MeshAsset', 'shared'>,
|
|
1099
|
+
worldId: World | number = 0,
|
|
1100
|
+
): MeshResidencyLease | undefined {
|
|
1101
|
+
const key = this.worldKey(handleSlot(handle), worldId);
|
|
1102
|
+
const entry = this.meshGpuHandles.get(key);
|
|
1103
|
+
return entry === undefined
|
|
1104
|
+
? undefined
|
|
1105
|
+
: this.meshLifetime(entry).retain(() => {
|
|
1106
|
+
if (this.meshGpuHandles.get(key) === entry) this.invalidateMesh(handle, worldId);
|
|
1107
|
+
else this.meshLifetime(entry).retire();
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** In-place author changes immediately miss the cache; old submissions keep their allocation. */
|
|
1104
1112
|
invalidateMesh(handle: number, worldId: World | number = 0): void {
|
|
1105
|
-
// SharedRefMutation publishes the encoded handle value, while the cache
|
|
1106
|
-
// key is derived from the decoded slot. Keep the invalidation path aligned
|
|
1107
|
-
// with getMeshGpuHandles/ensureResident so an in-place ECS payload change
|
|
1108
|
-
// cannot leave the old VBO resident.
|
|
1109
1113
|
const key = this.worldKey(handleSlot(toShared<'MeshAsset'>(handle)), worldId);
|
|
1110
1114
|
const entry = this.meshGpuHandles.get(key);
|
|
1111
|
-
if (entry
|
|
1112
|
-
entry.vertexBuffer.destroy();
|
|
1113
|
-
if (entry.indexBuffer !== null) entry.indexBuffer.destroy();
|
|
1114
|
-
this.meshGpuHandles.delete(key);
|
|
1115
|
-
this.meshResidencyEpoch += 1;
|
|
1115
|
+
if (entry !== undefined) this.retireMeshEntry(key, entry);
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
1118
|
/**
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** A lease captures one GPU allocation, never a reusable World handle slot. */
|
|
2
|
+
export interface MeshResidencyLease {
|
|
3
|
+
track(completed: Promise<unknown>): void;
|
|
4
|
+
release(evict: boolean): Promise<void> | undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Shared candidates and queue submissions have one allocation lifetime owner. */
|
|
8
|
+
export class MeshResidencyLifetime {
|
|
9
|
+
private leases = 0;
|
|
10
|
+
private retired = false;
|
|
11
|
+
private eviction: (() => void) | undefined;
|
|
12
|
+
private readonly submissions = new Set<Promise<unknown>>();
|
|
13
|
+
private readonly waiters = new Set<() => void>();
|
|
14
|
+
|
|
15
|
+
constructor(private destroy: (() => void) | undefined) {}
|
|
16
|
+
|
|
17
|
+
get owners(): number {
|
|
18
|
+
return this.leases;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
retain(evict: () => void): MeshResidencyLease {
|
|
22
|
+
this.leases += 1;
|
|
23
|
+
this.eviction = undefined;
|
|
24
|
+
let released = false;
|
|
25
|
+
let completion: Promise<void> | undefined;
|
|
26
|
+
return {
|
|
27
|
+
track: (submitted) => this.track(submitted),
|
|
28
|
+
release: (shouldEvict) => {
|
|
29
|
+
if (released) return completion;
|
|
30
|
+
released = true;
|
|
31
|
+
this.leases -= 1;
|
|
32
|
+
if (this.leases === 0 && shouldEvict) this.eviction = evict;
|
|
33
|
+
completion =
|
|
34
|
+
this.submissions.size === 0
|
|
35
|
+
? undefined
|
|
36
|
+
: new Promise<void>((resolve) => this.waiters.add(resolve));
|
|
37
|
+
this.flush();
|
|
38
|
+
return completion;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
track(completed: Promise<unknown>): void {
|
|
44
|
+
if (this.submissions.has(completed)) return;
|
|
45
|
+
this.submissions.add(completed);
|
|
46
|
+
const finish = (): void => {
|
|
47
|
+
this.submissions.delete(completed);
|
|
48
|
+
this.flush();
|
|
49
|
+
};
|
|
50
|
+
void completed.then(finish, finish);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
retire(): void {
|
|
54
|
+
this.retired = true;
|
|
55
|
+
this.flush();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private flush(): void {
|
|
59
|
+
if (this.submissions.size !== 0) return;
|
|
60
|
+
if (this.leases === 0 && this.eviction !== undefined) {
|
|
61
|
+
const evict = this.eviction;
|
|
62
|
+
this.eviction = undefined;
|
|
63
|
+
evict();
|
|
64
|
+
}
|
|
65
|
+
if (this.retired && this.leases === 0 && this.destroy !== undefined) {
|
|
66
|
+
const destroy = this.destroy;
|
|
67
|
+
this.destroy = undefined;
|
|
68
|
+
destroy();
|
|
69
|
+
}
|
|
70
|
+
for (const resolve of this.waiters) resolve();
|
|
71
|
+
this.waiters.clear();
|
|
72
|
+
}
|
|
73
|
+
}
|