Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/packages/glyph.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Implements portable font loading, retained Rust shaping and layout,
resource: ../../packages/glyph
workspace_package: '@pmndrs/glyph'
documentation_type: reference
source_digest: 'sha256:f0d02444415a88d11af570a4dd49ef7681316756fd3aa9e3a6f7d7ea09d96c82'
source_digest: 'sha256:5d9e3d71131a7bc1206842e1be7e9a5799a27ec8e2a64831e78acb158aa4ff96'
tags: [package, public-api, rust, wasm, threejs, typography]
sources:
- id: manifest
Expand Down
125 changes: 125 additions & 0 deletions packages/glyph/tests/integration/three-engine-runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,131 @@ test('Three coordinator shares shaping data across technique bindings and refere
decorationTarget.dispose();
decorationSession.dispose();

// Regression canary: a colored span whose fontSize animates through value-equality
// with its root keeps its style segment (the paint differs) while the shaping-run
// table merges across it -- `same_layout_style` compares layout scalars and ignores
// paint, and `font_size` is one of those scalars. When the next tick moves the size
// off equality the table splits again under metrics-only invalidation. The engine
// once retained the merged shape against the rebuilt table and rejected every later
// frame with invalidRequest, poisoning the session; `shaping_run_topology_stable`
// in engine/state.rs was added to stop that.
//
// This drives the merge -> split path end to end and asserts the session stays
// healthy across it. It does not isolate that one guard: forcing
// `shaping_run_topology_stable` to return `true` -- the pre-fix behaviour -- leaves
// this green, so a later path absorbs the split as well. Treat it as a canary over
// the whole retained-shape path rather than as proof of a single predicate.
const topologySession = coordinator.createSession({
requestCapacity: 8_192,
resultCapacity: 1024 * 1024,
textCapacity: 64,
});
const topologyStyles = (spanSize) => [
{
opcode: 'upsert',
paragraphId: 1,
styleId: 1,
cascadeOrder: 0,
start: 0,
end: 24,
root: true,
value: {
fontStackHandle: first.handle,
materialId: primaryMaterial.id,
fontSize: 16,
rasterPixelRatio: 1,
foregroundRgba: 0xffff_ffff,
},
},
{
opcode: 'upsert',
paragraphId: 1,
styleId: 2,
cascadeOrder: 1,
start: 8,
end: 16,
value: { fontSize: spanSize, foregroundRgba: 0xff44_22ff },
},
];
const topologyFrame = (previous, spanSize, extra = {}) =>
compileTextEngineFrameUpdate({
sessionId: topologySession.handle,
policyHandle: coordinator.policyHandle,
capabilitySet: 1,
expectedEngineRevision: previous?.engineRevision ?? 0,
consumedPlanRevision: previous?.planRevision ?? 0,
acknowledgedPublicationGeneration: previous?.publicationGeneration ?? 0,
limits: {
maxParagraphs: 1,
maxClusters: 32,
maxLines: 8,
maxRegions: 1,
maxExclusions: 1,
maxInlineObjects: 1,
maxSlotsPerBand: 2,
maxOutputBytes: 1024 * 1024,
},
styleMutations: topologyStyles(spanSize),
...extra,
});
// Frame 1: the span size equals the root — shaping runs merge across the span.
const topologyFirst = topologySession.update(
topologyFrame(undefined, 16, {
paragraphMutations: [{ opcode: 'upsert', paragraphId: 1, order: 0 }],
textMutations: [{ paragraphId: 1, start: 0, deleteCount: 0, insert: 'alpha beta gamma epsilon' }],
constraints: [
{
paragraphId: 1,
flowThreadId: 1,
geometryRevision: 1,
width: 512,
height: 128,
viewportBlockStart: 0,
viewportBlockEnd: 128,
resumeBlockOffset: 0,
maxLines: 8,
regionStart: 0,
resumeCluster: 0,
regionCount: 1,
resumeRegion: 0,
widthMode: 'at-most',
heightMode: 'at-most',
wrap: 'word',
align: 'start',
overflow: 'visible',
blockAlign: 'start',
},
],
regions: [
{
id: 1,
geometryRevision: 1,
shape: 'rectangle',
exclusionStart: 0,
exclusionCount: 0,
writingMode: 'horizontal-tb',
textOrientation: 'mixed',
inlineStart: 0,
blockStart: 0,
inlineEnd: 512,
blockEnd: 128,
clipInlineStart: 0,
clipBlockStart: 0,
clipInlineEnd: 512,
clipBlockEnd: 128,
},
],
}),
);
// Frame 2: the span size moves off equality — the run table splits again and the
// retained shape must be rebuilt, not reused.
const topologySecond = topologySession.update(topologyFrame(topologyFirst, 17.5));
assert.equal(topologySecond.engineRevision, topologyFirst.engineRevision + 1);
// Frame 3: and the session must remain healthy afterwards.
const topologyThird = topologySession.update(topologyFrame(topologySecond, 18.25));
assert.equal(topologyThird.engineRevision, topologySecond.engineRevision + 1);
topologySession.dispose();

target.dispose();
session.dispose();
first.release();
Expand Down
Loading