@forgeax/engine-render-graph 0.1.32 → 0.1.34
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 +13 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/compiled-graph.d.ts +1 -0
- package/dist/compiled-graph.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +118 -11
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/render-graph-builder.unit.test.ts +114 -0
- package/src/__tests__/render-graph.unit.test.ts +49 -40
- package/src/builder.ts +28 -13
- package/src/compiled-graph.ts +128 -2
- package/src/index.ts +4 -0
- package/src/types.ts +63 -0
package/src/compiled-graph.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
RenderGraphFrame,
|
|
27
27
|
RenderGraphPassInstrumentation,
|
|
28
28
|
RenderGraphPassRunner,
|
|
29
|
+
RenderGraphResourceAllocationInspection,
|
|
29
30
|
} from './types.js';
|
|
30
31
|
|
|
31
32
|
interface FrameResources {
|
|
@@ -34,6 +35,101 @@ interface FrameResources {
|
|
|
34
35
|
readonly views: ReadonlyMap<number, TextureView>;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
interface AllocationEntry {
|
|
39
|
+
readonly handle: unknown;
|
|
40
|
+
readonly bytes: number | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compiled-graph owner ledger. It counts only graph-created physical handles;
|
|
45
|
+
* imported handles are references owned by the caller and remain byte-unknown.
|
|
46
|
+
*/
|
|
47
|
+
class RenderGraphAllocationLedger {
|
|
48
|
+
private readonly live = new Map<unknown, number | undefined>();
|
|
49
|
+
private readonly pending = new Map<unknown, number | undefined>();
|
|
50
|
+
private liveBytes = 0;
|
|
51
|
+
private pendingBytes = 0;
|
|
52
|
+
private peakBytes = 0;
|
|
53
|
+
private successfulAllocationCount = 0;
|
|
54
|
+
private successfulAllocationBytes = 0;
|
|
55
|
+
private retiredBytes = 0;
|
|
56
|
+
private unknownByteSizeCount = 0;
|
|
57
|
+
private readonly importedResourceCount: number;
|
|
58
|
+
|
|
59
|
+
constructor(entries: readonly AllocationEntry[], importedResourceCount: number) {
|
|
60
|
+
this.importedResourceCount = importedResourceCount;
|
|
61
|
+
for (const entry of entries) this.allocate(entry.handle, entry.bytes);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
inspect(): RenderGraphResourceAllocationInspection {
|
|
65
|
+
return {
|
|
66
|
+
unit: 'engine-allocation-bytes',
|
|
67
|
+
physicalResidency: 'unknown',
|
|
68
|
+
liveBytes: this.liveBytes,
|
|
69
|
+
pendingRetirementBytes: this.pendingBytes,
|
|
70
|
+
peakBytes: this.peakBytes,
|
|
71
|
+
successfulAllocationCount: this.successfulAllocationCount,
|
|
72
|
+
successfulAllocationBytes: this.successfulAllocationBytes,
|
|
73
|
+
pendingRetirementCount: this.pending.size,
|
|
74
|
+
retiredBytes: this.retiredBytes,
|
|
75
|
+
failedAllocationRollbacks: 0,
|
|
76
|
+
failedAllocationRollbackBytes: 0,
|
|
77
|
+
unknownByteSizeCount: this.unknownByteSizeCount,
|
|
78
|
+
importedResourceCount: this.importedResourceCount,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
retireAll(): void {
|
|
83
|
+
for (const handle of [...this.live.keys()]) this.retire(handle);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
release(handle: unknown): void {
|
|
87
|
+
const pendingBytes = this.pending.get(handle);
|
|
88
|
+
if (pendingBytes !== undefined || this.pending.has(handle)) {
|
|
89
|
+
this.pending.delete(handle);
|
|
90
|
+
if (pendingBytes !== undefined) this.pendingBytes -= pendingBytes;
|
|
91
|
+
if (pendingBytes === undefined) this.unknownByteSizeCount -= 1;
|
|
92
|
+
this.retiredBytes += pendingBytes ?? 0;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const liveBytes = this.live.get(handle);
|
|
96
|
+
if (liveBytes !== undefined || this.live.has(handle)) {
|
|
97
|
+
this.live.delete(handle);
|
|
98
|
+
if (liveBytes !== undefined) this.liveBytes -= liveBytes;
|
|
99
|
+
if (liveBytes === undefined) this.unknownByteSizeCount -= 1;
|
|
100
|
+
this.retiredBytes += liveBytes ?? 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private allocate(handle: unknown, bytes: number | undefined): void {
|
|
105
|
+
if (this.live.has(handle) || this.pending.has(handle)) return;
|
|
106
|
+
this.live.set(handle, bytes);
|
|
107
|
+
this.successfulAllocationCount += 1;
|
|
108
|
+
if (bytes === undefined) this.unknownByteSizeCount += 1;
|
|
109
|
+
else {
|
|
110
|
+
this.liveBytes += bytes;
|
|
111
|
+
this.successfulAllocationBytes += bytes;
|
|
112
|
+
}
|
|
113
|
+
this.updatePeak();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private retire(handle: unknown): void {
|
|
117
|
+
const bytes = this.live.get(handle);
|
|
118
|
+
if (bytes === undefined && !this.live.has(handle)) return;
|
|
119
|
+
this.live.delete(handle);
|
|
120
|
+
if (bytes !== undefined) {
|
|
121
|
+
this.liveBytes -= bytes;
|
|
122
|
+
this.pendingBytes += bytes;
|
|
123
|
+
}
|
|
124
|
+
this.pending.set(handle, bytes);
|
|
125
|
+
this.updatePeak();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private updatePeak(): void {
|
|
129
|
+
this.peakBytes = Math.max(this.peakBytes, this.liveBytes + this.pendingBytes);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
37
133
|
function resolutionError(label: string, cause?: unknown): RenderGraphError {
|
|
38
134
|
return new RenderGraphError({
|
|
39
135
|
code: 'resource-resolution-failed',
|
|
@@ -48,6 +144,7 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
|
|
|
48
144
|
{
|
|
49
145
|
private retired = false;
|
|
50
146
|
private retireResult: Promise<Result<void, RenderGraphError>> | undefined;
|
|
147
|
+
private readonly allocationLedger: RenderGraphAllocationLedger;
|
|
51
148
|
|
|
52
149
|
constructor(
|
|
53
150
|
private readonly generation: number,
|
|
@@ -58,10 +155,30 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
|
|
|
58
155
|
private readonly passes: readonly CompiledPass<FrameCtx>[],
|
|
59
156
|
private readonly info: CompiledRenderGraphInfo,
|
|
60
157
|
private readonly colorTargetDescriptors: ReadonlyMap<string, ResolvedColorTargetDescriptor>,
|
|
61
|
-
) {
|
|
158
|
+
) {
|
|
159
|
+
const infoByLabel = new Map(info.resources.map((resource) => [resource.label, resource]));
|
|
160
|
+
const seen = new Set<unknown>();
|
|
161
|
+
const entries: AllocationEntry[] = [];
|
|
162
|
+
let importedResourceCount = 0;
|
|
163
|
+
for (const compiled of resources.values()) {
|
|
164
|
+
if (compiled.usage === 0) continue;
|
|
165
|
+
const handle = compiled.texture ?? compiled.buffer;
|
|
166
|
+
if (compiled.record.origin === 'imported') {
|
|
167
|
+
importedResourceCount += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (handle === undefined || seen.has(handle)) continue;
|
|
171
|
+
seen.add(handle);
|
|
172
|
+
entries.push({
|
|
173
|
+
handle,
|
|
174
|
+
bytes: infoByLabel.get(compiled.record.label)?.byteSize,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
this.allocationLedger = new RenderGraphAllocationLedger(entries, importedResourceCount);
|
|
178
|
+
}
|
|
62
179
|
|
|
63
180
|
inspect(): CompiledRenderGraphInfo {
|
|
64
|
-
return this.info;
|
|
181
|
+
return { ...this.info, resourceAllocation: this.allocationLedger.inspect() };
|
|
65
182
|
}
|
|
66
183
|
|
|
67
184
|
getColorTargetView(name: string): TextureView | undefined {
|
|
@@ -260,6 +377,7 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
|
|
|
260
377
|
}
|
|
261
378
|
|
|
262
379
|
private async finishRetire(): Promise<Result<void, RenderGraphError>> {
|
|
380
|
+
this.allocationLedger.retireAll();
|
|
263
381
|
try {
|
|
264
382
|
await this.device.queue.onSubmittedWorkDone();
|
|
265
383
|
} catch (cause) {
|
|
@@ -277,11 +395,13 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
|
|
|
277
395
|
for (const compiled of this.resources.values()) {
|
|
278
396
|
if (compiled.record.origin === 'imported') continue;
|
|
279
397
|
if (compiled.texture === undefined && compiled.buffer === undefined) continue;
|
|
398
|
+
let destroyedSuccessfully = false;
|
|
280
399
|
try {
|
|
281
400
|
const destroyed =
|
|
282
401
|
compiled.record.kind === 'texture'
|
|
283
402
|
? this.device.destroyTexture(compiled.texture as Texture)
|
|
284
403
|
: this.device.destroyBuffer(compiled.buffer as Buffer);
|
|
404
|
+
destroyedSuccessfully = destroyed.ok;
|
|
285
405
|
if (!destroyed.ok && firstFailure === undefined) {
|
|
286
406
|
firstFailure = new RenderGraphError({
|
|
287
407
|
code: 'resource-retire-failed',
|
|
@@ -301,6 +421,12 @@ export class CompiledRenderGraphImpl<FrameCtx extends RenderGraphFrame>
|
|
|
301
421
|
hint: 'inspect detail.cause for the RHI destroy failure',
|
|
302
422
|
detail: { generation: this.generation, resourceLabel: compiled.record.label, cause },
|
|
303
423
|
});
|
|
424
|
+
} finally {
|
|
425
|
+
// A failed or throwing destroy leaves the handle owned by this
|
|
426
|
+
// graph. Keep its token pending so inspection reflects the
|
|
427
|
+
// retirement failure instead of claiming bytes were released.
|
|
428
|
+
if (destroyedSuccessfully)
|
|
429
|
+
this.allocationLedger.release(compiled.texture ?? compiled.buffer);
|
|
304
430
|
}
|
|
305
431
|
}
|
|
306
432
|
return firstFailure === undefined ? ok(undefined) : err(firstFailure);
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,7 @@ export {
|
|
|
68
68
|
export type {
|
|
69
69
|
CompiledRenderGraph,
|
|
70
70
|
CompiledRenderGraphInfo,
|
|
71
|
+
CompiledResourceByteSizeUnknownReason,
|
|
71
72
|
CompiledResourceDescriptor,
|
|
72
73
|
ComputeGraphPass,
|
|
73
74
|
CopyGraphPass,
|
|
@@ -96,8 +97,11 @@ export type {
|
|
|
96
97
|
RasterGraphPass,
|
|
97
98
|
RenderGraphCompileOptions,
|
|
98
99
|
RenderGraphFrame,
|
|
100
|
+
RenderGraphGenerationAllocationEntry,
|
|
101
|
+
RenderGraphGenerationAllocationInspection,
|
|
99
102
|
RenderGraphPassExecution,
|
|
100
103
|
RenderGraphPassInstrumentation,
|
|
101
104
|
RenderGraphPassInstrumentationScope,
|
|
102
105
|
RenderGraphPassRunner,
|
|
106
|
+
RenderGraphResourceAllocationInspection,
|
|
103
107
|
} from './types.js';
|
package/src/types.ts
CHANGED
|
@@ -186,6 +186,11 @@ export interface GraphAccessInfo {
|
|
|
186
186
|
readonly usage: GraphBufferAccess | GraphTextureAccess;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
export type CompiledResourceByteSizeUnknownReason =
|
|
190
|
+
| 'imported-owner'
|
|
191
|
+
| 'format-or-layout-unknown'
|
|
192
|
+
| 'not-allocated';
|
|
193
|
+
|
|
189
194
|
/** Detached physical allocation facts for one compiled graph resource. */
|
|
190
195
|
export type CompiledResourceDescriptor =
|
|
191
196
|
| {
|
|
@@ -226,6 +231,8 @@ export interface CompiledRenderGraphInfo {
|
|
|
226
231
|
readonly physicalAllocationKey?: string | undefined;
|
|
227
232
|
/** Descriptor-derived byte size when the format is uncompressed and known. */
|
|
228
233
|
readonly byteSize?: number | undefined;
|
|
234
|
+
/** Why byteSize is absent; no zero is fabricated for an unknown owner/format. */
|
|
235
|
+
readonly byteSizeUnknownReason?: CompiledResourceByteSizeUnknownReason | undefined;
|
|
229
236
|
/** Texture format retained for producer-owned resource inspection. */
|
|
230
237
|
readonly format?: TextureFormat | undefined;
|
|
231
238
|
/** Texture allocation facts retained for capability/lifetime inspection. */
|
|
@@ -236,6 +243,62 @@ export interface CompiledRenderGraphInfo {
|
|
|
236
243
|
}[];
|
|
237
244
|
/** Monotonic graph generation assigned when this graph was compiled. */
|
|
238
245
|
readonly generation: number;
|
|
246
|
+
/**
|
|
247
|
+
* Graph-owned logical allocation facts. Imported resources are counted only
|
|
248
|
+
* as references; their physical bytes stay with the importing owner.
|
|
249
|
+
*/
|
|
250
|
+
readonly resourceAllocation?: RenderGraphResourceAllocationInspection | undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* RenderGraph-owned logical allocation facts. Byte totals include only
|
|
255
|
+
* descriptor-derived sizes known by this package; physical GPU residency and
|
|
256
|
+
* imported-resource bytes remain unknown by contract.
|
|
257
|
+
*/
|
|
258
|
+
export interface RenderGraphResourceAllocationInspection {
|
|
259
|
+
readonly unit: 'engine-allocation-bytes';
|
|
260
|
+
readonly physicalResidency: 'unknown';
|
|
261
|
+
readonly liveBytes: number;
|
|
262
|
+
readonly pendingRetirementBytes: number;
|
|
263
|
+
readonly peakBytes: number;
|
|
264
|
+
readonly successfulAllocationCount: number;
|
|
265
|
+
readonly successfulAllocationBytes: number;
|
|
266
|
+
readonly pendingRetirementCount: number;
|
|
267
|
+
readonly retiredBytes: number;
|
|
268
|
+
readonly failedAllocationRollbacks: number;
|
|
269
|
+
readonly failedAllocationRollbackBytes: number;
|
|
270
|
+
readonly unknownByteSizeCount: number;
|
|
271
|
+
readonly importedResourceCount: number;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** One graph generation included in the renderer-wide replacement ledger. */
|
|
275
|
+
export interface RenderGraphGenerationAllocationEntry {
|
|
276
|
+
readonly generation: number;
|
|
277
|
+
/** A graph may be both the current active graph and an unsubmitted candidate. */
|
|
278
|
+
readonly roles: readonly ('active' | 'candidate' | 'retiring')[];
|
|
279
|
+
readonly retirement: 'active' | 'pending' | 'failed';
|
|
280
|
+
readonly allocation: RenderGraphResourceAllocationInspection;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Renderer-wide logical allocation facts across graph generations. Current
|
|
285
|
+
* live and pending bytes are summed across the generations that overlap;
|
|
286
|
+
* peakBytes is captured from simultaneous owner events instead of summing
|
|
287
|
+
* historical per-graph peaks. This does not estimate physical VRAM or include
|
|
288
|
+
* imported owner bytes.
|
|
289
|
+
*/
|
|
290
|
+
export interface RenderGraphGenerationAllocationInspection {
|
|
291
|
+
readonly unit: 'engine-allocation-bytes';
|
|
292
|
+
readonly physicalResidency: 'unknown';
|
|
293
|
+
readonly availability: 'complete' | 'partial' | 'unavailable';
|
|
294
|
+
readonly unavailableGenerationCount: number;
|
|
295
|
+
readonly entries: readonly RenderGraphGenerationAllocationEntry[];
|
|
296
|
+
readonly liveBytes: number;
|
|
297
|
+
readonly pendingRetirementBytes: number;
|
|
298
|
+
/** Peak simultaneous logical bytes observed by this renderer owner. */
|
|
299
|
+
readonly peakBytes: number;
|
|
300
|
+
readonly failedRetirementCount: number;
|
|
301
|
+
readonly failedRetirementBytes: number;
|
|
239
302
|
}
|
|
240
303
|
|
|
241
304
|
export interface RenderGraphFrame {
|