Skip to content

Commit 80f2ab5

Browse files
authored
FrameGraph: Fix some memory leaks and a few errors in NRGE + switch build methods to async (#17487)
1 parent 30b71a6 commit 80f2ab5

File tree

15 files changed

+199
-90
lines changed

15 files changed

+199
-90
lines changed

packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
WritableObject,
1212
IShadowLight,
1313
INodeRenderGraphCustomBlockDescription,
14+
Immutable,
1415
} from "core/index";
1516
import { Observable } from "../../Misc/observable";
1617
import { NodeRenderGraphOutputBlock } from "./Blocks/outputBlock";
@@ -83,7 +84,12 @@ export class NodeRenderGraph {
8384
public attachedBlocks: NodeRenderGraphBlock[] = [];
8485

8586
/**
86-
* Observable raised when the node render graph is built
87+
* Observable raised before the node render graph is built
88+
*/
89+
public onBeforeBuildObservable = new Observable<FrameGraph>();
90+
91+
/**
92+
* Observable raised after the node render graph is built
8793
* Note that this is the same observable as the one in the underlying FrameGraph!
8894
*/
8995
public get onBuildObservable() {
@@ -136,6 +142,13 @@ export class NodeRenderGraph {
136142
return this._scene;
137143
}
138144

145+
/**
146+
* Gets the options used to create this node render graph
147+
*/
148+
public get options(): Immutable<INodeRenderGraphCreateOptions> {
149+
return this._options;
150+
}
151+
139152
/**
140153
* Creates a new node render graph
141154
* @param name defines the name of the node render graph
@@ -163,9 +176,7 @@ export class NodeRenderGraph {
163176

164177
if (options.rebuildGraphOnEngineResize) {
165178
this._resizeObserver = this._engine.onResizeObservable.add(async () => {
166-
this.build();
167-
168-
await this.whenReadyAsync();
179+
await this.buildAsync(false, true, false);
169180
});
170181
}
171182
}
@@ -285,14 +296,29 @@ export class NodeRenderGraph {
285296
}
286297

287298
/**
288-
* Build the final list of blocks that will be executed by the "execute" method
299+
* @deprecated Use buildAsync instead
289300
* @param dontBuildFrameGraph If the underlying frame graph should not be built (default: false)
290301
*/
291-
public build(dontBuildFrameGraph = false) {
302+
public build(dontBuildFrameGraph = false): void {
303+
void this.buildAsync(dontBuildFrameGraph, false, false);
304+
}
305+
306+
/**
307+
* Build the final list of blocks that will be executed by the "execute" method.
308+
* It also builds the underlying frame graph unless specified otherwise.
309+
* @param dontBuildFrameGraph If the underlying frame graph should not be built (default: false)
310+
* @param waitForReadiness If the method should wait for the frame graph to be ready before resolving (default: true). Note that this parameter has no effect if "dontBuildFrameGraph" is true.
311+
* @param setAsSceneFrameGraph If the built frame graph must be set as the scene's frame graph (default: true)
312+
*/
313+
public async buildAsync(dontBuildFrameGraph = false, waitForReadiness = true, setAsSceneFrameGraph = true): Promise<void> {
292314
if (!this.outputBlock) {
293315
throw new Error("You must define the outputBlock property before building the node render graph");
294316
}
295317

318+
if (setAsSceneFrameGraph) {
319+
this._scene.frameGraph = this._frameGraph;
320+
}
321+
296322
this._initializeBlock(this.outputBlock);
297323

298324
this._frameGraph.clear();
@@ -306,6 +332,8 @@ export class NodeRenderGraph {
306332
this._autoFillExternalInputs();
307333
}
308334

335+
this.onBeforeBuildObservable.notifyObservers(this._frameGraph);
336+
309337
// Make sure that one of the object renderer is flagged as the main object renderer
310338
const objectRendererBlocks = this.getBlocksByPredicate<NodeRenderGraphBaseObjectRendererBlock>((block) => block instanceof NodeRenderGraphBaseObjectRendererBlock);
311339
if (objectRendererBlocks.length > 0 && !objectRendererBlocks.find((block) => block.isMainObjectRenderer)) {
@@ -316,7 +344,7 @@ export class NodeRenderGraph {
316344
this.outputBlock.build(state);
317345

318346
if (!dontBuildFrameGraph) {
319-
this._frameGraph.build();
347+
await this._frameGraph.buildAsync(waitForReadiness);
320348
}
321349
} finally {
322350
this._buildId = NodeRenderGraph._BuildIdGenerator++;
@@ -368,16 +396,14 @@ export class NodeRenderGraph {
368396
* Returns a promise that resolves when the node render graph is ready to be executed
369397
* This method must be called after the graph has been built (NodeRenderGraph.build called)!
370398
* @param timeStep Time step in ms between retries (default is 16)
371-
* @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 5000)
399+
* @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 10000)
372400
* @returns The promise that resolves when the graph is ready
373401
*/
374402
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
375-
public whenReadyAsync(timeStep = 16, maxTimeout = 5000): Promise<void> {
403+
public async whenReadyAsync(timeStep = 16, maxTimeout = 10000): Promise<void> {
376404
this._frameGraph.pausedExecution = true;
377-
// eslint-disable-next-line github/no-then
378-
return this._frameGraph.whenReadyAsync(timeStep, maxTimeout).then(() => {
379-
this._frameGraph.pausedExecution = false;
380-
});
405+
await this._frameGraph.whenReadyAsync(timeStep, maxTimeout);
406+
this._frameGraph.pausedExecution = false;
381407
}
382408

383409
/**
@@ -667,6 +693,7 @@ export class NodeRenderGraph {
667693

668694
/**
669695
* Makes a duplicate of the current node render graph.
696+
* Note that you should call buildAsync() on the returned graph to make it usable.
670697
* @param name defines the name to use for the new node render graph
671698
* @returns the new node render graph
672699
*/
@@ -678,7 +705,6 @@ export class NodeRenderGraph {
678705

679706
clone.parseSerializedObject(serializationObject);
680707
clone._buildId = this._buildId;
681-
clone.build();
682708

683709
return clone;
684710
}
@@ -747,11 +773,11 @@ export class NodeRenderGraph {
747773
* @param nodeRenderGraphOptions defines options to use when creating the node render graph
748774
* @returns a new NodeRenderGraph
749775
*/
750-
public static CreateDefault(name: string, scene: Scene, nodeRenderGraphOptions?: INodeRenderGraphCreateOptions): NodeRenderGraph {
776+
public static async CreateDefaultAsync(name: string, scene: Scene, nodeRenderGraphOptions?: INodeRenderGraphCreateOptions): Promise<NodeRenderGraph> {
751777
const renderGraph = new NodeRenderGraph(name, scene, nodeRenderGraphOptions);
752778

753779
renderGraph.setToDefault();
754-
renderGraph.build();
780+
await renderGraph.buildAsync(false, true, false);
755781

756782
return renderGraph;
757783
}
@@ -769,7 +795,7 @@ export class NodeRenderGraph {
769795

770796
renderGraph.parseSerializedObject(source);
771797
if (!skipBuild) {
772-
renderGraph.build();
798+
void renderGraph.buildAsync();
773799
}
774800

775801
return renderGraph;
@@ -793,12 +819,12 @@ export class NodeRenderGraph {
793819
skipBuild: boolean = true
794820
): Promise<NodeRenderGraph> {
795821
if (snippetId === "_BLANK") {
796-
return Promise.resolve(NodeRenderGraph.CreateDefault("blank", scene, nodeRenderGraphOptions));
822+
return NodeRenderGraph.CreateDefaultAsync("blank", scene, nodeRenderGraphOptions);
797823
}
798824

799825
return new Promise((resolve, reject) => {
800826
const request = new WebRequest();
801-
request.addEventListener("readystatechange", () => {
827+
request.addEventListener("readystatechange", async () => {
802828
if (request.readyState == 4) {
803829
if (request.status == 200) {
804830
const snippet = JSON.parse(JSON.parse(request.responseText).jsonPayload);
@@ -813,7 +839,7 @@ export class NodeRenderGraph {
813839

814840
try {
815841
if (!skipBuild) {
816-
nodeRenderGraph.build();
842+
await nodeRenderGraph.buildAsync();
817843
}
818844
resolve(nodeRenderGraph);
819845
} catch (err) {

packages/dev/core/src/FrameGraph/Passes/pass.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,7 @@ export class FrameGraphPass<T extends FrameGraphContext> implements IFrameGraphP
3737
public _isValid(): Nullable<string> {
3838
return this._executeFunc !== undefined ? null : "Execute function is not set (call setExecuteFunc to set it)";
3939
}
40+
41+
/** @internal */
42+
public _dispose() {}
4043
}

packages/dev/core/src/FrameGraph/Passes/renderPass.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,9 @@ export class FrameGraphRenderPass extends FrameGraphPass<FrameGraphRenderContext
146146
? null
147147
: "Render target and render target depth cannot both be undefined.";
148148
}
149+
150+
/** @internal */
151+
public override _dispose() {
152+
this._frameGraphRenderTarget?.dispose();
153+
}
149154
}

packages/dev/core/src/FrameGraph/frameGraph.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class FrameGraph implements IDisposable {
3434
private readonly _tasks: FrameGraphTask[] = [];
3535
private readonly _passContext: FrameGraphContext;
3636
private readonly _renderContext: FrameGraphRenderContext;
37+
private readonly _initAsyncPromises: Promise<void>[] = [];
3738
private _currentProcessedTask: FrameGraphTask | null = null;
3839
private _whenReadyAsyncCancel: Nullable<() => void> = null;
3940

@@ -147,6 +148,7 @@ export class FrameGraph implements IDisposable {
147148
}
148149

149150
this._tasks.push(task);
151+
this._initAsyncPromises.push(task.initAsync());
150152
}
151153

152154
/**
@@ -203,14 +205,37 @@ export class FrameGraph implements IDisposable {
203205
return pass;
204206
}
205207

208+
/** @internal */
209+
public async _whenAsynchronousInitializationDoneAsync(): Promise<void> {
210+
if (this._initAsyncPromises.length > 0) {
211+
await Promise.all(this._initAsyncPromises);
212+
this._initAsyncPromises.length = 0;
213+
}
214+
}
215+
216+
/**
217+
* @deprecated Use buildAsync instead
218+
*/
219+
public build(): void {
220+
void this.buildAsync(false);
221+
}
222+
223+
private _built = false; // TODO: to be removed when build() is removed
224+
206225
/**
207226
* Builds the frame graph.
208227
* This method should be called after all tasks have been added to the frame graph (FrameGraph.addTask) and before the graph is executed (FrameGraph.execute).
228+
* @param waitForReadiness If true, the method will wait for the frame graph to be ready before returning (default is true)
209229
*/
210-
public build(): void {
230+
public async buildAsync(waitForReadiness = true): Promise<void> {
211231
this.textureManager._releaseTextures(false);
212232

233+
this.pausedExecution = true;
234+
this._built = false;
235+
213236
try {
237+
await this._whenAsynchronousInitializationDoneAsync();
238+
214239
for (const task of this._tasks) {
215240
task._reset();
216241

@@ -233,12 +258,21 @@ export class FrameGraph implements IDisposable {
233258
task.onTexturesAllocatedObservable.notifyObservers(this._renderContext);
234259
}
235260

261+
this._built = true;
262+
236263
this.onBuildObservable.notifyObservers(this);
264+
265+
if (waitForReadiness) {
266+
await this.whenReadyAsync();
267+
}
237268
} catch (e) {
238269
this._tasks.length = 0;
239270
this._currentProcessedTask = null;
240271
this.textureManager._isRecordingTask = false;
241272
throw e;
273+
} finally {
274+
this.pausedExecution = false;
275+
this._built = true;
242276
}
243277
}
244278

@@ -256,14 +290,28 @@ export class FrameGraph implements IDisposable {
256290
}
257291

258292
/**
259-
* Returns a promise that resolves when the frame graph is ready to be executed
260-
* This method must be called after the graph has been built (FrameGraph.build called)!
293+
* Returns a promise that resolves when the frame graph is ready to be executed.
294+
* In general, calling “await buildAsync()” should suffice, as this function also waits for readiness by default.
261295
* @param timeStep Time step in ms between retries (default is 16)
262-
* @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 5000)
296+
* @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 10000)
263297
* @returns The promise that resolves when the graph is ready
264298
*/
265-
public async whenReadyAsync(timeStep = 16, maxTimeout = 5000): Promise<void> {
299+
public async whenReadyAsync(timeStep = 16, maxTimeout = 10000): Promise<void> {
266300
let firstNotReadyTask: FrameGraphTask | null = null;
301+
302+
// TODO: to be removed when build() is removed
303+
await new Promise((resolve) => {
304+
const checkBuilt = () => {
305+
if (this._built) {
306+
resolve(void 0);
307+
return;
308+
}
309+
setTimeout(checkBuilt, 16);
310+
};
311+
checkBuilt();
312+
});
313+
// END TODO
314+
267315
return await new Promise((resolve, reject) => {
268316
this._whenReadyAsyncCancel = _RetryWithInterval(
269317
() => {

packages/dev/core/src/FrameGraph/frameGraphRenderTarget.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,8 @@ export class FrameGraphRenderTarget {
9494

9595
return this._renderTargetDepth === other._renderTargetDepth;
9696
}
97+
98+
public dispose() {
99+
this._renderTargetWrapper?.dispose();
100+
}
97101
}

packages/dev/core/src/FrameGraph/frameGraphTask.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ export abstract class FrameGraphTask {
6666
*/
6767
public abstract record(skipCreationOfDisabledPasses?: boolean): void;
6868

69+
/**
70+
* This function is called once after the task has been added to the frame graph and before the frame graph is built for the first time.
71+
* This allows you to initialize asynchronous resources, which is not possible in the constructor.
72+
* @returns A promise that resolves when the initialization is complete.
73+
*/
74+
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
75+
public initAsync(): Promise<void> {
76+
return Promise.resolve();
77+
}
78+
6979
/**
7080
* An observable that is triggered after the textures have been allocated.
7181
*/
@@ -112,6 +122,12 @@ export abstract class FrameGraphTask {
112122

113123
/** @internal */
114124
public _reset() {
125+
for (const pass of this._passes) {
126+
pass._dispose();
127+
}
128+
for (const pass of this._passesDisabled) {
129+
pass._dispose();
130+
}
115131
this._passes.length = 0;
116132
this._passesDisabled.length = 0;
117133
}

packages/dev/core/src/FrameGraph/frameGraphTypes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,7 @@ export interface IFrameGraphPass {
9797

9898
/** @internal */
9999
_isValid(): Nullable<string>;
100+
101+
/** @internal */
102+
_dispose(): void;
100103
}

packages/dev/core/src/Misc/screenshotTools.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -714,15 +714,7 @@ export async function CreateScreenshotForFrameGraphAsync(
714714
*/
715715
engine.getCaps().parallelShaderCompile = undefined;
716716

717-
frameGraph.build();
718-
719-
// We don't want the frame graph to render while waiting for whenReadyAsync to complete
720-
frameGraph.pausedExecution = true;
721-
722-
await frameGraph.whenReadyAsync();
723-
724-
// eslint-disable-next-line require-atomic-updates
725-
frameGraph.pausedExecution = false;
717+
await frameGraph.buildAsync();
726718

727719
const numberOfFrames = numberOfFramesToRender ?? (textureManager.hasHistoryTextures ? 32 : 1);
728720

@@ -752,9 +744,7 @@ export async function CreateScreenshotForFrameGraphAsync(
752744

753745
textureManager.resetBackBufferTextures();
754746

755-
frameGraph.build();
756-
757-
await frameGraph.whenReadyAsync();
747+
await frameGraph.buildAsync();
758748

759749
// eslint-disable-next-line require-atomic-updates
760750
frameGraph.pausedExecution = pausedExecution;

0 commit comments

Comments
 (0)