Stop resolving every object in every frame of a run (#287)
World.syncToModel runs after every frame of a run and resolved each object of the scene anew to compare it with its entity. With 222 entities that was most of the Editor line in the performance panel, also for entities that are never drawn. - The world keeps per entity what the model said when it last read the object (syncFromModel, spawn) or wrote it. A frame compares the entity with that and resolves nothing while both agree. - A place that differs is written as three numbers. - A value that differs is written as an override, and that one object is resolved again. An override can bring rows of its own, such as a sprite's shader. - The objects of entities that left the run are dropped only in a frame in which one left. World.remove notes that. - Editing during a run works as before: the editor has the model read again on every project event, which renews what the world keeps. One difference: a write to the model that goes past the editor and does not have it read again now stays. The next frame used to overwrite it with the entity's state. No code does that, during a run only the world writes. Measured on stein2 with a copy of the reference project of #284, ms per frame: syncToModel alone 0.92 to 0.12 (Node, 600 frames), the Editor line in the browser 1.14 / 1.20 / 1.29 to 0.25 / 0.25 / 0.27. Physics and Drawing are unchanged. Tests: - Gate: 198 files, 2891 tests (2887 + 4), exit 0 - e2e, full run: 1613 checks passed, exit 0. That measures the total that was only computed since #273. - Smokes: 24 / 19 / 21, exit 0 - Four new unit tests, no new e2e check: 60 unchanged frames resolve nothing, an edit during the run arrives and stays, an override with rows of its own is seen with them, container content in an editor run is written into the instance edits once - Seven counter-probes fail, 1 to 4 tests each Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cfd288c4b7
commit
26cc6324af
3 files changed
+212
-37
No files matched your search
@@ -220,6 +220,31 @@ describe('placing a container', () => {
|
||||
expect(f.sceneOf().objects).toHaveLength(1);
|
||||
expect(f.sceneOf().objects[0]!.instanceEdits[f.tA]).toBeDefined();
|
||||
});
|
||||
|
||||
it('writes what a run changes of unbaked content into the instance edits, once (#287)', () => {
|
||||
const f = templateSetup();
|
||||
const world = World.instantiate(f.project, f.sceneId, { bakeContainers: false });
|
||||
const instance = () => f.sceneOf().objects[0]!;
|
||||
const content = world.findByObjectId(f.child(f.tA))!;
|
||||
const home = { x: content.x, y: content.y };
|
||||
|
||||
content.x = home.x + 7;
|
||||
content.props.width = 12;
|
||||
expect(world.syncToModel()).toEqual({ structure: false, values: true });
|
||||
expect(instance().instanceEdits[f.tA]).toMatchObject({
|
||||
position: { x: home.x + 7 },
|
||||
overrides: { width: 12 },
|
||||
});
|
||||
expect(world.syncToModel()).toEqual({ structure: false, values: false });
|
||||
|
||||
// Back at the template's place the edit of the place is gone, and the next frame has nothing to say.
|
||||
content.x = home.x;
|
||||
expect(world.syncToModel().values).toBe(true);
|
||||
expect(instance().instanceEdits[f.tA]).toEqual({ position: {}, overrides: { width: 12 } });
|
||||
expect(world.syncToModel().values).toBe(false);
|
||||
world.syncFromModel();
|
||||
expect({ x: content.x, y: content.y, width: content.props.width }).toEqual({ ...home, width: 12 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('editing a container template', () => {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||||
import { AddNodeCommand } from '../../src/core/commands/graphCommands';
|
||||
import {
|
||||
ClearOverrideCommand,
|
||||
MoveSceneObjectCommand,
|
||||
RemoveSceneObjectsCommand,
|
||||
SetOverrideCommand,
|
||||
SetParentCommand,
|
||||
} from '../../src/core/commands/sceneObjectCommands';
|
||||
import { worldMatrixOf } from '../../src/core/graph';
|
||||
import { findScene } from '../../src/core/model/Project';
|
||||
import { findAsset, findScene } from '../../src/core/model/Project';
|
||||
import { SHADER_KEY, SHADER_PROPERTY } from '../../src/core/properties/shaders';
|
||||
import { World } from '../../src/runtime/World';
|
||||
import { makeSpriteScene, place, roundTrip, startWorld } from '../helpers';
|
||||
|
||||
@@ -81,6 +84,102 @@ describe('world ↔ model synchronisation (editing while playing)', () => {
|
||||
expect(sceneOf().objects.map((o) => o.id)).toEqual([a]);
|
||||
});
|
||||
|
||||
it('syncToModel asks the model again only for an object whose values it wrote (#287)', () => {
|
||||
const f = setup();
|
||||
const { project, spriteId, a, b, object } = f;
|
||||
const { world } = startWorld(f);
|
||||
// Every resolve of a placed box reads its asset's values, so the reads count the resolves.
|
||||
const sprite = findAsset(project, spriteId)!;
|
||||
let reads = 0;
|
||||
sprite.props = new Proxy(sprite.props, {
|
||||
get(bag, key, receiver) {
|
||||
reads++;
|
||||
return Reflect.get(bag, key, receiver) as unknown;
|
||||
},
|
||||
});
|
||||
for (let frame = 0; frame < 60; frame++)
|
||||
expect(world.syncToModel()).toEqual({ structure: false, values: false });
|
||||
expect(reads).toBe(0);
|
||||
|
||||
// A place is three numbers: written, and nothing to resolve.
|
||||
world.findByObjectId(b)!.x = 310;
|
||||
expect(world.syncToModel().values).toBe(true);
|
||||
expect(object(b).position.x).toBe(310);
|
||||
expect(reads).toBe(0);
|
||||
expect(world.syncToModel().values).toBe(false);
|
||||
|
||||
// A value is written as an override, and that one object is read again – once.
|
||||
world.findByObjectId(a)!.props.width = 999;
|
||||
expect(world.syncToModel().values).toBe(true);
|
||||
expect(object(a).overrides).toEqual({ width: 999 });
|
||||
const afterWrite = reads;
|
||||
expect(afterWrite).toBeGreaterThan(0);
|
||||
expect(world.syncToModel()).toEqual({ structure: false, values: false });
|
||||
expect(reads).toBe(afterWrite);
|
||||
});
|
||||
|
||||
it('an edit made during the run arrives and is not overwritten by an older picture (#287)', () => {
|
||||
const f = setup();
|
||||
const { stack, sceneId, spriteId, a, b, object } = f;
|
||||
const { world } = startWorld(f);
|
||||
const entityA = world.findByObjectId(a)!;
|
||||
const entityB = world.findByObjectId(b)!;
|
||||
entityA.props.width = 999;
|
||||
world.syncToModel();
|
||||
|
||||
// The editor has the model read again after every command, as `Editor` does on a project event.
|
||||
const edit = (command: Parameters<typeof stack.execute>[0]) => {
|
||||
stack.execute(command);
|
||||
world.syncFromModel();
|
||||
};
|
||||
edit(new SetOverrideCommand(sceneId, a, 'width', 40));
|
||||
expect(entityA.props.width).toBe(40);
|
||||
expect(world.syncToModel()).toEqual({ structure: false, values: false });
|
||||
expect(object(a).overrides).toEqual({ width: 40 });
|
||||
|
||||
edit(new SetAssetPropertyCommand(spriteId, 'height', 70));
|
||||
expect(entityB.props.height).toBe(70);
|
||||
expect(world.syncToModel()).toEqual({ structure: false, values: false });
|
||||
expect(object(b).overrides).toEqual({});
|
||||
|
||||
edit(new MoveSceneObjectCommand(sceneId, b, { x: 5, y: 6, z: 0 }));
|
||||
expect(world.syncToModel().values).toBe(false);
|
||||
expect(object(b).position).toEqual({ x: 5, y: 6, z: 0 });
|
||||
|
||||
// What the run changes afterwards goes on top of the edit, key by key.
|
||||
entityA.props.height = 11;
|
||||
expect(world.syncToModel().values).toBe(true);
|
||||
expect(object(a).overrides).toEqual({ width: 40, height: 11 });
|
||||
|
||||
// An object placed during the run is mirrored like the others.
|
||||
const c = place(f, spriteId, { x: 50, y: 50 });
|
||||
world.syncFromModel();
|
||||
world.findByObjectId(c)!.y = 77;
|
||||
expect(world.syncToModel().values).toBe(true);
|
||||
expect(object(c).position.y).toBe(77);
|
||||
});
|
||||
|
||||
it('an override that brings rows of its own is seen with them from the next frame on (#287)', () => {
|
||||
const f = makeSpriteScene();
|
||||
const text = new CreateAssetCommand('text', 'Title');
|
||||
const shader = new CreateAssetCommand('shader', 'Glow');
|
||||
f.stack.execute(text);
|
||||
f.stack.execute(shader);
|
||||
const declared = { name: 'glow', default: 3, min: 0, max: 10 };
|
||||
f.stack.execute(new AddNodeCommand(shader.asset.id, SHADER_PROPERTY, 0, 0, declared));
|
||||
const id = place(f, text.asset.id, { x: 0, y: 0 });
|
||||
const { world } = startWorld(f);
|
||||
const entity = world.findByObjectId(id)!;
|
||||
|
||||
entity.props[SHADER_KEY] = shader.asset.id;
|
||||
world.syncToModel();
|
||||
expect(f.object(id).overrides).toEqual({ [SHADER_KEY]: shader.asset.id });
|
||||
// The shader's row is the object's now, so a value the run sets for it is one to write.
|
||||
entity.props.glow = 8;
|
||||
expect(world.syncToModel().values).toBe(true);
|
||||
expect(f.object(id).overrides.glow).toBe(8);
|
||||
});
|
||||
|
||||
it('spawn adds an object to the runtime scene and an entity at once, without resetting others', () => {
|
||||
const f = setup();
|
||||
const { sceneId, spriteId, a, sceneOf } = f;
|
||||
|
||||
Reference in new issue
Block a user