Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Viewer auto suspend rendering when scene is idle #15864

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6d6a68f
Suspend rendering when viewer is offscreen (should move to viewerFact…
ryantrem Nov 15, 2024
206a5a1
Merge branch 'master' into viewer-offscreen-suspend-rendering
ryantrem Nov 15, 2024
f320ee0
Add dynamic top/bottom content to test app
ryantrem Nov 15, 2024
92d02cc
Move interaction observer to viewerFactory
ryantrem Nov 15, 2024
ac950e6
Merge branch 'master' into viewer-offscreen-suspend-rendering
ryantrem Nov 15, 2024
aa7a1b1
Minor cleanup
ryantrem Nov 15, 2024
e2ed4f7
Merge branch 'master' into viewer-offscreen-suspend-rendering
ryantrem Nov 15, 2024
7f07c1d
Fix small merge error
ryantrem Nov 15, 2024
9ed61d6
Testing out camera updates
ryantrem Nov 16, 2024
315ba72
Switch to correct observable
ryantrem Nov 18, 2024
b466a02
Merge branch 'master' into viewer-idle-suspend-rendering
ryantrem Nov 18, 2024
544d77e
More work on conditional rendering
ryantrem Nov 19, 2024
86abefe
More work on conditional rendering
ryantrem Nov 19, 2024
17340e8
SnapshotRenderingHelper timing
ryantrem Nov 20, 2024
1355e27
Change name to isReady
ryantrem Nov 20, 2024
66a6424
Merge branch 'master' into viewer-idle-suspend-rendering
ryantrem Nov 20, 2024
c0f50f0
Handle animations
ryantrem Nov 20, 2024
a028b59
Logging
ryantrem Nov 20, 2024
4d6029d
Add configuration for renderWhenIdle
ryantrem Nov 20, 2024
5984763
Simplify scene mutation logic
ryantrem Nov 22, 2024
c79f6a2
Clear color sets scene mutated
ryantrem Nov 22, 2024
5d349ec
Merge branch 'master' into viewer-idle-suspend-rendering
ryantrem Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions packages/dev/core/src/Misc/snapshotRenderingHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class SnapshotRenderingHelper {
private _disableRenderingRefCount = 0;
private _currentPerformancePriorityMode = ScenePerformancePriority.BackwardCompatible;
private _pendingCurrentPerformancePriorityMode?: ScenePerformancePriority;
private _isEnabling = false;

/**
* Creates a new snapshot rendering helper
Expand Down Expand Up @@ -100,6 +101,10 @@ export class SnapshotRenderingHelper {
});
}

public get isReady() {
return !this._isEnabling;
}

/**
* Enable snapshot rendering
* Use this method instead of engine.snapshotRendering=true, to make sure everything is ready before enabling snapshot rendering.
Expand All @@ -114,6 +119,7 @@ export class SnapshotRenderingHelper {
return;
}

this._isEnabling = true;
this._disableRenderingRefCount = 0;

this._currentPerformancePriorityMode = this._pendingCurrentPerformancePriorityMode ?? this._scene.performancePriority;
Expand All @@ -122,13 +128,24 @@ export class SnapshotRenderingHelper {

this._scene.executeWhenReady(() => {
if (this._disableRenderingRefCount > 0) {
this._isEnabling = false;
return;
}

// Make sure a full frame is rendered before enabling snapshot rendering, so use "+2" instead of "+1"
this._executeAtFrame(this._engine.frameId + 2, () => {
const targetFrameId = this._engine.frameId + 2;
this._executeAtFrame(targetFrameId, () => {
this._engine.snapshotRendering = true;
});

// Render one frame with snapshot rendering enabled to make sure everything is ready
this._executeAtFrame(
targetFrameId + 1,
() => {
this._isEnabling = false;
},
"always"
);
});
}

Expand Down Expand Up @@ -158,7 +175,7 @@ export class SnapshotRenderingHelper {
}
this._pendingCurrentPerformancePriorityMode = undefined;
},
true
"whenDisabled"
);
});
}
Expand Down Expand Up @@ -274,9 +291,9 @@ export class SnapshotRenderingHelper {
}
}

private _executeAtFrame(frameId: number, func: () => void, executeWhenModeIsDisabled = false) {
private _executeAtFrame(frameId: number, func: () => void, mode: "whenEnabled" | "whenDisabled" | "always" = "whenEnabled") {
const obs = this._engine.onEndFrameObservable.add(() => {
if ((this._disableRenderingRefCount > 0 && !executeWhenModeIsDisabled) || (this._disableRenderingRefCount === 0 && executeWhenModeIsDisabled)) {
if (mode !== "always" && ((this._disableRenderingRefCount > 0 && mode === "whenEnabled") || (this._disableRenderingRefCount === 0 && mode === "whenDisabled"))) {
this._engine.onEndFrameObservable.remove(obs);
return;
}
Expand Down
79 changes: 75 additions & 4 deletions packages/tools/viewer-alpha/src/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { Viewport } from "core/Maths/math.viewport";
import { GetHotSpotToRef } from "core/Meshes/abstractMesh.hotSpot";
import { SnapshotRenderingHelper } from "core/Misc/snapshotRenderingHelper";
import { BuildTuple } from "core/Misc/arrayTools";
import { Logger } from "core/Misc/logger";

const toneMappingOptions = ["none", "standard", "aces", "neutral"] as const;
export type ToneMapping = (typeof toneMappingOptions)[number];
Expand Down Expand Up @@ -146,6 +147,11 @@ export type ViewerDetails = {
* @returns A token that should be disposed when the request for suspending rendering is no longer needed.
*/
suspendRendering(): IDisposable;

/**
* Marks the scene as mutated, which will trigger a render on the next frame (unless rendering is suspended).
*/
markSceneMutated(): void;
};

export type ViewerOptions = Partial<
Expand All @@ -154,6 +160,13 @@ export type ViewerOptions = Partial<
* Called once when the viewer is initialized and provides viewer details that can be used for advanced customization.
*/
onInitialized: (details: Readonly<ViewerDetails>) => void;

/**
* When enabled, rendering will be suspended when no scene state driven by the Viewer has changed.
* This can reduce resource CPU/GPU pressure when the scene is static.
* Enabled by default.
*/
autoSuspendRendering: boolean;
}>
>;

Expand Down Expand Up @@ -208,6 +221,8 @@ export class ViewerHotSpotResult {
public visibility: number = NaN;
}

// TODO: Need to add properties for anything in the upper layers, such as clearColor, so we can mark the scene as mutated.

/**
* @experimental
* Provides an experience for viewing a single 3D model.
Expand Down Expand Up @@ -301,6 +316,8 @@ export class Viewer implements IDisposable {
private _contrast: number;
private _exposure: number;

private readonly _autoSuspendRendering: boolean = true;
private _sceneMutated = false;
private _suspendRenderCount = 0;
private _isDisposed = false;

Expand All @@ -322,6 +339,7 @@ export class Viewer implements IDisposable {
private readonly _engine: AbstractEngine,
options?: ViewerOptions
) {
this._autoSuspendRendering = options?.autoSuspendRendering ?? true;
{
const scene = new Scene(this._engine);

Expand Down Expand Up @@ -360,12 +378,21 @@ export class Viewer implements IDisposable {
});

const camera = new ArcRotateCamera("Viewer Default Camera", 0, 0, 1, Vector3.Zero(), scene);
camera.onViewMatrixChangedObservable.add(() => {
this._sceneMutated = true;
});

scene.onClearColorChangedObservable.add(() => {
this._sceneMutated = true;
});

this._details = {
viewer: this,
scene,
camera,
model: null,
suspendRendering: this._suspendRendering.bind(this),
markSceneMutated: () => (this._sceneMutated = true),
};
}
this._details.scene.skipFrustumClipping = true;
Expand Down Expand Up @@ -435,6 +462,7 @@ export class Viewer implements IDisposable {
this._snapshotHelper.disableSnapshotRendering();
material.microSurface = 1.0 - value;
this._snapshotHelper.enableSnapshotRendering();
this._sceneMutated = true;
}
}
this.onSkyboxBlurChanged.notifyObservers();
Expand Down Expand Up @@ -501,6 +529,7 @@ export class Viewer implements IDisposable {
this._details.scene.imageProcessingConfiguration.isEnabled = this._toneMappingEnabled || this._contrast !== 1 || this._exposure !== 1;

this._snapshotHelper.enableSnapshotRendering();
this._sceneMutated = true;
}

/**
Expand Down Expand Up @@ -609,6 +638,7 @@ export class Viewer implements IDisposable {
this._activeAnimation.goToFrame(value * (this._activeAnimation.to - this._activeAnimation.from));
this.onAnimationProgressChanged.notifyObservers();
this._autoRotationBehavior.resetLastInteractionTime();
this._sceneMutated = true;
}
}

Expand Down Expand Up @@ -700,6 +730,7 @@ export class Viewer implements IDisposable {
this._isLoadingModel = false;
this.onLoadingProgressChanged.notifyObservers();
this._snapshotHelper.enableSnapshotRendering();
this._sceneMutated = true;
}
});
}
Expand Down Expand Up @@ -785,6 +816,7 @@ export class Viewer implements IDisposable {
throw e;
} finally {
this._snapshotHelper.enableSnapshotRendering();
this._sceneMutated = true;
}
});
}
Expand Down Expand Up @@ -898,6 +930,22 @@ export class Viewer implements IDisposable {
return true;
}

private get _shouldRender() {
// We should render if:
// 1. Auto suspend rendering is disabled.
// 2. The scene has been mutated.
// 3. The snapshot helper is not yet in a ready state.
// 4. An animation is playing.
// 5. Animation is paused, but any individual animatable hasn't transitioned to a paused state yet.
return (
!this._autoSuspendRendering ||
this._sceneMutated ||
!this._snapshotHelper.isReady ||
this.isAnimationPlaying ||
this._details.model?.animationGroups.some((group) => group.animatables.some((animatable) => animatable.animationStarted))
);
}

private _suspendRendering(): IDisposable {
this._renderLoopController?.dispose();
this._suspendRenderCount++;
Expand All @@ -917,11 +965,30 @@ export class Viewer implements IDisposable {

private _beginRendering(): void {
if (!this._renderLoopController) {
let renderedLastFrame = false;
const render = () => {
this._details.scene.render();
if (this.isAnimationPlaying) {
this.onAnimationProgressChanged.notifyObservers();
this._autoRotationBehavior.resetLastInteractionTime();
// When we resume rendering, continue rendering until the scene reports it is ready.
const shouldRender = this._shouldRender || (renderedLastFrame && !this._details.scene.isReady(true));
if (shouldRender) {
this._sceneMutated = false;
this._details.scene.render();

if (!renderedLastFrame) {
Logger.Log("Viewer Resumed Rendering");
renderedLastFrame = true;
}

if (this.isAnimationPlaying) {
this.onAnimationProgressChanged.notifyObservers();
this._autoRotationBehavior.resetLastInteractionTime();
}
} else {
this._details.camera.update();

if (renderedLastFrame) {
Logger.Log("Viewer Suspended Rendering");
renderedLastFrame = false;
}
}
};

Expand All @@ -934,6 +1001,10 @@ export class Viewer implements IDisposable {
disposed = true;
this._engine.stopRenderLoop(render);
this._renderLoopController = null;

if (renderedLastFrame) {
Logger.Log("Viewer Suspended Rendering");
}
}
},
};
Expand Down
9 changes: 8 additions & 1 deletion packages/tools/viewer-alpha/src/viewerElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,12 @@ export class HTML3DElement extends LitElement {
@property({ reflect: true })
public engine: NonNullable<CanvasViewerOptions["engine"]> = getDefaultEngine();

/**
* When true, the scene will be rendered even if no scene state has changed.
*/
@property({ attribute: "render-when-idle", type: Boolean })
public renderWhenIdle = false;

/**
* The model URL.
*/
Expand Down Expand Up @@ -725,7 +731,7 @@ export class HTML3DElement extends LitElement {
override update(changedProperties: PropertyValues<this>): void {
super.update(changedProperties);

if (changedProperties.get("engine")) {
if (changedProperties.get("engine") || changedProperties.has("renderWhenIdle")) {
this._tearDownViewer();
this._setupViewer();
} else {
Expand Down Expand Up @@ -935,6 +941,7 @@ export class HTML3DElement extends LitElement {

await createViewerForCanvas(canvas, {
engine: this.engine,
autoSuspendRendering: !this.renderWhenIdle,
onInitialized: (details) => {
this._viewerDetails = details;

Expand Down
15 changes: 9 additions & 6 deletions packages/tools/viewer-alpha/src/viewerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,6 @@ export async function createViewerForCanvas(canvas: HTMLCanvasElement, options?:
const finalOptions = { ...defaultCanvasViewerOptions, ...options };
const disposeActions: (() => void)[] = [];

// If the canvas is resized, note that the engine needs a resize, but don't resize it here as it will result in flickering.
let needsResize = false;
const resizeObserver = new ResizeObserver(() => (needsResize = true));
resizeObserver.observe(canvas);
disposeActions.push(() => resizeObserver.disconnect());

// Create an engine instance.
let engine: AbstractEngine;
switch (finalOptions.engine ?? getDefaultEngine()) {
Expand All @@ -60,6 +54,15 @@ export async function createViewerForCanvas(canvas: HTMLCanvasElement, options?:
// Override the onInitialized callback to add in some specific behavior.
const onInitialized = finalOptions.onInitialized;
finalOptions.onInitialized = (details) => {
// If the canvas is resized, note that the engine needs a resize, but don't resize it here as it will result in flickering.
let needsResize = false;
const resizeObserver = new ResizeObserver(() => {
needsResize = true;
details.markSceneMutated();
});
resizeObserver.observe(canvas);
disposeActions.push(() => resizeObserver.disconnect());

// Resize if needed right before rendering the Viewer scene to avoid any flickering.
const beforeRenderObserver = details.scene.onBeforeRenderObservable.add(() => {
if (needsResize) {
Expand Down
2 changes: 2 additions & 0 deletions packages/tools/viewer-alpha/test/apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@
await new Promise((resolve) => setTimeout(resolve, 2000));
//viewerElement.source = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/BrainStem/glTF-Binary/BrainStem.glb";
//viewerElement.environment = "";
//viewerElement.skyboxBlur = 0;
//viewerElement.contrast = 4;
// error case
//viewerElement.source = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/BrainStem/glTF-Binary/BrainStem2.glb";
await new Promise((resolve) => setTimeout(resolve, 2000));
Expand Down