Skip to content

Commit 6f7b10f

Browse files
authored
SFE: Display optimizer and runtime creation performance in SFE, and add an optimizer toggle (#127)
1 parent c1a669f commit 6f7b10f

File tree

8 files changed

+157
-12
lines changed

8 files changed

+157
-12
lines changed

packages/editor/src/components/propertyTab/propertyTabComponent.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,27 @@ interface IPropertyTabComponentState {
3737
currentFrameNodePort: Nullable<FrameNodePort>;
3838
currentNodePort: Nullable<NodePort>;
3939
uploadInProgress: boolean;
40+
optimize: Nullable<boolean>;
4041
}
4142

4243
export class PropertyTabComponent extends react.Component<IPropertyTabComponentProps, IPropertyTabComponentState> {
4344
private _onResetRequiredObserver?: Observer<boolean>;
45+
private _onOptimizerEnabledChangedObserver?: Observer<boolean>;
46+
4447
// private _modeSelect: React.RefObject<OptionsLineComponent>;
4548

4649
constructor(props: IPropertyTabComponentProps) {
4750
super(props);
4851

52+
const optimize = this.props.globalState.optimizerEnabled?.value || null;
53+
4954
this.state = {
5055
currentNode: null,
5156
currentFrame: null,
5257
currentFrameNodePort: null,
5358
currentNodePort: null,
5459
uploadInProgress: false,
60+
optimize,
5561
};
5662

5763
// this._modeSelect = React.createRef();
@@ -103,12 +109,23 @@ export class PropertyTabComponent extends react.Component<IPropertyTabComponentP
103109
this._onResetRequiredObserver = this.props.globalState.onResetRequiredObservable?.add(() => {
104110
this.forceUpdate();
105111
});
112+
113+
if (this.props.globalState.optimizerEnabled) {
114+
this._onOptimizerEnabledChangedObserver = this.props.globalState.optimizerEnabled.onChangedObservable.add(
115+
(value: boolean) => {
116+
this.setState({ optimize: value });
117+
}
118+
);
119+
}
106120
}
107121

108122
override componentWillUnmount() {
109123
if (this._onResetRequiredObserver) {
110124
this._onResetRequiredObserver.remove();
111125
}
126+
if (this._onOptimizerEnabledChangedObserver) {
127+
this._onOptimizerEnabledChangedObserver.remove();
128+
}
112129
}
113130

114131
processInputBlockUpdate(ib: AnyInputBlock) {
@@ -317,6 +334,17 @@ export class PropertyTabComponent extends react.Component<IPropertyTabComponentP
317334
this.props.globalState.onlyShowCustomBlocksObservable.notifyObservers(value);
318335
}}
319336
/>
337+
{this.props.globalState.optimizerEnabled && (
338+
<CheckBoxLineComponent
339+
label="Optimize Smart Filter"
340+
isSelected={() => !!this.state.optimize}
341+
onSelect={(value: boolean) => {
342+
if (this.props.globalState.optimizerEnabled) {
343+
this.props.globalState.optimizerEnabled.value = value;
344+
}
345+
}}
346+
/>
347+
)}
320348
</LineContainerComponent>
321349
{(this.props.globalState.loadSmartFilter ||
322350
this.props.globalState.downloadSmartFilter ||

packages/editor/src/globalState.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { GraphNode } from "@babylonjs/shared-ui-components/nodeGraphSystem/
1313
import type { BlockEditorRegistration } from "./configuration/blockEditorRegistration.js";
1414
import type { IPortData } from "@babylonjs/shared-ui-components/nodeGraphSystem/interfaces/portData.js";
1515
import { BlockTools } from "./blockTools.js";
16+
import type { ObservableProperty } from "./helpers/observableProperty.js";
1617

1718
export type TexturePreset = {
1819
name: string;
@@ -30,6 +31,8 @@ export class GlobalState {
3031

3132
onSmartFilterLoadedObservable: Nullable<Observable<SmartFilter>>;
3233

34+
optimizerEnabled: Nullable<ObservableProperty<boolean>>;
35+
3336
smartFilter: SmartFilter;
3437

3538
blockEditorRegistration: BlockEditorRegistration;
@@ -95,6 +98,7 @@ export class GlobalState {
9598
engine: Nullable<ThinEngine>,
9699
onNewEngine: Nullable<(engine: ThinEngine) => void>,
97100
onSmartFilterLoadedObservable: Nullable<Observable<SmartFilter>>,
101+
optimizerEnabled: Nullable<ObservableProperty<boolean>>,
98102
smartFilter: Nullable<SmartFilter>,
99103
blockEditorRegistration: BlockEditorRegistration,
100104
hostElement: HTMLElement,
@@ -124,6 +128,7 @@ export class GlobalState {
124128
this.engine = engine;
125129
this.onNewEngine = onNewEngine;
126130
this.onSmartFilterLoadedObservable = onSmartFilterLoadedObservable;
131+
this.optimizerEnabled = optimizerEnabled;
127132
this.smartFilter = smartFilter ?? new SmartFilter("New Filter");
128133
this.blockEditorRegistration = blockEditorRegistration;
129134
this.hostElement = hostElement;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Observable } from "@babylonjs/core/Misc/observable.js";
2+
3+
/**
4+
* An Observable that doesn't allow you to notify observers.
5+
*/
6+
export type ReadOnlyObservable<T> = Omit<Observable<T>, "notifyObserver" | "notifyObservers">;
7+
8+
/**
9+
* Represents a property that can be observed for changes. The setter of the value property
10+
* will notify observers of the onChangedObservable about the change.
11+
*/
12+
export class ObservableProperty<T> {
13+
private _value: T;
14+
private _onChangedObservable: Observable<T> = new Observable<T>();
15+
16+
public get value(): T {
17+
return this._value;
18+
}
19+
20+
public set value(newValue: T) {
21+
if (this._value !== newValue) {
22+
this._value = newValue;
23+
this._onChangedObservable.notifyObservers(this._value);
24+
}
25+
}
26+
public readonly onChangedObservable: ReadOnlyObservable<T>;
27+
28+
public constructor(value: T) {
29+
this._value = value;
30+
this.onChangedObservable = this._onChangedObservable;
31+
}
32+
}

packages/editor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export * from "./configuration/editorBlocks/inputBlockDeserializer.js";
1717
export * from "./configuration/editorBlocks/webCamInputBlock/webCamInputBlock.js";
1818
export * from "./helpers/registerAnimations.js";
1919
export * from "./configuration/constants.js";
20+
export * from "./helpers/observableProperty.js";

packages/editor/src/initializePreview.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ export function initializePreview(canvas: HTMLCanvasElement): ThinEngine {
1818
},
1919
false
2020
);
21+
engine.getCaps().parallelShaderCompile = undefined;
2122
return engine;
2223
}

packages/editor/src/smartFilterEditorControl.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CreatePopup } from "@babylonjs/shared-ui-components/popupHelper.js";
1313
import type { IBlockRegistration } from "@babylonjs/smart-filters-blocks";
1414
import type { LogEntry } from "./components/log/logComponent.js";
1515
import type { BlockEditorRegistration } from "./configuration/blockEditorRegistration.js";
16+
import type { ObservableProperty } from "./helpers/observableProperty.js";
1617

1718
/**
1819
* Options to configure the Smart Filter Editor
@@ -37,6 +38,11 @@ export type SmartFilterEditorOptions = {
3738
*/
3839
onSmartFilterLoadedObservable?: Observable<SmartFilter>;
3940

41+
/**
42+
* If supplied, the editor will display a toggle to enable or disable the optimizer, and update this property when it changes.
43+
*/
44+
optimizerEnabled?: ObservableProperty<boolean>;
45+
4046
/**
4147
* A BlockEditorRegistration object which is responsible for providing the information
4248
* required for the Editor to be able to display and work with the Smart Filter
@@ -151,6 +157,7 @@ export class SmartFilterEditorControl {
151157
options.engine ?? null,
152158
options.onNewEngine ?? null,
153159
options.onSmartFilterLoadedObservable ?? null,
160+
options.optimizerEnabled ?? null,
154161
options.filter ?? null,
155162
options.blockEditorRegistration,
156163
hostElement,

packages/sfe/src/app.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import "@babylonjs/core/Engines/Extensions/engine.rawTexture.js";
2-
32
import type { ThinEngine } from "@babylonjs/core/Engines/thinEngine";
43
import { Observable, type Observer } from "@babylonjs/core/Misc/observable.js";
54
import type { Nullable } from "@babylonjs/core/types";
@@ -10,6 +9,7 @@ import {
109
getBlockEditorRegistration,
1110
inputBlockDeserializer,
1211
LogEntry,
12+
ObservableProperty,
1313
SmartFilterEditorControl,
1414
type SmartFilterEditorOptions,
1515
} from "@babylonjs/smart-filters-editor-control";
@@ -29,6 +29,8 @@ import { loadSmartFilterFromFile } from "./smartFilterLoadSave/loadSmartFilterFr
2929
import { texturePresets } from "./texturePresets.js";
3030
import { serializeSmartFilter } from "./smartFilterLoadSave/serializeSmartFilter.js";
3131

32+
const LocalStorageOptimizeName = "OptimizeSmartFilter";
33+
3234
/**
3335
* The main entry point for the Smart Filter editor.
3436
*/
@@ -39,15 +41,26 @@ async function main(): Promise<void> {
3941
}
4042

4143
// Services and options to keep around for the lifetime of the page
42-
const optimize = false;
4344
let currentSmartFilter: Nullable<SmartFilter> = null;
4445
let renderer: Nullable<SmartFilterRenderer> = null;
4546
const onSmartFilterLoadedObservable = new Observable<SmartFilter>();
47+
const optimizerEnabled = new ObservableProperty<boolean>(
48+
localStorage.getItem(LocalStorageOptimizeName) === "true" || false
49+
);
4650
const onSaveEditorDataRequiredObservable = new Observable<void>();
4751
let afterEngineResizerObserver: Nullable<Observer<ThinEngine>> = null;
4852
const onLogRequiredObservable = new Observable<LogEntry>();
4953
let engine: Nullable<ThinEngine> = null;
5054

55+
// Set up optimize property change behavior
56+
optimizerEnabled.onChangedObservable.add(async (value: boolean) => {
57+
localStorage.setItem(LocalStorageOptimizeName, value ? "true" : "false");
58+
if (renderer && renderer.optimize !== value) {
59+
renderer.optimize = value;
60+
await startRendering();
61+
}
62+
});
63+
5164
// Create the Smart Filter deserializer
5265
const smartFilterDeserializer = new SmartFilterDeserializer(
5366
(
@@ -125,7 +138,7 @@ async function main(): Promise<void> {
125138
}
126139
}
127140

128-
renderer = new SmartFilterRenderer(newEngine, optimize);
141+
renderer = new SmartFilterRenderer(newEngine, optimizerEnabled.value);
129142
await startRendering();
130143

131144
if (justLoadedSmartFilter) {
@@ -135,8 +148,24 @@ async function main(): Promise<void> {
135148

136149
const startRendering = async () => {
137150
if (renderer && currentSmartFilter) {
138-
if (await renderer.startRendering(currentSmartFilter, onLogRequiredObservable)) {
139-
onLogRequiredObservable.notifyObservers(new LogEntry("Smart Filter built successfully", false));
151+
const renderResult = await renderer.startRendering(currentSmartFilter, onLogRequiredObservable);
152+
if (renderResult.succeeded) {
153+
let statsString = "";
154+
const stats: string[] = [];
155+
if (renderResult.optimizationTimeMs !== null) {
156+
stats.push(`Optimizer: ${Math.floor(renderResult.optimizationTimeMs).toLocaleString()}ms`);
157+
}
158+
if (renderResult.runtimeCreationTimeMs !== null) {
159+
stats.push(
160+
`Runtime Creation: ${Math.floor(renderResult.runtimeCreationTimeMs).toLocaleString()}ms`
161+
);
162+
}
163+
if (stats.length > 0) {
164+
statsString = ` [${stats.join(", ")}]`;
165+
}
166+
onLogRequiredObservable.notifyObservers(
167+
new LogEntry("Smart Filter built successfully" + statsString, false)
168+
);
140169
}
141170
}
142171
};
@@ -158,6 +187,7 @@ async function main(): Promise<void> {
158187
const options: SmartFilterEditorOptions = {
159188
onNewEngine,
160189
onSmartFilterLoadedObservable,
190+
optimizerEnabled,
161191
blockEditorRegistration: blockEditorRegistration,
162192
hostElement,
163193
downloadSmartFilter: () => {
@@ -170,9 +200,8 @@ async function main(): Promise<void> {
170200
try {
171201
if (renderer) {
172202
currentSmartFilter = await loadSmartFilterFromFile(smartFilterDeserializer, engine, file);
173-
if (await renderer.startRendering(currentSmartFilter, onLogRequiredObservable)) {
174-
onLogRequiredObservable.notifyObservers(new LogEntry("Loaded Smart Filter from JSON", false));
175-
}
203+
onLogRequiredObservable.notifyObservers(new LogEntry("Loaded Smart Filter from JSON", false));
204+
startRendering();
176205
return currentSmartFilter;
177206
}
178207
} catch (err: unknown) {

packages/sfe/src/smartFilterRenderer.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ import {
1212
import { RenderTargetGenerator } from "@babylonjs/smart-filters";
1313
import { LogEntry, registerAnimations, TextureAssetCache } from "@babylonjs/smart-filters-editor-control";
1414

15+
/**
16+
* Describes the result of rendering a Smart Filter
17+
*/
18+
export type RenderResult = {
19+
/**
20+
* Indicates if the Smart Filter was rendered successfully
21+
*/
22+
succeeded: boolean;
23+
24+
/**
25+
* The time it took to optimize the Smart Filter (null if not applicable)
26+
*/
27+
optimizationTimeMs: Nullable<number>;
28+
29+
/**
30+
* The time it took to compile the Smart Filter (null if not applicable)
31+
*/
32+
runtimeCreationTimeMs: Nullable<number>;
33+
};
34+
1535
/**
1636
* Simple example of rendering a Smart Filter
1737
*/
@@ -70,13 +90,27 @@ export class SmartFilterRenderer {
7090
* @param onLogRequiredObservable - The observable to use to notify when a log entry is required
7191
* @returns A promise that resolves as true if the rendering started successfully, false otherwise
7292
*/
73-
public async startRendering(filter: SmartFilter, onLogRequiredObservable: Observable<LogEntry>): Promise<boolean> {
93+
public async startRendering(
94+
filter: SmartFilter,
95+
onLogRequiredObservable: Observable<LogEntry>
96+
): Promise<RenderResult> {
97+
let optimizationTimeMs: Nullable<number> = null;
98+
let runtimeCreationTimeMs: Nullable<number> = null;
99+
74100
try {
75101
this._lastRenderedSmartFilter = filter;
76-
const filterToRender = this.optimize ? this._optimize(filter) : filter;
102+
const filterToRender = filter;
103+
if (this.optimize) {
104+
const optimizeStartTime = performance.now();
105+
this._optimize(filter);
106+
optimizationTimeMs = performance.now() - optimizeStartTime;
107+
}
77108

78109
const rtg = new RenderTargetGenerator(this.optimize);
110+
111+
const createRuntimeStartTime = performance.now();
79112
const runtime = await filterToRender.createRuntimeAsync(this.engine, rtg);
113+
runtimeCreationTimeMs = performance.now() - createRuntimeStartTime;
80114

81115
// NOTE: Always load assets and animations from the unoptimized filter because it has all the metadata needed to load assets and
82116
// shares runtime data with the optimized filter so loading assets for it will work for the optimized filter as well
@@ -87,11 +121,19 @@ export class SmartFilterRenderer {
87121

88122
this._setRuntime(runtime);
89123

90-
return true;
124+
return {
125+
succeeded: true,
126+
optimizationTimeMs,
127+
runtimeCreationTimeMs,
128+
};
91129
} catch (err: any) {
92130
const message = err["message"] || err["_compilationError"] || err;
93131
onLogRequiredObservable.notifyObservers(new LogEntry(`Could not render Smart Filter:\n${message}`, true));
94-
return false;
132+
return {
133+
succeeded: false,
134+
optimizationTimeMs: null,
135+
runtimeCreationTimeMs: null,
136+
};
95137
}
96138
}
97139

0 commit comments

Comments
 (0)