Skip to content
Open
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
16 changes: 12 additions & 4 deletions apps/benchmarks/src/benchmark/package-size-budgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ export const packageSizeBudgets = {
// code (schema normalization and freezing, DSL session provenance,
// schemaPolicyBuffers/schemaFieldTable). Re-based when tsdown bundling lands per
// the technique contract plan.
// The external-raster routing around the Worker font-bake plan added +1,786 raw /
// +919 minified of core routing code; the generated evidence was not refolded at
// the time, so the growth first surfaced in the CI gate's fresh build (228,199 raw
// / 155,956 minified, byte-identical across linux-x64 and darwin). Gzip and Brotli
// still fit under their prior ceilings.
'core-subpath-js': {
rawBytes: 227_000,
minifiedBytes: 155_500,
rawBytes: 229_000,
minifiedBytes: 156_500,
gzipBytes: 39_700,
brotliBytes: 34_200,
},
Expand Down Expand Up @@ -63,9 +68,12 @@ export const packageSizeBudgets = {
// the compressed ceilings hold with tight headroom by design.
// Column flow (contentBox columns over ordered regions) added ~+1.7 KB raw of
// geometry derivation and validation in the Three adapter.
// The external-raster routing rode into the Three bundle too: +1,786 raw /
// +914 minified (370,521 raw / 241,995 minified measured by the CI gate).
// Gzip and Brotli still fit under their prior ceilings.
'three-runtime-js': {
rawBytes: 370_000,
minifiedBytes: 241_500,
rawBytes: 371_500,
minifiedBytes: 242_500,
gzipBytes: 62_500,
brotliBytes: 52_800,
},
Expand Down
13 changes: 13 additions & 0 deletions docs/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
from text to Glyph. Typography concepts such as `Text`, formatted text, and the text-shaper artifact retain their
domain names. Regenerated authenticated font artifacts and reviewed package-size evidence under the new identity.

## 2026-08-13

- **External runtime bake routing** — The third-party proof lane (`benchmark:external-raster`) had been failing on
main: the runtime shoveled every requested raster into the Worker font-bake plan, and the Worker's embedded
baker switch correctly rejected kinds it does not carry — killing the whole load even though the host-side path
through `technique.runtimeBaker` exists for exactly this case. The routing authority is now one declared set,
`workerRasterKinds`, exported from the runtime-bake subpath: Worker plans carry only Worker-embedded kinds, and
every other technique's raster deliberately misses in the baked artifact and bakes host-side through the baker
its own declaration names. Proven red-green in Node with the published example package (a strict stub enforcing
the Worker's contract) and by the restored browser lane: deterministic external raster frames on WebGPU and
WebGL2 with identical hashes. Verified pre-existing on a clean origin/main worktree before fixing, so this is a
repair, not a regression from the stack.

## 2026-08-12

- **Column flow (11.18 slice)** — `ParagraphContentBox` gains `columns { count, gap }`, mapping the public `Text`
Expand Down
2 changes: 1 addition & 1 deletion docs/packages/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Provides the shared interactive and automated benchmark product sur
resource: ../../apps/benchmarks
workspace_package: '@pmndrs/glyph-benchmarks'
documentation_type: reference
source_digest: 'sha256:00aa46b3f1dea9587df3a45417bd881260f05c41895121b46b17c6aa71ecc284'
source_digest: 'sha256:daf4eb1aae9d603b9a055b155b8867712094111b2efa11a77e32e395b1ebfa7c'
tags: [package, benchmarks, react, vite, product-e2e]
sources:
- id: manifest
Expand Down
2 changes: 1 addition & 1 deletion docs/packages/glyph-example-raster.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Proves the published raster and baker extension boundary with a pri
resource: ../../packages/glyph-example-raster
workspace_package: '@pmndrs/glyph-example-raster'
documentation_type: reference
source_digest: 'sha256:043a08a8cfdf41c283576d82c993f95dad743ebf8c2256ff5744128cf4aca95e'
source_digest: 'sha256:93833ff29db776bfc2d0e3ebfd79d17501b480f3e0a89135a6fca4bf89fbd89d'
tags: [package, raster, extension-proof, threejs, tsl]
sources:
- id: manifest
Expand Down
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:c5b0141b2c814098bb31a9fa20f01bb013ce931951de96bb4fa59cddbee0c0c3'
source_digest: 'sha256:adb0aa8d4dd23153e97b36b3a359b699b0656c5f449331c30d8293b72d5e302e'
tags: [package, public-api, rust, wasm, threejs, typography]
sources:
- id: manifest
Expand Down
71 changes: 71 additions & 0 deletions packages/glyph-example-raster/tests/runtime-bake-routing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import assert from 'node:assert/strict';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { createTextRuntime, type RuntimeFontBakeRequest } from '@pmndrs/text';
import { bakeFont } from '@pmndrs/text/bake';
import { workerRasterKinds } from '@pmndrs/text/runtime-bake';
import { afterEach, test } from 'vitest';

import { glyphExample } from '../src/index.js';

const fixtureDirectory = new URL('../../../apps/benchmarks/fixtures/fonts/inter-v4.1/', import.meta.url);
const shaperWasmUrl = new URL('../../text/dist/text_shaper.wasm', import.meta.url);

const cleanups: (() => Promise<void>)[] = [];
afterEach(async () => {
while (cleanups.length > 0) await cleanups.pop()?.();
});

/**
* The published external contract, end to end: an external technique's raster
* never rides the Worker font-bake plan and instead bakes host-side through
* the baker its own declaration names. The stub stands in for the Worker and
* enforces its real contract by rejecting any kind outside the declared set;
* it returns a core artifact baked from the same source so the host-side
* attachment passes the provenance check.
*/
test('the example technique bakes host-side while the Worker plan stays first-party', async () => {
const source = await readFile(new URL('Inter-Regular.ttf', fixtureDirectory));
const outputRoot = await mkdtemp(join(tmpdir(), 'glyph-example-routing-'));
cleanups.push(() => rm(outputRoot, { recursive: true, force: true }));
const stubOutput = join(outputRoot, 'Inter-Regular.font.glb');
await bakeFont({
input: new URL('Inter-Regular.ttf', fixtureDirectory),
output: stubOutput,
font: { fontFaceIndex: 0 },
});
const artifact = await readFile(stubOutput);

const runtime = await createTextRuntime({ wasm: await readFile(shaperWasmUrl) });
cleanups.push(async () => runtime.dispose());
const requests: RuntimeFontBakeRequest[] = [];
const runtimeBake = async (request: RuntimeFontBakeRequest) => {
for (const raster of request.rasters ?? []) {
if (!workerRasterKinds.includes(raster.kind)) {
throw new Error(`runtime font baker does not support raster kind ${raster.kind}`);
}
}
requests.push(request);
return new Uint8Array(artifact.buffer.slice(artifact.byteOffset, artifact.byteOffset + artifact.byteLength));
};

const [example] = await runtime.loadFont({
input: {
source: `data:font/ttf;base64,${source.toString('base64')}`,
runtimeBake,
},
rasters: [{ technique: glyphExample, options: { paletteSeed: 17, inset: 0.1 } }],
});

assert.equal(requests.length, 1, 'the source load bakes its core through the Worker path once');
assert.deepEqual(
(requests[0]?.rasters ?? []).map(({ kind }) => kind),
[],
'the Worker plan carries no external kinds',
);
assert.equal(example.technique, glyphExample);
assert.ok(example.data, 'the external raster decodes from its host-baked artifact');
example.dispose();
});
10 changes: 10 additions & 0 deletions packages/glyph/src/internal/runtime-bake-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ export interface RuntimeBakeUnicodeRangeV0 {
readonly end: number;
}

/**
* The raster kinds the runtime bake Worker embeds bakers for. This set is the
* single routing authority: the host puts only these kinds into a Worker font
* bake, and every other technique bakes host-side through the baker its own
* declaration names (`technique.runtimeBaker`). The Worker's kind switch is
* the realization of this set; its rejection of anything else guards protocol
* violations, not routing.
*/
export const workerRasterKinds: readonly string[] = Object.freeze(['bitmap', 'msdf', 'slug']);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete. Holding two versions of baking routing still.


export interface RuntimeBakeRasterV0 {
readonly kind: string;
readonly extension: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/glyph/src/runtime-bake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
type RuntimeBakeRequestV0,
type RuntimeBakeResultV0,
} from './internal/runtime-bake-protocol.js';

export { workerRasterKinds } from './internal/runtime-bake-protocol.js';
import { SerialWorkerHost } from './internal/serial-worker-host.js';
import { isBakeProgressMessageV0, type BakeProgressMessageV0 } from './internal/bake-progress-protocol.js';

Expand Down
13 changes: 11 additions & 2 deletions packages/glyph/src/text-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
} from './loader.js';
import { canonicalJson, deriveRasterKey } from './internal/raster-identity.js';
import { normalizeUnicodeRanges } from './internal/font-selection.js';
import type { RuntimeBakeRasterV0, RuntimeBakeUnicodeRangeV0 } from './internal/runtime-bake-protocol.js';
import {
workerRasterKinds,
type RuntimeBakeRasterV0,
type RuntimeBakeUnicodeRangeV0,
} from './internal/runtime-bake-protocol.js';
import { getRegisteredFontData } from './internal/registered-font.js';
import type {
AnyRasterTechnique,
Expand Down Expand Up @@ -212,7 +216,12 @@ class TextRuntimeImpl implements TextRuntime {
if ('baked' in input)
return this.#defaultLoader.load({ baked: input.baked }, signal === undefined ? {} : { signal });
const unicodeRanges = input.unicodeRanges === undefined ? undefined : normalizeUnicodeRanges(input.unicodeRanges);
const rasters = await Promise.all(rasterRequests.map(runtimeBakeRaster));
// Only Worker-embedded kinds ride the Worker font-bake plan. Every other
// technique is left out of the plan on purpose: its raster misses in the
// baked artifact and bakes host-side through the technique's own declared
// runtime baker.
const workerRequests = rasterRequests.filter((request) => workerRasterKinds.includes(request.technique.kind));
const rasters = await Promise.all(workerRequests.map(runtimeBakeRaster));
const planKey = canonicalJson({
rasters,
unicodeRanges: unicodeRanges ?? null,
Expand Down
75 changes: 75 additions & 0 deletions packages/glyph/tests/integration/runtime-bake.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,81 @@ test('one TextRuntime source load sends its normalized ranges and complete raste
assert.equal(msdfFont.font, slugFont.font);
});

test('external techniques bake through their own declared baker, never the Worker plan', async (t) => {
// Self-contained routing proof: a minimal external technique whose declared
// baker throws a sentinel. Reaching the sentinel proves the host-side route;
// the strict stub proves the Worker plan never saw the external kind.
const { source } = await fixturePromise;
const outputRoot = await mkdtemp(join(tmpdir(), 'pmndrs-glyph-external-route-'));
t.after(() => rm(outputRoot, { recursive: true, force: true }));
const stubOutput = join(outputRoot, 'Inter-Regular.font.glb');
await bakeFont({
input: new URL('Inter-Regular.ttf', fixtureDirectory),
output: stubOutput,
font: { fontFaceIndex: 0 },
rasters: [
{
baker: bitmapBaker,
packaging: { artifact: 'embedded', pages: 'embedded' },
options: { strikes: [32] },
},
],
});
const artifact = await readFile(stubOutput);
const { defineRasterTechnique } = await import('@pmndrs/glyph');
const { workerRasterKinds } = await import('@pmndrs/glyph/runtime-bake');
const external = defineRasterTechnique({
id: 'test.external-route',
kind: 'testExternal',
extension: 'TEST_external_route',
version: 0,
runtimeBaker: () =>
Promise.resolve({
kind: 'testExternal',
bake() {
throw new Error('external-route-sentinel');
},
}),
descriptor() {
return {};
},
async decode() {
return {};
},
dispose() {},
});
const requests = [];
const runtime = await createTextRuntime({
wasm: await readFile(new URL('../../dist/text_shaper.wasm', import.meta.url)),
});
t.after(() => runtime.dispose());
const runtimeBake = async (request) => {
for (const raster of request.rasters ?? []) {
if (!workerRasterKinds.includes(raster.kind)) {
throw new Error(`runtime font baker does not support raster kind ${raster.kind}`);
}
}
requests.push(request);
return new Uint8Array(artifact.slice(0));
};
await assert.rejects(
runtime.loadFont({
input: {
source: `data:font/ttf;base64,${Buffer.from(source).toString('base64')}`,
runtimeBake,
},
rasters: [{ technique: bitmap, options: { strikes: [32] } }, { technique: external }],
}),
/external-route-sentinel/,
'the external technique must reach its own declared baker',
);
assert.equal(requests.length, 1);
assert.deepEqual(
requests[0].rasters.map(({ kind }) => kind),
['bitmap'],
'the Worker plan carries only kinds the Worker declares',
);
});
test('the Worker retries a failed Wasm fetch and retains the recovered core', async (t) => {
const { source } = await fixturePromise;
const originals = {
Expand Down
Loading