Keep drags at full speed in large projects (#394)
Dragging a band of a Filter or a Parametric EQ ran at about 30 fps in a large project. Each frame of a drag announces the changed project, and every listener of that event ran, also those whose work nobody sees while the drag goes on. - FramePreview marks its own frame, read through isDragFrame(). - The Scene view behind another tab skips such a frame and draws on coming back, as it already did while a game runs (#265). - preloadProjectFiles skips it as well. It walks every asset and checks every bake, which costs milliseconds in a large project. - The command that ends a drag emits for itself, and a drag that changes nothing ends with a plain preview, so both catch up then. - This holds for every drag through FramePreview, the knobs of the devices in the graph included. Measured on stein2 with a copy of a project of 162 assets, a band of a Parametric EQ dragged at 250 pointer moves a second for 3 s: - Listeners per frame: 17 ms before, 7.4 ms after. - Hidden Scene view: 814 ms before, 53 ms after. - preloadProjectFiles: 514 ms before, 0.4 ms after. - What is left is mostly the Hierarchy and Assets panels on screen (about 3 and 1.3 ms a frame). They stay as they are. Tests: - Unit frame-preview, play-throttle: 29 passed (1 new) - Counter-check: without the mark the new test fails - e2e 34 (melektaus-atx): ALL PASSED, 136 checks - Gate (melektaus-atx): CHECK EXIT 0, 288 files, 4417 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f7eb03143
commit
cb3183227b
4 files changed
+44
-11
No files matched your search
+6
-2
@@ -18,6 +18,7 @@ import { installLoginHook } from './auth/loginHook';
|
|||||||
import { consoleLog } from './console/log';
|
import { consoleLog } from './console/log';
|
||||||
import { installDebugHook } from './debugHook';
|
import { installDebugHook } from './debugHook';
|
||||||
import { Editor } from './Editor';
|
import { Editor } from './Editor';
|
||||||
|
import { isDragFrame } from './framePreview';
|
||||||
import { startPlugins } from './plugins/startPlugins';
|
import { startPlugins } from './plugins/startPlugins';
|
||||||
import { CelUploadStore } from './pixelart/celUploadStore';
|
import { CelUploadStore } from './pixelart/celUploadStore';
|
||||||
import { EditorState } from './state';
|
import { EditorState } from './state';
|
||||||
@@ -74,8 +75,11 @@ export async function startEditor(app: Application, root: VStack): Promise<void>
|
|||||||
// Drawn pictures (#185) are looked up in the project this session holds; the next session
|
// Drawn pictures (#185) are looked up in the project this session holds; the next session
|
||||||
// installs its own, which is also what lets go of everything the old one had drawn.
|
// installs its own, which is also what lets go of everything the old one had drawn.
|
||||||
textureLibrary.setPictureFactory(projectPictureFactory(() => state.project));
|
textureLibrary.setPictureFactory(projectPictureFactory(() => state.project));
|
||||||
// Asked on every change, which is cheap – a file that is held or on its way is skipped.
|
// Asked on every change but a drag's frame (#394): in a large project the walk over all assets
|
||||||
state.on('project', () => preloadProjectFiles(state.project));
|
// costs milliseconds, and the command that ends the drag asks again.
|
||||||
|
state.on('project', () => {
|
||||||
|
if (!isDragFrame()) preloadProjectFiles(state.project);
|
||||||
|
});
|
||||||
const editor = new Editor(
|
const editor = new Editor(
|
||||||
app,
|
app,
|
||||||
root,
|
root,
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ import { cancelFrameWork, onFrame } from '../ui/render';
|
|||||||
import type { EditorState } from './state';
|
import type { EditorState } from './state';
|
||||||
|
|
||||||
let instances = 0;
|
let instances = 0;
|
||||||
|
let inDragFrame = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the `project` event going on is a drag's frame. What nobody sees during it – a hidden view,
|
||||||
|
* files to load ahead – may wait for the command that ends the drag (#394).
|
||||||
|
*/
|
||||||
|
export function isDragFrame(): boolean {
|
||||||
|
return inDragFrame;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A drag's `state.preview()`, at most once per frame (#213). The gesture still writes the model on
|
* A drag's `state.preview()`, at most once per frame (#213). The gesture still writes the model on
|
||||||
@@ -20,7 +29,14 @@ export class FramePreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
request(): void {
|
request(): void {
|
||||||
onFrame(this.key, () => this.state.preview());
|
onFrame(this.key, () => {
|
||||||
|
inDragFrame = true;
|
||||||
|
try {
|
||||||
|
this.state.preview();
|
||||||
|
} finally {
|
||||||
|
inDragFrame = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(): void {
|
cancel(): void {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { findAsset, type ObjectHolder } from '../../core/model/Project';
|
|||||||
import { cellAtPoint } from '../../core/model/tilemap';
|
import { cellAtPoint } from '../../core/model/tilemap';
|
||||||
import { footprintCorners, iconCorners } from '../../core/properties/bounds';
|
import { footprintCorners, iconCorners } from '../../core/properties/bounds';
|
||||||
import { localMatrixOf, parentTransform, transformOf } from '../../core/properties/transform';
|
import { localMatrixOf, parentTransform, transformOf } from '../../core/properties/transform';
|
||||||
import { FramePreview } from '../framePreview';
|
import { FramePreview, isDragFrame } from '../framePreview';
|
||||||
import { panelPolicy } from '../playThrottle';
|
import { panelPolicy } from '../playThrottle';
|
||||||
import { type EditorState, objectSelection } from '../state';
|
import { type EditorState, objectSelection } from '../state';
|
||||||
import { paintedLayerId, paintedTilemap } from '../panels/TilemapPanel';
|
import { paintedLayerId, paintedTilemap } from '../panels/TilemapPanel';
|
||||||
@@ -204,15 +204,16 @@ export class SceneViewport extends FreeViewport {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set when a redraw was skipped behind the game tab (#265); the next time on screen draws it. */
|
/**
|
||||||
|
* Set when a redraw was skipped behind another tab: while a game runs (#265), or for a drag's frame
|
||||||
|
* (#394). The next time on screen draws it.
|
||||||
|
*/
|
||||||
private missedRedraw = false;
|
private missedRedraw = false;
|
||||||
|
|
||||||
override redraw(): void {
|
override redraw(): void {
|
||||||
if (
|
const playing =
|
||||||
!this.visible &&
|
this.state.mode === 'play' && !panelPolicy(this.state.project.whilePlaying).hiddenSceneView;
|
||||||
this.state.mode === 'play' &&
|
if (!this.visible && (playing || isDragFrame())) {
|
||||||
!panelPolicy(this.state.project.whilePlaying).hiddenSceneView
|
|
||||||
) {
|
|
||||||
this.missedRedraw = true;
|
this.missedRedraw = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type {
|
|||||||
import { PALETTE_KEY } from '../../src/core/model/pixelart';
|
import { PALETTE_KEY } from '../../src/core/model/pixelart';
|
||||||
import { createEmptyProject } from '../../src/core/model/Project';
|
import { createEmptyProject } from '../../src/core/model/Project';
|
||||||
import { CurveCanvas } from '../../src/editor/curve/CurveCanvas';
|
import { CurveCanvas } from '../../src/editor/curve/CurveCanvas';
|
||||||
import { FramePreview } from '../../src/editor/framePreview';
|
import { FramePreview, isDragFrame } from '../../src/editor/framePreview';
|
||||||
import { GraphDrags } from '../../src/editor/graph/graphDrags';
|
import { GraphDrags } from '../../src/editor/graph/graphDrags';
|
||||||
import type { GraphCanvasHost } from '../../src/editor/graph/graphHost';
|
import type { GraphCanvasHost } from '../../src/editor/graph/graphHost';
|
||||||
import type { InspectorHost } from '../../src/editor/inspector/host';
|
import type { InspectorHost } from '../../src/editor/inspector/host';
|
||||||
@@ -89,6 +89,18 @@ describe('FramePreview', () => {
|
|||||||
expect(state.preview).not.toHaveBeenCalled();
|
expect(state.preview).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('says during its own preview that the event is a drag frame, and at no other time (#394)', () => {
|
||||||
|
const tick = frames();
|
||||||
|
const seen: boolean[] = [];
|
||||||
|
const state = { preview: vi.fn(() => seen.push(isDragFrame())) };
|
||||||
|
new FramePreview(state, 'drag').request();
|
||||||
|
expect(isDragFrame()).toBe(false);
|
||||||
|
tick();
|
||||||
|
state.preview();
|
||||||
|
expect(seen).toEqual([true, false]);
|
||||||
|
expect(isDragFrame()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps two gestures of the same name apart', () => {
|
it('keeps two gestures of the same name apart', () => {
|
||||||
const tick = frames();
|
const tick = frames();
|
||||||
const a = fakeState();
|
const a = fakeState();
|
||||||
|
|||||||
Reference in new issue
Block a user