Skip to content

Commit 7f5913c

Browse files
Perf: Pause RAF loop when there are no updates (#99)
- [x] Implement RAF Loop Pause optimization - [x] Fix SDF text font loading race condition - This results in text that is not rendered if an SDF font atlas texture is not uploaded in time to the GPU before the RAF loop is paused. - [x] Fix text doubling - Some text appears to be double rendered which is causing text to appear thicker and tests to fail. - Turns out the `gl.clear()` command was still be affected by the last scissor operation causing the entire canvas to not get cleared each time.
2 parents 3a2342e + a07aa68 commit 7f5913c

File tree

9 files changed

+42
-153
lines changed

9 files changed

+42
-153
lines changed

examples/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,11 @@ async function runAutomation(driverName: string, logFps: boolean) {
262262
const snapshot = (window as any).snapshot as
263263
| ((testName: string) => Promise<void>)
264264
| undefined;
265+
// Allow some time for all images to load and the RaF to unpause
266+
// and render if needed.
267+
await delay(200);
265268
if (snapshot) {
266-
console.error(`Calling snapshot(${testName})`);
269+
console.log(`Calling snapshot(${testName})`);
267270
await snapshot(testName);
268271
} else {
269272
console.error(

src/core/CoreNode.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ export class CoreNode extends EventEmitter implements ICoreNode {
217217
}
218218

219219
private onTextureLoaded: TextureLoadedEventHandler = (target, dimensions) => {
220+
// Texture was loaded. In case the RAF loop has already stopped, we request
221+
// a render to ensure the texture is rendered.
222+
this.stage.requestRender();
220223
this.emit('loaded', {
221224
type: 'texture',
222225
dimensions,

src/core/CoreTextNode.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export class CoreTextNode extends CoreNode implements ICoreTextNode {
102102
}
103103
this.updateLocalTransform();
104104

105+
// Incase the RAF loop has been stopped already before text was loaded,
106+
// we request a render so it can be drawn.
107+
this.stage.requestRender();
105108
this.emit('loaded', {
106109
type: 'text',
107110
dimensions: {

src/core/Stage.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19-
20-
import { Scene } from './scene/Scene.js';
21-
2219
import { startLoop, getTimeStamp } from './platform.js';
23-
2420
import { WebGlCoreRenderer } from './renderers/webgl/WebGlCoreRenderer.js';
2521
import { assertTruthy } from '../utils.js';
2622
import { AnimationManager } from './animations/AnimationManager.js';
@@ -62,14 +58,15 @@ export class Stage extends EventEmitter {
6258
public readonly textRenderers: Partial<TextRendererMap>;
6359
public readonly shManager: CoreShaderManager;
6460
public readonly renderer: WebGlCoreRenderer;
65-
private scene: Scene;
61+
public readonly root: CoreNode;
6662

6763
/// State
6864
deltaTime = 0;
6965
lastFrameTime = 0;
7066
currentFrameTime = 0;
7167
private fpsNumFrames = 0;
7268
private fpsElapsedTime = 0;
69+
private renderRequested = false;
7370

7471
/**
7572
* Stage constructor
@@ -146,7 +143,7 @@ export class Stage extends EventEmitter {
146143
shaderProps: null,
147144
});
148145

149-
this.scene = new Scene(rootNode);
146+
this.root = rootNode;
150147

151148
// execute platform start loop
152149
if (autoStart) {
@@ -158,8 +155,8 @@ export class Stage extends EventEmitter {
158155
* Update animations
159156
*/
160157
updateAnimations() {
161-
const { scene, animationManager } = this;
162-
if (!scene?.root) {
158+
const { animationManager } = this;
159+
if (!this.root) {
163160
return;
164161
}
165162
this.lastFrameTime = this.currentFrameTime;
@@ -177,33 +174,32 @@ export class Stage extends EventEmitter {
177174
* Check if the scene has updates
178175
*/
179176
hasSceneUpdates() {
180-
const { scene } = this;
181-
182-
if (!scene?.root) {
183-
return false;
184-
}
185-
186-
return !!scene?.root?.updateType;
177+
return !!this.root.updateType || this.renderRequested;
187178
}
188179

189180
/**
190181
* Start a new frame draw
191182
*/
192183
drawFrame() {
193-
const { renderer, scene } = this;
184+
const { renderer, renderRequested } = this;
194185

195186
// Update tree if needed
196-
if (scene.root.updateType !== 0) {
197-
scene.root.update(this.deltaTime);
187+
if (this.root.updateType !== 0) {
188+
this.root.update(this.deltaTime);
198189
}
199190

200191
// test if we need to update the scene
201192
renderer?.reset();
202193

203-
this.addQuads(scene.root);
194+
this.addQuads(this.root);
204195

205196
renderer?.render();
206197

198+
// Reset renderRequested flag if it was set
199+
if (renderRequested) {
200+
this.renderRequested = false;
201+
}
202+
207203
// If there's an FPS update interval, emit the FPS update event
208204
// when the specified interval has elapsed.
209205
const { fpsUpdateInterval } = this.options;
@@ -240,6 +236,13 @@ export class Stage extends EventEmitter {
240236
}
241237
}
242238

239+
/**
240+
* Request a render pass without forcing an update
241+
*/
242+
requestRender() {
243+
this.renderRequested = true;
244+
}
245+
243246
/**
244247
* Given a font name, and possible renderer override, return the best compatible text renderer.
245248
*
@@ -304,12 +307,4 @@ export class Stage extends EventEmitter {
304307
// the covariant state argument in the setter method map
305308
return resolvedTextRenderer as unknown as TextRenderer;
306309
}
307-
308-
//#region Properties
309-
310-
get root() {
311-
return this.scene?.root || null;
312-
}
313-
314-
//#endregion Properties
315310
}

src/core/platform.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export const startLoop = (stage: Stage) => {
2626
const runLoop = () => {
2727
stage.updateAnimations();
2828

29+
if (!stage.hasSceneUpdates()) {
30+
setTimeout(runLoop, 16.666666666666668);
31+
return;
32+
}
33+
2934
stage.drawFrame();
3035
requestAnimationFrame(runLoop);
3136
};

src/core/renderers/webgl/WebGlCoreRenderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export class WebGlCoreRenderer extends CoreRenderer {
179179
this.curBufferIdx = 0;
180180
this.curRenderOp = null;
181181
this.renderOps.length = 0;
182+
this.gl.disable(this.gl.SCISSOR_TEST);
182183
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
183184
}
184185

src/core/scene/Scene.ts

Lines changed: 0 additions & 120 deletions
This file was deleted.

src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,9 @@ export class SdfTrFontFace<
8282
},
8383
);
8484

85-
// TODO: Add texture loaded support
86-
// this.texture.on('loaded', () => {
87-
// this.checkLoaded();
88-
// });
85+
this.texture.on('loaded', () => {
86+
this.checkLoaded();
87+
});
8988

9089
// Set this.data to the fetched data from dataUrl
9190
fetch(atlasDataUrl)
@@ -120,7 +119,7 @@ export class SdfTrFontFace<
120119

121120
private checkLoaded(): void {
122121
if (this.loaded) return;
123-
if (/*this.texture.loaded && */ this.data) {
122+
if (this.texture.state === 'loaded' && this.data) {
124123
(this.loaded as boolean) = true;
125124
this.emit('loaded');
126125
}
36.6 KB
Loading

0 commit comments

Comments
 (0)