diff --git a/apps/benchmarks/src/benchmark/package-size-budgets.ts b/apps/benchmarks/src/benchmark/package-size-budgets.ts index 27ff05fe..c8c42175 100644 --- a/apps/benchmarks/src/benchmark/package-size-budgets.ts +++ b/apps/benchmarks/src/benchmark/package-size-budgets.ts @@ -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, }, @@ -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, }, diff --git a/docs/log.md b/docs/log.md index d415ce90..3332faa6 100644 --- a/docs/log.md +++ b/docs/log.md @@ -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` diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 14d69498..93c32d13 100644 --- a/docs/packages/benchmarks.md +++ b/docs/packages/benchmarks.md @@ -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 diff --git a/docs/packages/glyph-example-raster.md b/docs/packages/glyph-example-raster.md index 9466f1a3..9bf49709 100644 --- a/docs/packages/glyph-example-raster.md +++ b/docs/packages/glyph-example-raster.md @@ -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 diff --git a/docs/packages/glyph.md b/docs/packages/glyph.md index acb4820e..72e0bb35 100644 --- a/docs/packages/glyph.md +++ b/docs/packages/glyph.md @@ -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 diff --git a/packages/glyph-example-raster/tests/runtime-bake-routing.test.ts b/packages/glyph-example-raster/tests/runtime-bake-routing.test.ts new file mode 100644 index 00000000..1bd49ba6 --- /dev/null +++ b/packages/glyph-example-raster/tests/runtime-bake-routing.test.ts @@ -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)[] = []; +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(); +}); diff --git a/packages/glyph/src/internal/runtime-bake-protocol.ts b/packages/glyph/src/internal/runtime-bake-protocol.ts index cc4644c7..62c09bb1 100644 --- a/packages/glyph/src/internal/runtime-bake-protocol.ts +++ b/packages/glyph/src/internal/runtime-bake-protocol.ts @@ -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']); + export interface RuntimeBakeRasterV0 { readonly kind: string; readonly extension: string; diff --git a/packages/glyph/src/runtime-bake.ts b/packages/glyph/src/runtime-bake.ts index 54ad0f47..48dd477f 100644 --- a/packages/glyph/src/runtime-bake.ts +++ b/packages/glyph/src/runtime-bake.ts @@ -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'; diff --git a/packages/glyph/src/text-runtime.ts b/packages/glyph/src/text-runtime.ts index 03d7e150..fff0818c 100644 --- a/packages/glyph/src/text-runtime.ts +++ b/packages/glyph/src/text-runtime.ts @@ -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, @@ -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, diff --git a/packages/glyph/tests/integration/runtime-bake.test.mjs b/packages/glyph/tests/integration/runtime-bake.test.mjs index 2d35d404..81d10521 100644 --- a/packages/glyph/tests/integration/runtime-bake.test.mjs +++ b/packages/glyph/tests/integration/runtime-bake.test.mjs @@ -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 = {