@flighthq/render 0.3.0-next.906.07cea63 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +6 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +6 -1
- package/dist/contract.js.map +1 -1
- package/dist/enableColorAdjustmentGuards.js +6 -6
- package/dist/enableColorAdjustmentGuards.js.map +1 -1
- package/dist/enableColorAdjustments.d.ts +4 -0
- package/dist/enableColorAdjustments.d.ts.map +1 -0
- package/dist/enableColorAdjustments.js +110 -0
- package/dist/enableColorAdjustments.js.map +1 -0
- package/dist/enableSceneRenderGuards.d.ts +22 -0
- package/dist/enableSceneRenderGuards.d.ts.map +1 -0
- package/dist/enableSceneRenderGuards.js +36 -0
- package/dist/enableSceneRenderGuards.js.map +1 -0
- package/dist/explainScene2DCoverage.d.ts +4 -0
- package/dist/explainScene2DCoverage.d.ts.map +1 -0
- package/dist/explainScene2DCoverage.js +57 -0
- package/dist/explainScene2DCoverage.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/renderProxy.d.ts.map +1 -1
- package/dist/renderProxy.js +34 -9
- package/dist/renderProxy.js.map +1 -1
- package/dist/renderRegistryGuards.d.ts +5 -0
- package/dist/renderRegistryGuards.d.ts.map +1 -0
- package/dist/renderRegistryGuards.js +87 -0
- package/dist/renderRegistryGuards.js.map +1 -0
- package/dist/renderRegistrySignals.d.ts +3 -0
- package/dist/renderRegistrySignals.d.ts.map +1 -0
- package/dist/renderRegistrySignals.js +19 -0
- package/dist/renderRegistrySignals.js.map +1 -0
- package/dist/renderState.d.ts +1 -0
- package/dist/renderState.d.ts.map +1 -1
- package/dist/renderState.js +26 -0
- package/dist/renderState.js.map +1 -1
- package/dist/renderTarget.d.ts +4 -2
- package/dist/renderTarget.d.ts.map +1 -1
- package/dist/renderTarget.js +52 -2
- package/dist/renderTarget.js.map +1 -1
- package/dist/renderTransform2d.d.ts.map +1 -1
- package/dist/renderTransform2d.js +4 -1
- package/dist/renderTransform2d.js.map +1 -1
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +16 -0
- package/dist/renderer.js.map +1 -1
- package/dist/scene2dWorkingColorSpace.d.ts +3 -0
- package/dist/scene2dWorkingColorSpace.d.ts.map +1 -0
- package/dist/scene2dWorkingColorSpace.js +19 -0
- package/dist/scene2dWorkingColorSpace.js.map +1 -0
- package/dist/sceneRender.d.ts +6 -2
- package/dist/sceneRender.d.ts.map +1 -1
- package/dist/sceneRender.js +47 -13
- package/dist/sceneRender.js.map +1 -1
- package/package.json +15 -12
- package/src/enableColorAdjustmentGuards.test.ts +6 -4
- package/src/enableColorAdjustments.test.ts +233 -0
- package/src/enableSceneRenderGuards.test.ts +96 -0
- package/src/explainScene2DCoverage.test.ts +113 -0
- package/src/renderProxy.test.ts +142 -11
- package/src/renderQueue.test.ts +1 -1
- package/src/renderRegistryGuards.test.ts +94 -0
- package/src/renderRegistrySignals.test.ts +15 -0
- package/src/renderState.test.ts +16 -1
- package/src/renderTarget.test.ts +113 -5
- package/src/renderTransform2d.test.ts +15 -1
- package/src/renderViewport.test.ts +1 -1
- package/src/renderer.test.ts +44 -1
- package/src/scene2dWorkingColorSpace.test.ts +11 -0
- package/src/sceneRender.test.ts +75 -2
- package/dist/renderColorScaleBias.d.ts +0 -3
- package/dist/renderColorScaleBias.d.ts.map +0 -1
- package/dist/renderColorScaleBias.js +0 -21
- package/dist/renderColorScaleBias.js.map +0 -1
- package/src/renderColorScaleBias.test.ts +0 -53
package/src/renderProxy.test.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { createMatrix, createRectangle } from '@flighthq/geometry/contract';
|
|
2
2
|
import {
|
|
3
3
|
addNodeChild,
|
|
4
|
+
addNodeChildAt,
|
|
4
5
|
getNodeAppearanceRevision,
|
|
6
|
+
getNodeChildrenRevision,
|
|
5
7
|
getNodeLocalContentRevision,
|
|
6
8
|
getNodeLocalTransformRevision,
|
|
9
|
+
getNodeParentReferenceRevision,
|
|
7
10
|
invalidateNodeAppearance,
|
|
8
11
|
invalidateNodeLocalTransform,
|
|
12
|
+
removeNodeChild,
|
|
9
13
|
} from '@flighthq/node/contract';
|
|
10
14
|
import { createDisplayObject, setNode2DClip } from '@flighthq/scene2d/contract';
|
|
11
|
-
import { createSprite } from '@flighthq/
|
|
12
|
-
import type { ClipRegion } from '@flighthq/types/contract';
|
|
15
|
+
import { createSprite } from '@flighthq/scene2d/contract';
|
|
16
|
+
import type { ClipRegion, Node, RenderProxy, RenderProxy2D, RenderState } from '@flighthq/types/contract';
|
|
13
17
|
|
|
14
18
|
import { registerRenderer } from './renderer';
|
|
15
19
|
import {
|
|
@@ -44,6 +48,14 @@ function makeClipRegion(): ClipRegion {
|
|
|
44
48
|
return { contours: null, rect: createRectangle(0, 0, 10, 10), winding: 'nonZero', version: 0 };
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
function markRenderProxyClean<Traits extends object>(data: RenderProxy, source: Node<Traits>): void {
|
|
52
|
+
data.lastAppearanceId = getNodeAppearanceRevision(source);
|
|
53
|
+
data.lastChildrenId = getNodeChildrenRevision(source);
|
|
54
|
+
data.lastLocalContentId = getNodeLocalContentRevision(source);
|
|
55
|
+
data.lastLocalTransformId = getNodeLocalTransformRevision(source);
|
|
56
|
+
data.lastParentReferenceId = getNodeParentReferenceRevision(source);
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
describe('createRenderProxy', () => {
|
|
48
60
|
it('initializes default values', () => {
|
|
49
61
|
const state = createRenderState();
|
|
@@ -55,7 +67,9 @@ describe('createRenderProxy', () => {
|
|
|
55
67
|
expect(node.alpha).toBe(1);
|
|
56
68
|
expect(node.appearanceFrameId).toBe(-1);
|
|
57
69
|
expect(node.lastAppearanceId).toBe(-1);
|
|
70
|
+
expect(node.lastChildrenId).toBe(-1);
|
|
58
71
|
expect(node.lastLocalTransformId).toBe(-1);
|
|
72
|
+
expect(node.lastParentReferenceId).toBe(-1);
|
|
59
73
|
expect(node.transformFrameId).toBe(-1);
|
|
60
74
|
expect(node.renderer).toBeNull();
|
|
61
75
|
expect(node.rendererData).toBeNull();
|
|
@@ -258,9 +272,7 @@ describe('isRenderProxyDirty', () => {
|
|
|
258
272
|
const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
259
273
|
const source = createDisplayObject();
|
|
260
274
|
const data = createRenderProxy2D(state, source);
|
|
261
|
-
data
|
|
262
|
-
data.lastLocalContentId = getNodeLocalContentRevision(source);
|
|
263
|
-
data.lastLocalTransformId = getNodeLocalTransformRevision(source);
|
|
275
|
+
markRenderProxyClean(data, source);
|
|
264
276
|
|
|
265
277
|
expect(isRenderProxyDirty(state, source, data)).toBe(false);
|
|
266
278
|
});
|
|
@@ -269,8 +281,7 @@ describe('isRenderProxyDirty', () => {
|
|
|
269
281
|
const state = createRenderState();
|
|
270
282
|
const source = createDisplayObject();
|
|
271
283
|
const data = createRenderProxy2D(state, source);
|
|
272
|
-
data
|
|
273
|
-
data.lastLocalTransformId = getNodeLocalTransformRevision(source);
|
|
284
|
+
markRenderProxyClean(data, source);
|
|
274
285
|
invalidateNodeAppearance(source);
|
|
275
286
|
|
|
276
287
|
expect(isRenderProxyDirty(state, source, data)).toBe(true);
|
|
@@ -280,8 +291,7 @@ describe('isRenderProxyDirty', () => {
|
|
|
280
291
|
const state = createRenderState();
|
|
281
292
|
const source = createDisplayObject();
|
|
282
293
|
const data = createRenderProxy2D(state, source);
|
|
283
|
-
data
|
|
284
|
-
data.lastLocalTransformId = getNodeLocalTransformRevision(source);
|
|
294
|
+
markRenderProxyClean(data, source);
|
|
285
295
|
const parentData = createRenderProxy2D(state, createDisplayObject());
|
|
286
296
|
parentData.transformFrameId = getRenderStateRuntime(state).currentFrameId;
|
|
287
297
|
|
|
@@ -293,8 +303,7 @@ describe('isRenderProxyDirty', () => {
|
|
|
293
303
|
const source = createDisplayObject();
|
|
294
304
|
const data = createRenderProxy2D(state, source);
|
|
295
305
|
data.transform2D = createMatrix();
|
|
296
|
-
data
|
|
297
|
-
data.lastLocalTransformId = getNodeLocalTransformRevision(source);
|
|
306
|
+
markRenderProxyClean(data, source);
|
|
298
307
|
source.x = 10;
|
|
299
308
|
invalidateNodeLocalTransform(source);
|
|
300
309
|
|
|
@@ -337,6 +346,89 @@ describe('isRenderProxyVisible', () => {
|
|
|
337
346
|
});
|
|
338
347
|
|
|
339
348
|
describe('prepareScene2DRender', () => {
|
|
349
|
+
it('dirties a warm proxy when it is reattached after another prepared child', () => {
|
|
350
|
+
const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
351
|
+
const root = createDisplayObject();
|
|
352
|
+
const childA = createDisplayObject();
|
|
353
|
+
const childB = createDisplayObject();
|
|
354
|
+
addNodeChild(root, childA);
|
|
355
|
+
expect(prepareScene2DRender(state, root)).toBe(true);
|
|
356
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
357
|
+
|
|
358
|
+
removeNodeChild(root, childA);
|
|
359
|
+
addNodeChild(root, childB);
|
|
360
|
+
expect(prepareScene2DRender(state, root)).toBe(true);
|
|
361
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
362
|
+
|
|
363
|
+
removeNodeChild(root, childB);
|
|
364
|
+
addNodeChild(root, childA);
|
|
365
|
+
expect(prepareScene2DRender(state, root)).toBe(true);
|
|
366
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('dirties the tree when a prepared child is removed', () => {
|
|
370
|
+
const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
371
|
+
const root = createDisplayObject();
|
|
372
|
+
const child = createDisplayObject();
|
|
373
|
+
addNodeChild(root, child);
|
|
374
|
+
prepareScene2DRender(state, root);
|
|
375
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
376
|
+
|
|
377
|
+
removeNodeChild(root, child);
|
|
378
|
+
|
|
379
|
+
expect(prepareScene2DRender(state, root)).toBe(true);
|
|
380
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('dirties the tree when prepared siblings are reordered', () => {
|
|
384
|
+
const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
385
|
+
const root = createDisplayObject();
|
|
386
|
+
const childA = createDisplayObject();
|
|
387
|
+
const childB = createDisplayObject();
|
|
388
|
+
addNodeChild(root, childA);
|
|
389
|
+
addNodeChild(root, childB);
|
|
390
|
+
prepareScene2DRender(state, root);
|
|
391
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
392
|
+
|
|
393
|
+
addNodeChildAt(root, childA, 1);
|
|
394
|
+
|
|
395
|
+
expect(prepareScene2DRender(state, root)).toBe(true);
|
|
396
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('recomputes inherited transform and appearance when a warm child changes parent', () => {
|
|
400
|
+
const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
401
|
+
const root = createDisplayObject();
|
|
402
|
+
const parentA = createDisplayObject();
|
|
403
|
+
const parentB = createDisplayObject();
|
|
404
|
+
const child = createDisplayObject();
|
|
405
|
+
parentA.x = 10;
|
|
406
|
+
parentA.alpha = 0.5;
|
|
407
|
+
parentB.x = 100;
|
|
408
|
+
parentB.alpha = 0.25;
|
|
409
|
+
child.x = 3;
|
|
410
|
+
child.alpha = 0.5;
|
|
411
|
+
invalidateNodeLocalTransform(parentA);
|
|
412
|
+
invalidateNodeAppearance(parentA);
|
|
413
|
+
invalidateNodeLocalTransform(parentB);
|
|
414
|
+
invalidateNodeAppearance(parentB);
|
|
415
|
+
invalidateNodeLocalTransform(child);
|
|
416
|
+
invalidateNodeAppearance(child);
|
|
417
|
+
addNodeChild(root, parentA);
|
|
418
|
+
addNodeChild(root, parentB);
|
|
419
|
+
addNodeChild(parentA, child);
|
|
420
|
+
prepareScene2DRender(state, root);
|
|
421
|
+
expect(getOrCreateRenderProxy2D(state, child).transform2D.tx).toBe(13);
|
|
422
|
+
expect(getOrCreateRenderProxy2D(state, child).alpha).toBe(0.25);
|
|
423
|
+
|
|
424
|
+
addNodeChild(parentB, child);
|
|
425
|
+
|
|
426
|
+
expect(prepareScene2DRender(state, root)).toBe(true);
|
|
427
|
+
expect(getOrCreateRenderProxy2D(state, child).transform2D.tx).toBe(103);
|
|
428
|
+
expect(getOrCreateRenderProxy2D(state, child).alpha).toBe(0.125);
|
|
429
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
|
|
340
432
|
it('creates render nodes for all enabled nodes in the tree', () => {
|
|
341
433
|
const state = createRenderState();
|
|
342
434
|
const root = createDisplayObject();
|
|
@@ -389,6 +481,23 @@ describe('prepareScene2DRender', () => {
|
|
|
389
481
|
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
390
482
|
});
|
|
391
483
|
|
|
484
|
+
it('returns true when the registered renderer reports an identity-tier change', () => {
|
|
485
|
+
const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
486
|
+
const root = createDisplayObject();
|
|
487
|
+
let rendererDirty = false;
|
|
488
|
+
registerRenderer(state, root.kind, {
|
|
489
|
+
createData: () => null,
|
|
490
|
+
isDirty: () => rendererDirty,
|
|
491
|
+
submit: vi.fn(),
|
|
492
|
+
});
|
|
493
|
+
prepareScene2DRender(state, root);
|
|
494
|
+
expect(prepareScene2DRender(state, root)).toBe(false);
|
|
495
|
+
|
|
496
|
+
rendererDirty = true;
|
|
497
|
+
|
|
498
|
+
expect(prepareScene2DRender(state, root)).toBe(true);
|
|
499
|
+
});
|
|
500
|
+
|
|
392
501
|
it('accumulates clip depth down a clipped subtree', () => {
|
|
393
502
|
const state = createRenderState();
|
|
394
503
|
const root = createDisplayObject();
|
|
@@ -461,6 +570,28 @@ describe('updateRenderProxy2D', () => {
|
|
|
461
570
|
updateRenderProxy2D(state, root, data, undefined);
|
|
462
571
|
expect(data.clipDepth).toBe(1);
|
|
463
572
|
});
|
|
573
|
+
|
|
574
|
+
it('runs the optional color resolver after material and before clip and revision bookkeeping', () => {
|
|
575
|
+
const state = createRenderState();
|
|
576
|
+
const root = createDisplayObject();
|
|
577
|
+
const material = { kind: 'TestMaterial' } as never;
|
|
578
|
+
root.material = material;
|
|
579
|
+
setNode2DClip(root, makeClipRegion());
|
|
580
|
+
const data = getOrCreateRenderProxy2D(state, root);
|
|
581
|
+
const resolver = vi.fn((_state: RenderState, resolvedData: RenderProxy) => {
|
|
582
|
+
expect(resolvedData.material).toBe(material);
|
|
583
|
+
expect((resolvedData as RenderProxy2D).clipDepth).toBe(0);
|
|
584
|
+
expect(resolvedData.lastChildrenId).toBe(-1);
|
|
585
|
+
});
|
|
586
|
+
getRenderStateRuntime(state).colorAdjustmentResolver = resolver;
|
|
587
|
+
|
|
588
|
+
updateRenderProxy2D(state, root, data, undefined);
|
|
589
|
+
|
|
590
|
+
expect(resolver).toHaveBeenCalledWith(state, data, undefined);
|
|
591
|
+
expect(resolver).toHaveBeenCalledTimes(1);
|
|
592
|
+
expect(data.clipDepth).toBe(1);
|
|
593
|
+
expect(data.lastChildrenId).toBe(getNodeChildrenRevision(root));
|
|
594
|
+
});
|
|
464
595
|
});
|
|
465
596
|
|
|
466
597
|
describe('updateRenderProxyRenderer', () => {
|
package/src/renderQueue.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { addNodeChild } from '@flighthq/node/contract';
|
|
2
2
|
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
3
|
-
import { createSprite } from '@flighthq/
|
|
3
|
+
import { createSprite } from '@flighthq/scene2d/contract';
|
|
4
4
|
import type { Renderer, RenderProxy, RenderQueueEntry } from '@flighthq/types/contract';
|
|
5
5
|
|
|
6
6
|
import { registerRenderer } from './renderer';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import { emitSignal } from '@flighthq/signals/contract';
|
|
3
|
+
import { RenderRegistry } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import { registerRenderer } from './renderer';
|
|
6
|
+
import { createRenderProxy } from './renderProxy';
|
|
7
|
+
import {
|
|
8
|
+
areRenderRegistryGuardsEnabled,
|
|
9
|
+
enableRenderRegistryGuards,
|
|
10
|
+
explainRenderRegistryMisses,
|
|
11
|
+
} from './renderRegistryGuards';
|
|
12
|
+
import { enableRenderRegistrySignals } from './renderRegistrySignals';
|
|
13
|
+
import { createRenderState } from './renderState';
|
|
14
|
+
|
|
15
|
+
describe('areRenderRegistryGuardsEnabled', () => {
|
|
16
|
+
it('reports whether the state-local registry guard is enabled', () => {
|
|
17
|
+
const state = createRenderState();
|
|
18
|
+
expect(areRenderRegistryGuardsEnabled(state)).toBe(false);
|
|
19
|
+
enableRenderRegistryGuards(state);
|
|
20
|
+
expect(areRenderRegistryGuardsEnabled(state)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('enableRenderRegistryGuards', () => {
|
|
25
|
+
it('is idempotent', () => {
|
|
26
|
+
const state = createRenderState();
|
|
27
|
+
enableRenderRegistryGuards(state);
|
|
28
|
+
enableRenderRegistryGuards(state);
|
|
29
|
+
expect(areRenderRegistryGuardsEnabled(state)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('warns once per state, registry, and kind', () => {
|
|
33
|
+
const firstState = createRenderState();
|
|
34
|
+
const secondState = createRenderState();
|
|
35
|
+
const sink = createMemoryLogSink(4);
|
|
36
|
+
addLogSink(sink.sink);
|
|
37
|
+
enableRenderRegistryGuards(firstState);
|
|
38
|
+
enableRenderRegistryGuards(secondState);
|
|
39
|
+
try {
|
|
40
|
+
createRenderProxy(firstState, { kind: 'acme.Missing' } as never);
|
|
41
|
+
createRenderProxy(firstState, { kind: 'acme.Missing' } as never);
|
|
42
|
+
createRenderProxy(secondState, { kind: 'acme.Missing' } as never);
|
|
43
|
+
|
|
44
|
+
const entries = getMemoryLogSinkEntries(sink);
|
|
45
|
+
expect(entries).toHaveLength(2);
|
|
46
|
+
expect(entries[0]?.data).toMatchObject({
|
|
47
|
+
kind: 'acme.Missing',
|
|
48
|
+
message:
|
|
49
|
+
'createRenderProxy: node kind has no registered renderer — call registerRenderer(state, kind, renderer)',
|
|
50
|
+
registry: RenderRegistry.NodeRenderer,
|
|
51
|
+
});
|
|
52
|
+
} finally {
|
|
53
|
+
removeLogSink(sink.sink);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('stays silent when the node renderer is registered', () => {
|
|
58
|
+
const state = createRenderState();
|
|
59
|
+
const sink = createMemoryLogSink(1);
|
|
60
|
+
addLogSink(sink.sink);
|
|
61
|
+
enableRenderRegistryGuards(state);
|
|
62
|
+
registerRenderer(state, 'acme.Registered', { createData: () => null, submit: () => {} });
|
|
63
|
+
try {
|
|
64
|
+
createRenderProxy(state, { kind: 'acme.Registered' } as never);
|
|
65
|
+
expect(getMemoryLogSinkEntries(sink)).toHaveLength(0);
|
|
66
|
+
} finally {
|
|
67
|
+
removeLogSink(sink.sink);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('explainRenderRegistryMisses', () => {
|
|
73
|
+
it('returns each recorded registry and kind pair once in event order', () => {
|
|
74
|
+
const state = createRenderState();
|
|
75
|
+
enableRenderRegistryGuards(state);
|
|
76
|
+
const signals = enableRenderRegistrySignals(state);
|
|
77
|
+
emitSignal(signals.onRegistryMiss, RenderRegistry.TextureResolver, 'acme.Texture');
|
|
78
|
+
emitSignal(signals.onRegistryMiss, RenderRegistry.TextureResolver, 'acme.Texture');
|
|
79
|
+
emitSignal(signals.onRegistryMiss, RenderRegistry.ShapeCommandHandler, 'acme.ShapeCommand');
|
|
80
|
+
|
|
81
|
+
expect(explainRenderRegistryMisses(state)).toEqual({
|
|
82
|
+
misses: [
|
|
83
|
+
{ kind: 'acme.Texture', registry: RenderRegistry.TextureResolver },
|
|
84
|
+
{ kind: 'acme.ShapeCommand', registry: RenderRegistry.ShapeCommandHandler },
|
|
85
|
+
],
|
|
86
|
+
status: 'misses-recorded',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns a complete empty explanation before any miss is recorded', () => {
|
|
91
|
+
const state = createRenderState();
|
|
92
|
+
expect(explainRenderRegistryMisses(state)).toEqual({ misses: [], status: 'complete' });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { enableRenderRegistrySignals } from './renderRegistrySignals';
|
|
2
|
+
import { createRenderState, getRenderStateRuntime } from './renderState';
|
|
3
|
+
|
|
4
|
+
describe('enableRenderRegistrySignals', () => {
|
|
5
|
+
it('allocates one state-local registry-miss seam and reuses it', () => {
|
|
6
|
+
const state = createRenderState();
|
|
7
|
+
expect(getRenderStateRuntime(state).registryMiss).toBeNull();
|
|
8
|
+
|
|
9
|
+
const signals = enableRenderRegistrySignals(state);
|
|
10
|
+
|
|
11
|
+
expect(signals.onRegistryMiss).toBeDefined();
|
|
12
|
+
expect(getRenderStateRuntime(state).registryMiss?.signals).toBe(signals);
|
|
13
|
+
expect(enableRenderRegistrySignals(state)).toBe(signals);
|
|
14
|
+
});
|
|
15
|
+
});
|
package/src/renderState.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createMatrix } from '@flighthq/geometry/contract';
|
|
|
2
2
|
import type { RenderState } from '@flighthq/types/contract';
|
|
3
3
|
import { BlendMode, EntityRuntimeKey } from '@flighthq/types/contract';
|
|
4
4
|
|
|
5
|
-
import { createRenderState, createRenderStateRuntime, getRenderStateRuntime } from './renderState';
|
|
5
|
+
import { createRenderState, createRenderStateRuntime, destroyRenderState, getRenderStateRuntime } from './renderState';
|
|
6
6
|
|
|
7
7
|
describe('createRenderState', () => {
|
|
8
8
|
let state: RenderState;
|
|
@@ -69,6 +69,7 @@ describe('createRenderState', () => {
|
|
|
69
69
|
describe('createRenderStateRuntime', () => {
|
|
70
70
|
it('initializes the machinery fields', () => {
|
|
71
71
|
const runtime = createRenderStateRuntime();
|
|
72
|
+
expect(runtime.colorAdjustmentResolver).toBeNull();
|
|
72
73
|
expect(runtime.currentFrameId).toStrictEqual(0);
|
|
73
74
|
expect(runtime.renderProxyMap).toStrictEqual(new WeakMap());
|
|
74
75
|
expect(runtime.renderProxyAdapterMap).toStrictEqual(new WeakMap());
|
|
@@ -85,6 +86,20 @@ describe('createRenderStateRuntime', () => {
|
|
|
85
86
|
});
|
|
86
87
|
});
|
|
87
88
|
|
|
89
|
+
describe('destroyRenderState', () => {
|
|
90
|
+
it('clears state-owned traversal and registration storage', () => {
|
|
91
|
+
const state = createRenderState();
|
|
92
|
+
const runtime = getRenderStateRuntime(state);
|
|
93
|
+
runtime.tempStack.push({} as never);
|
|
94
|
+
runtime.renderEffectPaddingResolverRegistry = new Map();
|
|
95
|
+
|
|
96
|
+
destroyRenderState(state);
|
|
97
|
+
|
|
98
|
+
expect(runtime.tempStack).toHaveLength(0);
|
|
99
|
+
expect(runtime.renderEffectPaddingResolverRegistry).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
88
103
|
describe('getRenderStateRuntime', () => {
|
|
89
104
|
it('returns the runtime attached under EntityRuntimeKey', () => {
|
|
90
105
|
const state = createRenderState();
|
package/src/renderTarget.test.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { createMatrix, createRectangle } from '@flighthq/geometry/contract';
|
|
1
|
+
import { createMatrix, createRectangle, multiplyMatrix } from '@flighthq/geometry/contract';
|
|
2
|
+
import { getNodeLocalMatrix } from '@flighthq/node/contract';
|
|
2
3
|
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
|
-
computeScene2DRenderTargetTransform,
|
|
6
6
|
computeRenderCacheTransform,
|
|
7
7
|
computeRenderTargetSize,
|
|
8
|
+
computeScene2DRenderTargetTransform,
|
|
9
|
+
explainRenderTargetAxes,
|
|
10
|
+
resolveRenderTargetDescriptor,
|
|
8
11
|
} from './renderTarget';
|
|
9
12
|
|
|
10
13
|
describe('computeRenderCacheTransform', () => {
|
|
@@ -40,6 +43,14 @@ describe('computeRenderTargetSize', () => {
|
|
|
40
43
|
expect(result.height).toBe(88);
|
|
41
44
|
});
|
|
42
45
|
|
|
46
|
+
it('adds directional padding on its corresponding sides', () => {
|
|
47
|
+
const result = computeRenderTargetSize(
|
|
48
|
+
{ x: -3, y: 4, width: 100, height: 80 },
|
|
49
|
+
{ bottom: 7, left: 2, right: 5, top: 3 },
|
|
50
|
+
);
|
|
51
|
+
expect(result).toEqual({ width: 107, height: 90 });
|
|
52
|
+
});
|
|
53
|
+
|
|
43
54
|
it('respects minWidth and minHeight', () => {
|
|
44
55
|
const result = computeRenderTargetSize({ x: 0, y: 0, width: 0, height: 0 }, 0, 32, 16);
|
|
45
56
|
expect(result.width).toBe(32);
|
|
@@ -73,12 +84,109 @@ describe('computeScene2DRenderTargetTransform', () => {
|
|
|
73
84
|
expect(out2.tx).not.toBe(out1.tx);
|
|
74
85
|
});
|
|
75
86
|
|
|
76
|
-
it('
|
|
87
|
+
it('cancels a transformed detached root while retaining the content-origin translation', () => {
|
|
77
88
|
const obj = createDisplayObject();
|
|
78
89
|
obj.x = 50;
|
|
79
90
|
obj.y = 30;
|
|
80
|
-
|
|
91
|
+
obj.rotation = 31;
|
|
92
|
+
obj.scaleX = 2;
|
|
93
|
+
obj.scaleY = 0.5;
|
|
94
|
+
const bounds = createRectangle(-12, -7, 100, 80);
|
|
81
95
|
const out = createMatrix();
|
|
82
|
-
|
|
96
|
+
computeScene2DRenderTargetTransform(out, obj, bounds, 9, 4);
|
|
97
|
+
const composed = createMatrix();
|
|
98
|
+
multiplyMatrix(composed, out, getNodeLocalMatrix(obj));
|
|
99
|
+
|
|
100
|
+
expect(composed.a).toBeCloseTo(1);
|
|
101
|
+
expect(composed.b).toBeCloseTo(0);
|
|
102
|
+
expect(composed.c).toBeCloseTo(0);
|
|
103
|
+
expect(composed.d).toBeCloseTo(1);
|
|
104
|
+
expect(composed.tx).toBeCloseTo(21);
|
|
105
|
+
expect(composed.ty).toBeCloseTo(11);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('resolveRenderTargetDescriptor', () => {
|
|
110
|
+
it('resolves every optional target axis and clear policy', () => {
|
|
111
|
+
expect(resolveRenderTargetDescriptor({ width: 64, height: 48 })).toEqual({
|
|
112
|
+
width: 64,
|
|
113
|
+
height: 48,
|
|
114
|
+
format: 'rgba8',
|
|
115
|
+
colorAttachments: 1,
|
|
116
|
+
colorFormats: ['rgba8'],
|
|
117
|
+
sampleCount: 1,
|
|
118
|
+
depth: 'none',
|
|
119
|
+
colorSpace: 'srgb',
|
|
120
|
+
clearColors: [],
|
|
121
|
+
clearDepth: 1,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('explainRenderTargetAxes', () => {
|
|
126
|
+
it('reports every changed axis in stable descriptor order', () => {
|
|
127
|
+
const requested = resolveRenderTargetDescriptor({
|
|
128
|
+
width: 64,
|
|
129
|
+
height: 48,
|
|
130
|
+
format: 'rgba16f',
|
|
131
|
+
colorAttachments: 2,
|
|
132
|
+
sampleCount: 8,
|
|
133
|
+
depth: 'depth-stencil-sampled',
|
|
134
|
+
});
|
|
135
|
+
const effective = {
|
|
136
|
+
...requested,
|
|
137
|
+
format: 'rgba8' as const,
|
|
138
|
+
colorFormats: ['rgba8', 'rgba8'] as const,
|
|
139
|
+
sampleCount: 4,
|
|
140
|
+
depth: 'depth-stencil' as const,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
expect(explainRenderTargetAxes(requested, effective)).toEqual([
|
|
144
|
+
{ axis: 'format', effective: 'rgba8', requested: 'rgba16f' },
|
|
145
|
+
{ axis: 'colorFormats', effective: ['rgba8', 'rgba8'], requested: ['rgba16f', 'rgba16f'] },
|
|
146
|
+
{ axis: 'sampleCount', effective: 4, requested: 8 },
|
|
147
|
+
{ axis: 'depth', effective: 'depth-stencil', requested: 'depth-stencil-sampled' },
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns no differences for identical canonical axes', () => {
|
|
152
|
+
const axes = resolveRenderTargetDescriptor({ width: 64, height: 48 });
|
|
153
|
+
expect(explainRenderTargetAxes(axes, { ...axes, colorFormats: [...axes.colorFormats] })).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('normalizes dimensions, attachment count, and sample count once', () => {
|
|
158
|
+
expect(
|
|
159
|
+
resolveRenderTargetDescriptor({
|
|
160
|
+
width: 10.2,
|
|
161
|
+
height: 0,
|
|
162
|
+
colorAttachments: 1.2,
|
|
163
|
+
sampleCount: 3.1,
|
|
164
|
+
}),
|
|
165
|
+
).toMatchObject({
|
|
166
|
+
width: 11,
|
|
167
|
+
height: 1,
|
|
168
|
+
colorAttachments: 2,
|
|
169
|
+
sampleCount: 4,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('expands heterogeneous color formats across every requested attachment', () => {
|
|
174
|
+
const resolved = resolveRenderTargetDescriptor({
|
|
175
|
+
width: 64,
|
|
176
|
+
height: 48,
|
|
177
|
+
format: 'rgba16f',
|
|
178
|
+
colorAttachments: 3,
|
|
179
|
+
colorFormats: ['rgba8', 'rgba32f'],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(resolved.format).toBe('rgba8');
|
|
183
|
+
expect(resolved.colorFormats).toEqual(['rgba8', 'rgba32f', 'rgba16f']);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('copies caller-owned clear arrays', () => {
|
|
187
|
+
const clearColors = [0xff0000ff];
|
|
188
|
+
const resolved = resolveRenderTargetDescriptor({ width: 64, height: 48, clearColors });
|
|
189
|
+
clearColors[0] = 0;
|
|
190
|
+
expect(resolved.clearColors).toEqual([0xff0000ff]);
|
|
83
191
|
});
|
|
84
192
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry/contract';
|
|
1
2
|
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
2
3
|
|
|
3
4
|
import { createRenderProxy2D } from './renderProxy';
|
|
@@ -13,13 +14,26 @@ describe('updateRenderProxy2DTransform', () => {
|
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
it('returns false when nothing changed', () => {
|
|
16
|
-
const state = createRenderState();
|
|
17
|
+
const state = createRenderState({ sceneGraphSyncPolicy: 'requiresInvalidation' });
|
|
17
18
|
const obj = createDisplayObject();
|
|
18
19
|
const data = createRenderProxy2D(state, obj);
|
|
19
20
|
updateRenderProxy2DTransform(state, data);
|
|
20
21
|
expect(updateRenderProxy2DTransform(state, data)).toBe(false);
|
|
21
22
|
});
|
|
22
23
|
|
|
24
|
+
it('refreshes an unchanged root node when the render-state transform changes', () => {
|
|
25
|
+
const state = createRenderState();
|
|
26
|
+
const obj = createDisplayObject();
|
|
27
|
+
const data = createRenderProxy2D(state, obj);
|
|
28
|
+
|
|
29
|
+
updateRenderProxy2DTransform(state, data);
|
|
30
|
+
state.renderTransform2D = createMatrix(2, 0, 0, 2, 17, 0);
|
|
31
|
+
|
|
32
|
+
expect(updateRenderProxy2DTransform(state, data)).toBe(true);
|
|
33
|
+
expect(data.transform2D.a).toBe(2);
|
|
34
|
+
expect(data.transform2D.tx).toBe(17);
|
|
35
|
+
});
|
|
36
|
+
|
|
23
37
|
it('returns true when parent was updated this frame', () => {
|
|
24
38
|
const state = createRenderState();
|
|
25
39
|
getRenderStateRuntime(state).currentFrameId = 2;
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
invalidateNodeLocalBounds,
|
|
6
6
|
invalidateNodeLocalTransform,
|
|
7
7
|
} from '@flighthq/node/contract';
|
|
8
|
-
import {
|
|
8
|
+
import { createSprite, createDisplayObject } from '@flighthq/scene2d/contract';
|
|
9
9
|
import type { HasBoundsRectangleRuntime, RenderProxy2D, RenderViewport2D } from '@flighthq/types/contract';
|
|
10
10
|
|
|
11
11
|
import { createRenderProxy2D, prepareScene2DRender } from './renderProxy';
|
package/src/renderer.test.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { Scene2DClipHooks, Renderer, RenderState } from '@flighthq/types/contract';
|
|
1
|
+
import type { CanvasShapeCommand, Scene2DClipHooks, Renderer, RenderState } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
copyAllRenderersFromRenderState,
|
|
5
|
+
copyRenderStateRegistrations,
|
|
5
6
|
copyRenderersFromRenderState,
|
|
6
7
|
noopRendererData,
|
|
7
8
|
registerRenderer,
|
|
@@ -68,6 +69,48 @@ describe('copyRenderersFromRenderState', () => {
|
|
|
68
69
|
});
|
|
69
70
|
});
|
|
70
71
|
|
|
72
|
+
describe('copyRenderStateRegistrations', () => {
|
|
73
|
+
it('snapshot-copies effect-padding registrations without aliasing the maps', () => {
|
|
74
|
+
const source = createRenderState();
|
|
75
|
+
const target = createRenderState();
|
|
76
|
+
const resolver = vi.fn();
|
|
77
|
+
const colorAdjustmentResolver = vi.fn();
|
|
78
|
+
getRenderStateRuntime(source).colorAdjustmentResolver = colorAdjustmentResolver;
|
|
79
|
+
getRenderStateRuntime(source).renderEffectPaddingResolverRegistry = new Map([['acme.Effect', resolver]]);
|
|
80
|
+
|
|
81
|
+
copyRenderStateRegistrations(target, source);
|
|
82
|
+
|
|
83
|
+
expect(getRenderStateRuntime(target).colorAdjustmentResolver).toBe(colorAdjustmentResolver);
|
|
84
|
+
expect(getRenderStateRuntime(target).renderEffectPaddingResolverRegistry).not.toBe(
|
|
85
|
+
getRenderStateRuntime(source).renderEffectPaddingResolverRegistry,
|
|
86
|
+
);
|
|
87
|
+
expect(getRenderStateRuntime(target).renderEffectPaddingResolverRegistry?.get('acme.Effect')).toBe(resolver);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('snapshot-copies the shape-command registry without aliasing the map', () => {
|
|
91
|
+
const source = createRenderState();
|
|
92
|
+
const target = createRenderState();
|
|
93
|
+
const command = { key: 'beginFill', draw: vi.fn() } as unknown as CanvasShapeCommand;
|
|
94
|
+
getRenderStateRuntime(source).canvasShapeCommandRegistry = new Map([['beginFill', command]]);
|
|
95
|
+
|
|
96
|
+
copyRenderStateRegistrations(target, source);
|
|
97
|
+
|
|
98
|
+
expect(getRenderStateRuntime(target).canvasShapeCommandRegistry).not.toBe(
|
|
99
|
+
getRenderStateRuntime(source).canvasShapeCommandRegistry,
|
|
100
|
+
);
|
|
101
|
+
expect(getRenderStateRuntime(target).canvasShapeCommandRegistry?.get('beginFill')).toBe(command);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('leaves the shape-command registry null when the source never registered one', () => {
|
|
105
|
+
const source = createRenderState();
|
|
106
|
+
const target = createRenderState();
|
|
107
|
+
|
|
108
|
+
copyRenderStateRegistrations(target, source);
|
|
109
|
+
|
|
110
|
+
expect(getRenderStateRuntime(target).canvasShapeCommandRegistry).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
71
114
|
describe('noopRendererData', () => {
|
|
72
115
|
it('returns null', () => {
|
|
73
116
|
const state = createRenderState();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { SCENE2D_WORKING_COLOR_SPACE } from './scene2dWorkingColorSpace';
|
|
4
|
+
|
|
5
|
+
describe('SCENE2D_WORKING_COLOR_SPACE', () => {
|
|
6
|
+
// The 2D tower composites in the encoded domain — byte-through, no decode, no encode. Changing this
|
|
7
|
+
// is a rendered-output change for every 2D scene, not a refactor.
|
|
8
|
+
it('declares the 2D tower encoded, matching its byte-through pipeline', () => {
|
|
9
|
+
expect(SCENE2D_WORKING_COLOR_SPACE).toBe('srgb');
|
|
10
|
+
});
|
|
11
|
+
});
|