A still editor has nothing to draw, so the frame loop wakes on change (#44)
game-maker / suite (push) Successful in 10m46s
game-maker / suite (push) Successful in 10m46s
The renderer is taken off the ticker; the ticker itself keeps running, so the clock, a live world and the panels still get their step. Drawing now happens inside a 200 ms trailing window that every pointer, wheel and key event, every Widget.invalidate/resize, every Stack.relayout, every EditorState.emit and both library onChange hooks reopen. The window rather than a single frame is what makes the ~70 direct redraw() calls that never pass through invalidate() safe, along with repaints an event only starts: a microtask, a texture upload, a resolved font. Editor.movesByItself() keeps asking for every frame while Play or an editor run is on, a time shader is on screen, or the particle or animation preview is showing. The FPS counter now counts drawn frames over the time the renderer was awake. Idle seconds are not slow seconds, so it answers "how smooth while it moves" and holds its last answer instead of falling to 0 at rest. Measured on the built stand, 20 s per variant, CPU of all chromium processes from /proc: kept awake 75.5 % of a core at 60 fps, render on change 8.8 % with 0 frames drawn, awake again 83.9 % as an order probe. No colour check can see a missing repaint: pixelAt extracts from the stage on demand and is just as green on a screen frozen a minute ago. Step 33 watches the new __gm.renderStats() counter instead.
This commit is contained in:
1 parent
2637feb715
commit
bc57f61c91
16 files changed
+355
-7
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import type { Application } from 'pixi.js';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
initRenderLoop,
|
||||
isRendering,
|
||||
renderCount,
|
||||
requestRender,
|
||||
resetRenderLoop,
|
||||
} from '../../src/ui/render';
|
||||
|
||||
/** A stand-in for the Pixi application: counts draws and remembers what was put on the ticker. */
|
||||
function fakeApp() {
|
||||
const added: { fn: () => void; priority?: number }[] = [];
|
||||
const removed: { fn: unknown; context: unknown }[] = [];
|
||||
const app = {
|
||||
drawn: 0,
|
||||
render(): void {
|
||||
app.drawn += 1;
|
||||
},
|
||||
ticker: {
|
||||
add(fn: () => void, _context?: unknown, priority?: number) {
|
||||
added.push({ fn, priority });
|
||||
return this;
|
||||
},
|
||||
remove(fn: unknown, context?: unknown) {
|
||||
removed.push({ fn, context });
|
||||
return this;
|
||||
},
|
||||
},
|
||||
};
|
||||
return { app, added, removed, tick: () => added.forEach((entry) => entry.fn()) };
|
||||
}
|
||||
|
||||
describe('render loop', () => {
|
||||
let now = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
now = 1000;
|
||||
vi.spyOn(performance, 'now').mockImplementation(() => now);
|
||||
resetRenderLoop();
|
||||
});
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it('is asleep until something asks for a frame', () => {
|
||||
expect(isRendering()).toBe(false);
|
||||
});
|
||||
|
||||
it('stays awake for a window after the request, not for a single frame', () => {
|
||||
requestRender();
|
||||
expect(isRendering()).toBe(true);
|
||||
now += 199;
|
||||
expect(isRendering()).toBe(true);
|
||||
now += 2;
|
||||
expect(isRendering()).toBe(false);
|
||||
});
|
||||
|
||||
it('a later request extends the window rather than queueing a second one', () => {
|
||||
requestRender();
|
||||
now += 150;
|
||||
requestRender();
|
||||
now += 150;
|
||||
expect(isRendering()).toBe(true);
|
||||
});
|
||||
|
||||
it('takes the renderer off the ticker and drives it itself', () => {
|
||||
const { app, removed, tick } = fakeApp();
|
||||
initRenderLoop(app as unknown as Application);
|
||||
expect(removed).toEqual([{ fn: app.render, context: app }]);
|
||||
|
||||
// `initRenderLoop` opens the first window, so the first frames are drawn.
|
||||
tick();
|
||||
tick();
|
||||
expect(app.drawn).toBe(2);
|
||||
expect(renderCount()).toBe(2);
|
||||
});
|
||||
|
||||
it('draws nothing while nothing changed, and again after a request', () => {
|
||||
const { app, tick } = fakeApp();
|
||||
initRenderLoop(app as unknown as Application);
|
||||
now += 500;
|
||||
|
||||
for (let i = 0; i < 30; i++) tick();
|
||||
expect(app.drawn).toBe(0);
|
||||
expect(renderCount()).toBe(0);
|
||||
|
||||
requestRender();
|
||||
tick();
|
||||
expect(app.drawn).toBe(1);
|
||||
|
||||
// And it falls asleep again by itself.
|
||||
now += 500;
|
||||
tick();
|
||||
expect(app.drawn).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user