From 3be2b78973c7c171f9a27c3fac7855e3e27962a9 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 04:43:43 -0400 Subject: [PATCH 01/13] fix(tooling): complete fresh-checkout benchmark setup --- README.md | 2 +- docs/log.md | 2 ++ docs/packages/benchmarks.md | 2 +- mise.toml | 1 + rust-toolchain.toml | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d540e6ef..5a336e20 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ pnpm install pnpm check ``` -Use mise to install the exact root Node.js, pnpm, stable Rust, Meson, and Ninja pins. Meson and Ninja build the authenticated HarfBuzz oracle utilities used by the clean-checkout fixture gates. HarfBuzz gates those command-line utilities on GLib development metadata, so the build host must also provide `glib-2.0` through `pkg-config` (`libglib2.0-dev` on Ubuntu or `glib` on macOS with Homebrew). CI declares that native prerequisite and prints the selected GLib version before checks run. The canonical mise versions are required; do not substitute merely compatible local toolchains. The optional coverage-guided font-baker fuzzer is isolated +Use mise to install the exact root Node.js, pnpm, stable Rust, pipx, Meson, and Ninja pins. The pinned pipx tool bootstraps the pinned Meson application; Meson and Ninja build the authenticated HarfBuzz oracle utilities used by the clean-checkout fixture gates. HarfBuzz gates those command-line utilities on GLib development metadata, so the build host must also provide `glib-2.0` through `pkg-config` (`libglib2.0-dev` on Ubuntu or `glib` plus `pkgconf` on macOS with Homebrew). CI declares that native prerequisite and prints the selected GLib version before checks run. The canonical mise versions are required; do not substitute merely compatible local toolchains. The optional coverage-guided font-baker fuzzer is isolated under `packages/font-baker/fuzz`; its nested mise configuration provisions the exact dated nightly and `cargo-fuzz` release required by that workspace when `fuzz:rust` runs. diff --git a/docs/log.md b/docs/log.md index 4be870a1..24db4968 100644 --- a/docs/log.md +++ b/docs/log.md @@ -8,6 +8,8 @@ ## 2026-07-31 +- **Fresh-checkout HarfBuzz bootstrap** — Added an exact root pipx pin before the existing pipx-backed Meson pin, so `mise install` no longer assumes an ambient pipx executable. The minimal Rust toolchain declares Cargo explicitly instead of relying on an implicit profile component that was absent from a fresh Linux mise cache. The macOS prerequisite now names both Homebrew `glib` and `pkgconf`, making the required `glib-2.0` metadata discoverable when the authenticated HarfBuzz fixture workflow configures its pinned utilities. +- **Milestone 8.6 regeneration** — Refreshed the fail-closed autoresearch provenance after the coverage and SIMD work changed the canonical package-size report bytes. The baseline now authenticates the current report digest; the package-size lane independently rebuilds and checks the measured entries and reviewed ceilings. - **Host-scoped MTSDF admission evidence** — Labeled the compiled admission-module measurement by platform and architecture. The recorded host retains exact evidence freshness; foreign hosts must rebuild, reproduce the portable contract and synthetic output, retain zero imports, emit a complete SHA-256 identity, and remain under reviewed raw/optimized/gzip/Brotli ceilings. Focused contract tests reject stale portable fields, incomplete hashes, and budget overflow. - **Coverage-era size gates** — Reconciled the package-size record and its fail-closed autoresearch provenance with bounded raster coverage. Narrow current-capability ceilings pair with an independent regression that preserves the pre-coverage browser core, Bitmap host, MTSDF host, and MTSDF Wasm baselines and caps each accepted raw, minified, gzip, and Brotli delta. - **Portable raster fixture identity** — Removed the Bitmap fixture's host-specific optimized-Wasm byte count from its portable artifact identity. The integration gate still executes the rebuilt baker and requires every canonical artifact, record, page, and report byte to match; compiled-module hash and size remain host-labeled package-size evidence under reviewed foreign-host ceilings. diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 4ddfc163..75a3017c 100644 --- a/docs/packages/benchmarks.md +++ b/docs/packages/benchmarks.md @@ -181,7 +181,7 @@ The package-size lane measures the item 8.1 MTSDF kernel separately from the cov Inter and Amiri retain their established roles. A pinned static Noto Sans Devanagari face adds the Indic lane without weakening the baker's explicit variable-font rejection. Advanced Shaping recommends a script-appropriate font for each case but exposes every baked fixture so a human can inspect coverage failures instead of having the selection silently locked. The CJK default is a reproducible HarfBuzz 13 subset of the authored Noto Sans CJK JP case; DotGothic16 remains available and explicitly labeled as pixel style. The subset is showcase evidence, not an answer to complete CJK distribution: the full 65,535-glyph Noto face remains the authoritative shaping/paragraph oracle and Milestone 13 owns chunked raster paging. -The Japanese showcase freshness check is self-contained on a declared build host: it first provisions both pinned HarfBuzz 13.0.0 utilities through the source-archive hash and executable-version gate, then rebuilds the subset in temporary storage and compares the font, license, and manifest exactly. Upstream creates `hb-shape` and `hb-subset` only when GLib development metadata is available, so the provisioner requires `-Dglib=enabled` and fails during Meson configuration when that prerequisite is absent. CI installs `libglib2.0-dev` explicitly and reports the resolved `glib-2.0` version; local hosts must expose the equivalent package through `pkg-config`. The provisioner disables unrelated optional HarfBuzz backends explicitly, keeping the source-build graph independent of other libraries installed on the host. GLib supplies the command-line frontend rather than the shaping or subset implementation, and the exact HarfBuzz version plus generated bytes remain the fixture authorities. An ignored developer cache may accelerate the check but is never an undeclared prerequisite. +The Japanese showcase freshness check is self-contained on a declared build host: it first provisions both pinned HarfBuzz 13.0.0 utilities through the source-archive hash and executable-version gate, then rebuilds the subset in temporary storage and compares the font, license, and manifest exactly. Upstream creates `hb-shape` and `hb-subset` only when GLib development metadata is available, so the provisioner requires `-Dglib=enabled` and fails during Meson configuration when that prerequisite is absent. CI installs `libglib2.0-dev` explicitly and reports the resolved `glib-2.0` version; local macOS hosts must install both Homebrew `glib` and `pkgconf` so the equivalent metadata is discoverable through `pkg-config`. The provisioner disables unrelated optional HarfBuzz backends explicitly, keeping the source-build graph independent of other libraries installed on the host. GLib supplies the command-line frontend rather than the shaping or subset implementation, and the exact HarfBuzz version plus generated bytes remain the fixture authorities. An ignored developer cache may accelerate the check but is never an undeclared prerequisite. The browser product also carries the React 19 subpath proofs. A shared registry target mounts public nested `` through a real React Three Fiber root backed by `WebGPURenderer`, retains one forwarded core object through width reflow and canonical restoration, matches pinned natural/narrow paragraph oracles, verifies two span paints in one draw, and submits a real renderer frame over three deterministic samples. The live pending-resource probe intercepts the exact composed Inter request behind a manually released promise, observes the Suspense fallback before publication, releases the request without a timer, then proves the registered font key and all 2,937 glyphs before deterministic cleanup. The test renderer remains confined to package integration evidence and does not enter the product registry or application dependencies. diff --git a/mise.toml b/mise.toml index d3c66ca7..2e7b0e23 100644 --- a/mise.toml +++ b/mise.toml @@ -2,5 +2,6 @@ idiomatic_version_file_enable_tools = ["node", "pnpm", "rust"] [tools] +pipx = "1.16.5" "pipx:meson" = "1.11.1" "aqua:ninja-build/ninja" = "1.13.2" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4ff26bf9..90ba27e7 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] channel = "1.97.1" profile = "minimal" -components = ["clippy", "rustfmt"] +components = ["cargo", "clippy", "rustfmt"] targets = ["wasm32-unknown-unknown"] From af1c389c9938f1f13a96c4a526b43f23fbdc52b3 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 05:15:39 -0400 Subject: [PATCH 02/13] refactor(benchmarks): remove unused React surfaces --- apps/benchmarks/doctor.config.json | 16 ++ .../benchmarks/src/benchmark/runtime-world.ts | 5 - .../src/components/interactive-canvas.tsx | 147 ------------------ docs/log.md | 1 + docs/packages/benchmarks.md | 4 +- 5 files changed, 19 insertions(+), 154 deletions(-) create mode 100644 apps/benchmarks/doctor.config.json delete mode 100644 apps/benchmarks/src/components/interactive-canvas.tsx diff --git a/apps/benchmarks/doctor.config.json b/apps/benchmarks/doctor.config.json new file mode 100644 index 00000000..f0e215c4 --- /dev/null +++ b/apps/benchmarks/doctor.config.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://react.doctor/schema/config.json", + "noScore": true, + "supplyChain": { + "enabled": false + }, + "ignore": { + "files": ["vitexec/**", "src/benchmark/bake-host-baseline.ts", "src/benchmark/worker-queue-evidence.ts"], + "overrides": [ + { + "files": ["src/benchmark/paragraph-layout-digest.ts"], + "rules": ["deslop/unused-export"] + } + ] + } +} diff --git a/apps/benchmarks/src/benchmark/runtime-world.ts b/apps/benchmarks/src/benchmark/runtime-world.ts index 8afb3c7b..2c017196 100644 --- a/apps/benchmarks/src/benchmark/runtime-world.ts +++ b/apps/benchmarks/src/benchmark/runtime-world.ts @@ -150,8 +150,3 @@ export function useRuntimeTelemetry() { const world = useRuntimeWorld(); return useRequiredWorldTrait(world, RuntimeTelemetry); } - -export function useRuntimeCanvasSettings() { - const world = useRuntimeWorld(); - return useRequiredWorldTrait(world, RuntimeCanvasSettings); -} diff --git a/apps/benchmarks/src/components/interactive-canvas.tsx b/apps/benchmarks/src/components/interactive-canvas.tsx deleted file mode 100644 index ce2fb7dc..00000000 --- a/apps/benchmarks/src/components/interactive-canvas.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useEffect, useEffectEvent, useRef, type PointerEvent as ReactPointerEvent, type RefObject } from 'react'; -import type { CanvasViewController } from '../renderer/canvas-view-controller'; - -export type { CanvasViewController } from '../renderer/canvas-view-controller'; - -interface PointerPosition { - readonly x: number; - readonly y: number; -} - -interface GestureSnapshot { - readonly centerX: number; - readonly centerY: number; - readonly distance?: number; -} - -export function InteractiveCanvas({ - label, - canvasRef, - className, - controllerRef, - pan: panEnabled = true, - zoom = false, -}: { - readonly label: string; - readonly canvasRef: RefObject; - readonly className?: string; - readonly controllerRef: RefObject; - readonly pan?: boolean; - readonly zoom?: boolean; -}) { - const pointers = useRef(new Map()); - const gesture = useRef(undefined); - const panX = useRef(0); - const panY = useRef(0); - const zoomScale = useRef(1); - - function publishView(canvas: HTMLCanvasElement): void { - canvas.dataset.panX = String(panX.current); - canvas.dataset.panY = String(panY.current); - canvas.dataset.zoom = String(zoomScale.current); - } - - function pan(canvas: HTMLCanvasElement, deltaX: number, deltaY: number): void { - const applied = controllerRef.current?.panBy(deltaX, deltaY); - panX.current += applied?.deltaX ?? deltaX; - panY.current += applied?.deltaY ?? deltaY; - publishView(canvas); - } - - function applyZoom(canvas: HTMLCanvasElement, factor: number): void { - zoomScale.current *= factor; - controllerRef.current?.zoomBy?.(factor); - publishView(canvas); - } - - const applyWheelZoom = useEffectEvent((canvas: HTMLCanvasElement, factor: number): void => { - applyZoom(canvas, factor); - }); - - function reset(canvas: HTMLCanvasElement): void { - panX.current = 0; - panY.current = 0; - zoomScale.current = 1; - controllerRef.current?.resetView(); - publishView(canvas); - } - - function updateGesture(): GestureSnapshot | undefined { - const positions = [...pointers.current.values()]; - if (positions.length === 0) return undefined; - if (positions.length === 1) { - const position = positions[0]!; - return { centerX: position.x, centerY: position.y }; - } - const first = positions[0]!; - const second = positions[1]!; - return { - centerX: (first.x + second.x) / 2, - centerY: (first.y + second.y) / 2, - distance: Math.hypot(second.x - first.x, second.y - first.y), - }; - } - - function beginPointer(event: ReactPointerEvent): void { - if (!panEnabled && !zoom) return; - if (event.pointerType !== 'touch' && event.button !== 0) return; - if (event.isTrusted) event.currentTarget.setPointerCapture(event.pointerId); - pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY }); - gesture.current = updateGesture(); - } - - function movePointer(event: ReactPointerEvent): void { - if (!pointers.current.has(event.pointerId)) return; - pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY }); - const previous = gesture.current; - const next = updateGesture(); - gesture.current = next; - if (previous === undefined || next === undefined) return; - const touchMayPan = event.pointerType !== 'touch' || pointers.current.size >= 2; - if (panEnabled && touchMayPan) { - pan(event.currentTarget, next.centerX - previous.centerX, next.centerY - previous.centerY); - } - if (zoom && previous.distance !== undefined && next.distance !== undefined && previous.distance > 0) { - applyZoom(event.currentTarget, next.distance / previous.distance); - } - } - - function endPointer(event: ReactPointerEvent): void { - if (!pointers.current.delete(event.pointerId)) return; - gesture.current = updateGesture(); - if (event.currentTarget.hasPointerCapture(event.pointerId)) { - event.currentTarget.releasePointerCapture(event.pointerId); - } - } - - useEffect(() => { - const canvas = canvasRef.current; - if (canvas === null || !zoom) return; - const zoomWheel = (event: WheelEvent): void => { - event.preventDefault(); - applyWheelZoom(canvas, Math.exp(-event.deltaY * 0.0015)); - }; - canvas.addEventListener('wheel', zoomWheel, { passive: false }); - return () => canvas.removeEventListener('wheel', zoomWheel); - }, [canvasRef, controllerRef, zoom]); - - return ( - reset(event.currentTarget)} - onPointerCancel={endPointer} - onPointerDown={beginPointer} - onPointerMove={movePointer} - onPointerUp={endPointer} - /> - ); -} diff --git a/docs/log.md b/docs/log.md index 24db4968..2837b20b 100644 --- a/docs/log.md +++ b/docs/log.md @@ -8,6 +8,7 @@ ## 2026-07-31 +- **React Doctor closure** — Removed the unused interactive-canvas component and runtime canvas-settings hook. The benchmark-local full-scan configuration excludes only Vitexec entrypoints, two URL-loaded evidence modules, and one dynamically imported digest export that static reachability cannot observe. React Doctor completes with zero errors, warnings, affected files, or diagnostics while retaining all live application rules. - **Fresh-checkout HarfBuzz bootstrap** — Added an exact root pipx pin before the existing pipx-backed Meson pin, so `mise install` no longer assumes an ambient pipx executable. The minimal Rust toolchain declares Cargo explicitly instead of relying on an implicit profile component that was absent from a fresh Linux mise cache. The macOS prerequisite now names both Homebrew `glib` and `pkgconf`, making the required `glib-2.0` metadata discoverable when the authenticated HarfBuzz fixture workflow configures its pinned utilities. - **Milestone 8.6 regeneration** — Refreshed the fail-closed autoresearch provenance after the coverage and SIMD work changed the canonical package-size report bytes. The baseline now authenticates the current report digest; the package-size lane independently rebuilds and checks the measured entries and reviewed ceilings. - **Host-scoped MTSDF admission evidence** — Labeled the compiled admission-module measurement by platform and architecture. The recorded host retains exact evidence freshness; foreign hosts must rebuild, reproduce the portable contract and synthetic output, retain zero imports, emit a complete SHA-256 identity, and remain under reviewed raw/optimized/gzip/Brotli ceilings. Focused contract tests reject stale portable fields, incomplete hashes, and budget overflow. diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 75a3017c..06a09200 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/text-benchmarks' documentation_type: reference -source_digest: 'sha256:a0aef1dee6ee55ac6d208d8ebccdbe526558da17a101d450bcb5d2233493c743' +source_digest: 'sha256:40a640a9b1804578c1b8db16a97eb1b1b98c7f61e1ee33afccc705e67f882fc1' tags: [package, benchmarks, react, vite, product-e2e] sources: - id: manifest @@ -77,7 +77,7 @@ The MSDF / Slug comparison workload owns one renderer, two equal RGBA8 render ta Font delivery is an explicit benchmark axis. **Baked asset** exercises the normal sibling asset, while **Runtime bake** passes `{ source, baked: null }`, downloads the source font, builds the core font in the serial core-baker Worker, then builds the selected Bitmap or MSDF raster in its serial lazy Worker. The inspector distinguishes the always-loaded runtime/shaper graph from the conditional core and raster baker host, Worker, and Wasm graphs; it reports source download bytes, generated core/raster CPU bytes, bake durations, and atlas GPU memory. The runtime-fallback conformance workload renders both delivery paths through the same public pipeline and requires an exact RGBA frame match. Canonical Inter matched with zero differing bytes for Bitmap and MSDF on the admitted WebGPU product probe; the observed cold MSDF raster bake was roughly 114 seconds on this host and remains an observation, not a portability threshold. -Maintainer workflows are package-owned and exposed from the workspace root. `pnpm benchmarks` starts the application, `pnpm benchmarks:check` runs its deterministic local verification, `pnpm benchmarks:test:live` runs the complete hardware-GPU product lane, `pnpm benchmarks:test:raster-technique-compare` runs the focused WebGPU/WebGL comparison and finite-job lifecycle lane, and the `pnpm benchmarks:profile:*` commands reproduce Paragraph Stress width, Bitmap rendered-size, Icon Grid allocation/frame cadence, and the complete Presentation workload sweep. The Presentation sweep waits for workload-specific committed telemetry, seeks Advanced Shaping to its complete authored specimen, rejects missing glyphs, and records RAF p95/max/slow-frame counts beside renderer CPU/GPU telemetry for all three techniques. `pnpm benchmarks:profile:icon-grid:gc` retains the full virtualized catalog traversal while recording frame intervals, heap range, pool-recycle count, and a browser performance trace. New repeatable benchmark workflows belong behind a package script and short root alias rather than a temporary probe or undocumented shell recipe.[^paragraph-layout-profile][^presentation-framerate-sweep] +Maintainer workflows are package-owned and exposed from the workspace root. `pnpm benchmarks` starts the application, `pnpm benchmarks:check` runs its deterministic local verification, `pnpm benchmarks:test:live` runs the complete hardware-GPU product lane, `pnpm benchmarks:test:raster-technique-compare` runs the focused WebGPU/WebGL comparison and finite-job lifecycle lane, and the `pnpm benchmarks:profile:*` commands reproduce Paragraph Stress width, Bitmap rendered-size, Icon Grid allocation/frame cadence, and the complete Presentation workload sweep. The Presentation sweep waits for workload-specific committed telemetry, seeks Advanced Shaping to its complete authored specimen, rejects missing glyphs, and records RAF p95/max/slow-frame counts beside renderer CPU/GPU telemetry for all three techniques. `pnpm benchmarks:profile:icon-grid:gc` retains the full virtualized catalog traversal while recording frame intervals, heap range, pool-recycle count, and a browser performance trace. The benchmark-local React Doctor configuration keeps full live-source linting while excluding explicit Vitexec and URL-loaded entrypoints that its static reachability pass cannot discover; the accepted full scan has zero diagnostics. New repeatable benchmark workflows belong behind a package script and short root alias rather than a temporary probe or undocumented shell recipe.[^paragraph-layout-profile][^presentation-framerate-sweep] `pnpm benchmarks:test:presentation-demo` exercises the complete 60-second timed sequence through a focused control. Off-axis / 3D and Icon Grid each receive two seconds before Paint & Effects begins at second four; the more visual Zoom Text and returning Icon Grid scenes receive longer holds than Dynamic Layout. Advanced Shaping resets to CJK and reveals one complete five-case cycle at 180 grapheme units per second. Playing case transitions begin the next script at its first grapheme; a font-changing handoff deliberately blanks the live line until that generation commits instead of showing mismatched old-script state. Zoom Text continues its normal word cycle and cuts after three complete default-speed drops; a cancelled slot preparation clears its pending marker and retries during the same cycle instead of holding a word for another cycle. Text Ladder receives the derived 7.2 seconds required for its default-speed vertical travel and 1024 px marquee to pass completely through the left edge before the nine-second Icon Grid return. A final 8.016-second Off-axis / 3D scene supplies the closing frame. The probe requires window-capture Space handling, exact workload defaults after preload, advancing telemetry, a retained canvas, exactly one renderer, both Icon Grid entries, and the final Off-axis / 3D scene. From 86f092ce0457ce0992eae9b3caf4554ad624380e Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 05:36:21 -0400 Subject: [PATCH 03/13] docs: close Milestone 8 regeneration sweep --- docs/log.md | 2 +- docs/roadmap/roadmap.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/log.md b/docs/log.md index 2837b20b..5473cfcb 100644 --- a/docs/log.md +++ b/docs/log.md @@ -10,7 +10,7 @@ - **React Doctor closure** — Removed the unused interactive-canvas component and runtime canvas-settings hook. The benchmark-local full-scan configuration excludes only Vitexec entrypoints, two URL-loaded evidence modules, and one dynamically imported digest export that static reachability cannot observe. React Doctor completes with zero errors, warnings, affected files, or diagnostics while retaining all live application rules. - **Fresh-checkout HarfBuzz bootstrap** — Added an exact root pipx pin before the existing pipx-backed Meson pin, so `mise install` no longer assumes an ambient pipx executable. The minimal Rust toolchain declares Cargo explicitly instead of relying on an implicit profile component that was absent from a fresh Linux mise cache. The macOS prerequisite now names both Homebrew `glib` and `pkgconf`, making the required `glib-2.0` metadata discoverable when the authenticated HarfBuzz fixture workflow configures its pinned utilities. -- **Milestone 8.6 regeneration** — Refreshed the fail-closed autoresearch provenance after the coverage and SIMD work changed the canonical package-size report bytes. The baseline now authenticates the current report digest; the package-size lane independently rebuilds and checks the measured entries and reviewed ceilings. +- **Milestone 8.6 regeneration** — Regenerated the affected ABIs, optimized Wasm modules, baked fixtures, identities, size records, autoresearch provenance, and package digests. The complete Rust, TypeScript, Node/Worker, artifact, renderer, conformance, live-product, and packed-consumer sweep passes locally; the four independently valid stack layers pass the clean Linux CI gate in 19m16s, 18m29s, 18m32s, and 19m13s. - **Host-scoped MTSDF admission evidence** — Labeled the compiled admission-module measurement by platform and architecture. The recorded host retains exact evidence freshness; foreign hosts must rebuild, reproduce the portable contract and synthetic output, retain zero imports, emit a complete SHA-256 identity, and remain under reviewed raw/optimized/gzip/Brotli ceilings. Focused contract tests reject stale portable fields, incomplete hashes, and budget overflow. - **Coverage-era size gates** — Reconciled the package-size record and its fail-closed autoresearch provenance with bounded raster coverage. Narrow current-capability ceilings pair with an independent regression that preserves the pre-coverage browser core, Bitmap host, MTSDF host, and MTSDF Wasm baselines and caps each accepted raw, minified, gzip, and Brotli delta. - **Portable raster fixture identity** — Removed the Bitmap fixture's host-specific optimized-Wasm byte count from its portable artifact identity. The integration gate still executes the rebuilt baker and requires every canonical artifact, record, page, and report byte to match; compiled-module hash and size remain host-labeled package-size evidence under reviewed foreign-host ceilings. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 7711850f..72891c4f 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -639,7 +639,7 @@ Runtime baking is a supported delivery path, not merely a missing-asset recovery - [x] Flow the normalized options through Node and the serial module Worker into Bitmap and MTSDF bakers without adding either baker to baked-hit or unselected-raster graphs. Equal normalized requests produce identical bytes; bounded and complete passes have exact progress totals, and active cancellation recovers queued work in a replacement Worker. - [x] Replace hand-maintained ABI size/offset mirrors with fixed-width `#[repr(C)]` layout types. Build-only Rust generators derive JSON sizes, alignments, and offsets with `size_of`, `align_of`, and `offset_of!`, then generate exact typed `as const` TypeScript modules consumed by production hosts. Tests require generated JSON/TypeScript identity and prove production Wasm carries no duplicate ABI contract exports. - [x] Keep Wasm direct-memory values little-endian because WebAssembly linear memory is normatively little-endian, while retaining explicit format-mandated byte order in GLB, KTX2, SFNT, and other portable serialized artifacts. Sparse raster coverage uses the same explicit little-endian bit numbering and zero terminal padding. -- [ ] Regenerate every affected ABI JSON file, optimized Wasm resource, baked fixture, identity, size record, and package digest; run the complete Rust, TypeScript, Node/Worker parity, artifact-validation, renderer, conformance, and live-product regression sweep before accepting the new boundary. +- [x] Regenerate every affected ABI JSON file, optimized Wasm resource, baked fixture, identity, size record, and package digest; run the complete Rust, TypeScript, Node/Worker parity, artifact-validation, renderer, conformance, and live-product regression sweep before accepting the new boundary. - [x] Instrument the baker by phase and publish small, medium, and complete-face results for glyph selection, outline extraction, MTSDF texel generation, packing, texture-payload encoding, container serialization, Wasm-to-Worker copying, peak memory, and output bytes. Reports include glyphs, generated texels, edges visited, and throughput rather than one opaque wall-clock duration; direct Wasm and the real serial Worker retain exact artifact identity. - [x] Optimize the measured dominant phase without weakening native-msdfgen quality or deterministic artifact gates. Texel generation dominates, so an equivalent four-texel scalar tile and an adjacent-texel SIMD line-distance kernel were compared against the unchanged scalar quadratic/cubic lane fallback. Every exact oracle and complete-Inter identity remains unchanged. Adjacent SIMD improves the bounded Node and Chromium corpora by 2.4% and 0.9%, but is indistinguishable from scalar over complete Inter warm execution while adding 20.7% optimized and 11.4% Brotli bytes. Scalar tile improves bounded Node by 10.1% but regresses Chromium by 1.5% and complete Inter warm by 1.4% while adding 20.0% optimized and 10.9% Brotli bytes. Machine-checked structured observations retain those tradeoffs. Both candidates are rejected as universal runtime defaults, remain non-shipping experiment features, and scalar Wasm remains the single shipped kernel. TypeGPU/WebGPU compute remains research until identical-work evidence can justify its device and readback complexity. - [x] Select pinned dynamic Talc from the complete optimized Wasm corpus: byte-identical behavior retains the existing ownership/error/reused-Worker tests while saving 46,610 raw, 15,121 gzip, and 12,121 Brotli bytes versus `dlmalloc`. Reject a 128 MiB global arena because it raises initial memory to about 129 MiB for no meaningful transfer saving; keep request-local scratch arenas as profiling-led future work only. From e5d759d831fd3159df171bb1ce90bf1217c5e473 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 09:41:41 -0400 Subject: [PATCH 04/13] fix(text): preserve pending no-op generations --- docs/log.md | 1 + docs/packages/text.md | 6 +-- packages/text/src/internal/text-properties.ts | 4 ++ packages/text/src/text.ts | 2 + .../tests/integration/text-object.test.mjs | 49 +++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/log.md b/docs/log.md index 5473cfcb..32f16dcc 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,7 @@ ## 2026-08-01 +- **Pending Text no-op lifecycle** — Semantic no-op `Text.setProperties` calls now preserve the active cold generation, abort signal, and readiness promise. A deterministic delayed-decode regression proves repeated no-ops neither restart nor cancel initial raster preparation. - **Thin baker diagnostics boundary** — Moved direct Wasm timing and memory observation behind a private diagnostic-only TypeScript entry while retaining the Rust phase observer behind its non-default `profiling` feature. The production package-size build now rejects diagnostic module or symbol reachability, clock calls in thin baker hosts, and profiling/timing Wasm imports or exports; packed consumers receive no diagnostic module, and the package concept records exact thin-build, diagnostic-run, and evidence-refresh commands. - **MTSDF phase attribution** — Added a package-owned small, medium, complete, and combined profiler over the shared optimized native, direct-Wasm, and serial-Worker bake paths. Authenticated measurements isolate texel generation as the dominant phase while separately recording selection, outlines, packing, KTX2, GLB, transfer, and memory high-water evidence. The profiling-only TypeScript and Rust entry points stay outside production execution; the rebased Darwin arm64 production MTSDF baker measures 552,025 raw / 215,027 gzip / 169,041 Brotli bytes. - **Bounded-coverage size remediation** — Removed the second derived Serde serialization graph from Bitmap and MTSDF coverage-capable bakers while preserving strict seed validation and byte-identical canonical descriptors. The measured Darwin arm64 Wasm payloads now occupy 626,940 raw / 234,735 gzip / 180,503 Brotli bytes for Bitmap and 553,190 raw / 215,142 gzip / 169,365 Brotli bytes for MTSDF. Dedicated pre-coverage growth gates bound the remaining strict decoder, cmap-resolution, and canonical-policy cost. Bitmap runtime decode now also derives the canonical policy key from authenticated strikes and coverage before creating GPU resources, matching the existing MTSDF boundary. diff --git a/docs/packages/text.md b/docs/packages/text.md index d8b6dfba..523e10d3 100644 --- a/docs/packages/text.md +++ b/docs/packages/text.md @@ -5,7 +5,7 @@ description: Implements public font loading, shaping, paragraph measurement, sta resource: ../../packages/text workspace_package: '@pmndrs/text' documentation_type: reference -source_digest: 'sha256:0940cc85ec4a7497cd3e5c268e7b87cf14ba3dc92303d0074dfcc930d5f90f6c' +source_digest: 'sha256:5571126cbc4a0947c93b75048528e3e3f5e4d2ffc27d147c9fc83319c896c96a' tags: [package, public-api, typescript, contracts] sources: - id: manifest @@ -142,7 +142,7 @@ sources: title: Unicode analysis implementation generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T06:43:11Z' + at: '2026-08-01T13:39:26Z' --- # Package reference: `@pmndrs/text` @@ -201,7 +201,7 @@ One instanced batch family handles fill, outline, opacity, and translated hard s The checked SIMD comparison builds scalar, compiler-auto-vectorized, and explicit-four-lane kernels from isolated target directories. Every variant preserves all seven corrected native-oracle hashes and the complete Inter result of 2,915 generated glyphs, 22 non-rendering rejected slots, checksum `a5a6aa6e`, and composite SHA-256 `f6381c2f…eef6`; an instrumented warm seven-call corpus records seven request allocations, zero reallocations, and seven deallocations, one owned output copy occurs per call, and Wasm memory does not grow after the cold corpus. On Node 24, scalar measured 46.462 milliseconds for seven warm calls versus 47.079 milliseconds for explicit SIMD. Chromium 149 measured 47.6 versus 48.1 milliseconds. Explicit SIMD improves the complete Inter warm pass from 48.13 to 45.38 seconds, a 5.7% stress/offline win, and saves 297 Brotli bytes. Because the supported runtime default is bounded interactive baking and the target feature would require an alternate artifact, scalar remains the only shipped baker kernel; item 8.6 retains the explicit variant as evidence for phase-led optimization rather than exposing a toggle now. The repository-local Vitexec capture and full-font request emitter preserve the experiment as repeatable evidence rather than product complexity. -The framework-neutral `Text` object is now a real Three.js `Group` rather than a contract shim. It validates one complete candidate state before committing a patch, resolves every distinct root/span font through registry-scoped loader and HarfRust caches, shares decoded raster resources through `RasterRuntime`, and owns the resulting paragraph and raster batches as one generation. The first incomplete generation stays hidden; a later load keeps the prior complete generation visible until the replacement can swap atomically. Revision-scoped cancellation prevents stale work from publishing. Every committed replacement releases its superseded font-disposal subscription while preserving a shared paragraph when only constraints changed. Text edits synchronously revalidate span and feature ranges; a paragraph-wide feature over empty text normalizes to a no-op. Paint-only updates reuse the positioned layout and retain one glyph-to-span paint-index plan while the text and normalized shaping ranges are unchanged. Validation and batch upload receive the same resolved `GlyphPaint` value, and repeated same-range updates reuse its `Uint16Array` index storage instead of rebuilding a code-unit map, palette-key map, and glyph-index array. One reusable Three color converter removes transient `Color` objects without weakening public color validation. Semantic no-op paint updates skip instance uploads, width updates reuse paragraph shaping, shaping changes replace the paragraph, and disposal releases every owned batch and paragraph. MTSDF batches retain outline-width and shadow-offset structure per instance: color-only updates write only paint attributes, while structural paint changes take the full geometry/UV path. Direct scalar attribute writes avoid short temporary arrays on both paths. Runtime performance instrumentation does not ship in this package; the benchmark measures public `Text` scheduling and readiness externally. Integration evidence covers changing a span's color while retaining the exact committed layout, draw-batch object, and paint-index storage; MTSDF coverage separately proves color-only updates preserve origin/size/UV structure and structural changes recompute it. A raw span font inherits the root raster definition but resolves its own font-local resource, preventing cross-font atlas reuse. +The framework-neutral `Text` object is now a real Three.js `Group` rather than a contract shim. It validates one complete candidate state before committing a patch, resolves every distinct root/span font through registry-scoped loader and HarfRust caches, shares decoded raster resources through `RasterRuntime`, and owns the resulting paragraph and raster batches as one generation. The first incomplete generation stays hidden; a later load keeps the prior complete generation visible until the replacement can swap atomically. Revision-scoped cancellation prevents stale work from publishing. Semantic no-op updates preserve an in-flight cold generation, its abort signal, and its original readiness observation instead of restarting raster decode. Every committed replacement releases its superseded font-disposal subscription while preserving a shared paragraph when only constraints changed. Text edits synchronously revalidate span and feature ranges; a paragraph-wide feature over empty text normalizes to a no-op. Paint-only updates reuse the positioned layout and retain one glyph-to-span paint-index plan while the text and normalized shaping ranges are unchanged. Validation and batch upload receive the same resolved `GlyphPaint` value, and repeated same-range updates reuse its `Uint16Array` index storage instead of rebuilding a code-unit map, palette-key map, and glyph-index array. One reusable Three color converter removes transient `Color` objects without weakening public color validation. Semantic no-op paint updates skip instance uploads, width updates reuse paragraph shaping, shaping changes replace the paragraph, and disposal releases every owned batch and paragraph. MTSDF batches retain outline-width and shadow-offset structure per instance: color-only updates write only paint attributes, while structural paint changes take the full geometry/UV path. Direct scalar attribute writes avoid short temporary arrays on both paths. Runtime performance instrumentation does not ship in this package; the benchmark measures public `Text` scheduling and readiness externally. Integration evidence covers changing a span's color while retaining the exact committed layout, draw-batch object, and paint-index storage; MTSDF coverage separately proves color-only updates preserve origin/size/UV structure and structural changes recompute it. A raw span font inherits the root raster definition but resolves its own font-local resource, preventing cross-font atlas reuse. The `@pmndrs/text/react` export now provides the thin runtime described by the accepted API. It flattens nested text nodes into one UTF-16 string plus ordered inherited spans, rejects nested object/layout props and non-text children, creates one core object only after React 19 dependencies resolve, forwards that object through its ref, and reconciles ordinary R3F transforms separately from core text properties. The forbidden source `text` and `spans` props remain explicit `never` fields because R3F v10's wider intrinsic-element types would otherwise weaken that public boundary. Semantic feature and inline-paint comparison prevents fresh-but-equal React values from scheduling layout or glyph-buffer work; a fresh `onLayout` callback updates ownership without repainting. `useFont`, `.preload`, `.clear`, and `lazyRaster` reuse the same loader, shaper, and raster dependencies as the core. A deterministic microtask-delayed disposal distinguishes React Strict Mode's setup/cleanup/setup cycle without sleeps or timer cushions. diff --git a/packages/text/src/internal/text-properties.ts b/packages/text/src/internal/text-properties.ts index 4bcbb100..9b843d42 100644 --- a/packages/text/src/internal/text-properties.ts +++ b/packages/text/src/internal/text-properties.ts @@ -435,6 +435,10 @@ export function samePaintInput(left: TextState, right: TextState): boolean { ); } +export function sameTextInput(left: TextState, right: TextState): boolean { + return sameLayoutInput(left, right) && samePaintInput(left, right) && left.onLayout === right.onLayout; +} + export function samePaintProperties(left: ComparablePaintProperties, right: ComparablePaintProperties): boolean { return ( sameColor(left.color, right.color) && diff --git a/packages/text/src/text.ts b/packages/text/src/text.ts index 08355279..f1c812f8 100644 --- a/packages/text/src/text.ts +++ b/packages/text/src/text.ts @@ -15,6 +15,7 @@ import { sameLayoutInput, samePaintInput, sameParagraphInput, + sameTextInput, type NormalizedRasterRequest, type TextState, } from './internal/text-properties.js'; @@ -205,6 +206,7 @@ export class Text extends THREE.Group { setProperties(properties: TextUpdateProperties): void { this.#assertActive(); const next = normalizeTextState(this.#state, properties, false); + if (sameTextInput(this.#state, next)) return; let prevalidatedPaint: GlyphPaint | undefined; if (this.#generation !== undefined && sameLayoutInput(this.#generation.state, next)) { prevalidatedPaint = resolveGlyphPaint(next, this.#generation.paintPlan); diff --git a/packages/text/tests/integration/text-object.test.mjs b/packages/text/tests/integration/text-object.test.mjs index 38d5f965..15e78196 100644 --- a/packages/text/tests/integration/text-object.test.mjs +++ b/packages/text/tests/integration/text-object.test.mjs @@ -85,6 +85,55 @@ test('Text commits layout and draw generations atomically', async () => { assert.throws(() => text.setProperties({ opacity: 1 }), /disposed/); }); +test('Text no-op updates preserve one pending initial generation', async () => { + const restoreFetch = installFileFetch(); + const registry = new FontRegistry(); + const font = await registry.registerAsset(await readFile(fixtureUrl)); + const request = bitmap({ strikes: [16] }); + const decodeStarted = Promise.withResolvers(); + const releaseDecode = Promise.withResolvers(); + let decodeCount = 0; + let decodeSignal; + const raster = defineRaster({ + ...request.module, + async decode(...arguments_) { + decodeCount += 1; + decodeSignal = arguments_[2]; + decodeStarted.resolve(); + await releaseDecode.promise; + decodeSignal?.throwIfAborted(); + return request.module.decode(...arguments_); + }, + }); + const text = new Text({ + text: 'one cold generation', + font, + raster: { module: raster, options: request.options }, + fontSize: 16, + opacity: 0.5, + }); + try { + const initialReady = text.ready; + await decodeStarted.promise; + text.setProperties({}); + text.setProperties({ opacity: 0.5 }); + + assert.equal(text.ready, initialReady, 'semantic no-ops retain the original readiness observation'); + assert.equal(decodeCount, 1, 'semantic no-ops do not restart raster decoding'); + assert.equal(decodeSignal?.aborted, false, 'semantic no-ops do not abort the pending generation'); + + releaseDecode.resolve(); + await initialReady; + assert.equal(text.children.length, 1); + assert.equal(decodeCount, 1); + } finally { + releaseDecode.resolve(); + text.dispose(); + font.dispose(); + restoreFetch(); + } +}); + test('bitmap glyph-position transitions preserve authoritative layouts and pixel-snap inputs', async () => { const restoreFetch = installFileFetch(); const registry = new FontRegistry(); From d64f8d2b34e0192b8e91908c08fd7217b300e65a Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 09:42:36 -0400 Subject: [PATCH 05/13] docs(text): qualify configured MTSDF outline limits --- docs/log.md | 1 + docs/packages/text.md | 6 +-- packages/text/src/internal/msdf-contract.ts | 2 +- .../tests/integration/mtsdf-baker.test.mjs | 47 +++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docs/log.md b/docs/log.md index 32f16dcc..5e19ceae 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,7 @@ ## 2026-08-01 +- **Configured MTSDF outline authority** — Qualified the public four-atlas-pixel outline constant as the 64/8 default and proved a real 32/4 artifact derives its two-pixel limit from authenticated runtime metadata. - **Pending Text no-op lifecycle** — Semantic no-op `Text.setProperties` calls now preserve the active cold generation, abort signal, and readiness promise. A deterministic delayed-decode regression proves repeated no-ops neither restart nor cancel initial raster preparation. - **Thin baker diagnostics boundary** — Moved direct Wasm timing and memory observation behind a private diagnostic-only TypeScript entry while retaining the Rust phase observer behind its non-default `profiling` feature. The production package-size build now rejects diagnostic module or symbol reachability, clock calls in thin baker hosts, and profiling/timing Wasm imports or exports; packed consumers receive no diagnostic module, and the package concept records exact thin-build, diagnostic-run, and evidence-refresh commands. - **MTSDF phase attribution** — Added a package-owned small, medium, complete, and combined profiler over the shared optimized native, direct-Wasm, and serial-Worker bake paths. Authenticated measurements isolate texel generation as the dominant phase while separately recording selection, outlines, packing, KTX2, GLB, transfer, and memory high-water evidence. The profiling-only TypeScript and Rust entry points stay outside production execution; the rebased Darwin arm64 production MTSDF baker measures 552,025 raw / 215,027 gzip / 169,041 Brotli bytes. diff --git a/docs/packages/text.md b/docs/packages/text.md index 523e10d3..9484816f 100644 --- a/docs/packages/text.md +++ b/docs/packages/text.md @@ -5,7 +5,7 @@ description: Implements public font loading, shaping, paragraph measurement, sta resource: ../../packages/text workspace_package: '@pmndrs/text' documentation_type: reference -source_digest: 'sha256:5571126cbc4a0947c93b75048528e3e3f5e4d2ffc27d147c9fc83319c896c96a' +source_digest: 'sha256:1350bb3315a46cfd6ddc24ced5bc53854a31581436eb6aefd5b10949c718a517' tags: [package, public-api, typescript, contracts] sources: - id: manifest @@ -142,7 +142,7 @@ sources: title: Unicode analysis implementation generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T13:39:26Z' + at: '2026-08-01T13:45:00Z' --- # Package reference: `@pmndrs/text` @@ -197,7 +197,7 @@ Item 8.3 promotes `@pmndrs/text/raster/msdf` from an identity-only contract to t The runtime repeats no parallel wire-format implementation. Bitmap and MTSDF renderers plus both standalone validators consume the same dependency-light KTX2 and dense-record rules; only the standalone layer imports Khronos/Ajv. The renderers also share the lossless-atlas adapter, unit quad, parallel-array checks, and resolved-paint lookup. The MTSDF resource uploads only its authenticated base levels into one padded texture array, samples them bilinearly, sizes reconstruction with screen derivatives, and owns one material per logical array; disposal releases materials and textures transactionally. -One instanced batch family handles fill, outline, opacity, and translated hard shadow. The version-matched TSL graph reconstructs the fill edge from the RGB median and consumes alpha's true signed distance for effects. Shadow offsets expand each instance's geometric bounds and shift the same authenticated atlas sample, while clamped sampling and an explicit in-glyph mask prevent neighboring atlas cells from bleeding into the result. V0 outlines are bounded to half of the resource's authenticated full `pixelRange`; a larger request fails instead of silently clipping. Paint updates reuse geometry and rewrite only owned instance attributes. The canonical Inter integration test decodes all ten real legacy-default pages, creates and repaints a live batch, verifies normalized effect attributes, and proves idempotent batch/resource cleanup without loading baker Wasm into the runtime graph. +One instanced batch family handles fill, outline, opacity, and translated hard shadow. The version-matched TSL graph reconstructs the fill edge from the RGB median and consumes alpha's true signed distance for effects. Shadow offsets expand each instance's geometric bounds and shift the same authenticated atlas sample, while clamped sampling and an explicit in-glyph mask prevent neighboring atlas cells from bleeding into the result. V0 outlines are bounded to half of the resource's authenticated full `pixelRange`; the exported `MTSDF_MAX_OUTLINE_ATLAS_PIXELS` is specifically the four-pixel limit for the default 64/8 configuration, while non-default resources derive their limit from their own authenticated range. A larger request fails instead of silently clipping. Paint updates reuse geometry and rewrite only owned instance attributes. The canonical Inter integration test decodes all ten real legacy-default pages, creates and repaints a live batch, verifies normalized effect attributes, and proves idempotent batch/resource cleanup without loading baker Wasm into the runtime graph. A real 32/4 artifact additionally proves its two-atlas-pixel boundary normalizes to half of that resource's field range. The checked SIMD comparison builds scalar, compiler-auto-vectorized, and explicit-four-lane kernels from isolated target directories. Every variant preserves all seven corrected native-oracle hashes and the complete Inter result of 2,915 generated glyphs, 22 non-rendering rejected slots, checksum `a5a6aa6e`, and composite SHA-256 `f6381c2f…eef6`; an instrumented warm seven-call corpus records seven request allocations, zero reallocations, and seven deallocations, one owned output copy occurs per call, and Wasm memory does not grow after the cold corpus. On Node 24, scalar measured 46.462 milliseconds for seven warm calls versus 47.079 milliseconds for explicit SIMD. Chromium 149 measured 47.6 versus 48.1 milliseconds. Explicit SIMD improves the complete Inter warm pass from 48.13 to 45.38 seconds, a 5.7% stress/offline win, and saves 297 Brotli bytes. Because the supported runtime default is bounded interactive baking and the target feature would require an alternate artifact, scalar remains the only shipped baker kernel; item 8.6 retains the explicit variant as evidence for phase-led optimization rather than exposing a toggle now. The repository-local Vitexec capture and full-font request emitter preserve the experiment as repeatable evidence rather than product complexity. diff --git a/packages/text/src/internal/msdf-contract.ts b/packages/text/src/internal/msdf-contract.ts index c36e7e7d..1a35fd3f 100644 --- a/packages/text/src/internal/msdf-contract.ts +++ b/packages/text/src/internal/msdf-contract.ts @@ -14,7 +14,7 @@ export const MTSDF_PLANE_UNITS_PER_EM = 64 as const; export const MTSDF_MAX_EM_SIZE = 1_022 as const; /** Largest full range that can leave at least one inner texel in a fixed 1024 page. */ export const MTSDF_MAX_PIXEL_RANGE = 1_020 as const; -/** The encoded true-distance field covers four atlas pixels on either side of the edge. */ +/** Default 64/8 MTSDF field limit; configured resources derive their limit as `pixelRange / 2`. */ export const MTSDF_MAX_OUTLINE_ATLAS_PIXELS: number = MTSDF_PIXEL_RANGE / 2; export interface MsdfOptions { diff --git a/packages/text/tests/integration/mtsdf-baker.test.mjs b/packages/text/tests/integration/mtsdf-baker.test.mjs index 0a46fd80..03607835 100644 --- a/packages/text/tests/integration/mtsdf-baker.test.mjs +++ b/packages/text/tests/integration/mtsdf-baker.test.mjs @@ -165,6 +165,53 @@ test('bakes and validates authenticated 32 px/em quality policies', async () => descriptor, }); assert.equal(validated.pages.length, result.report.pages.length); + if (pixelRange === 4) { + const { document, views } = glbViews(raster.bytes); + const font = { handle: 7, shapingHash: showcaseShapingHash, glyphCount: 155 }; + const runtimeRaster = { + font: font.handle, + handle: 11, + kind: 'msdf', + extension: MSDF_EXTENSION, + version: 0, + rasterKey, + extensionData: document.extensions[MSDF_EXTENSION], + view(index) { + const view = views[index]; + if (view === undefined) throw new RangeError('missing embedded 32 px/em MTSDF runtime view'); + return view; + }, + dispose() {}, + }; + const resource = await msdf.decode(font, runtimeRaster); + try { + assert.equal(resource.emSize, 32); + assert.equal(resource.pixelRange, 4); + const records = views[extension.recordBufferView]; + assert.ok(records); + const layout = { + glyphIds: Uint16Array.of(firstPresentGlyph(records)), + glyphFontSlots: Uint16Array.of(0), + glyphFontSizes: Float32Array.of(32), + x: Float32Array.of(0), + y: Float32Array.of(0), + }; + const paint = { + paintIndices: Uint16Array.of(0), + palette: [{ color: [1, 1, 1, 1], outline: { color: [0, 0, 0, 1], width: 2 } }], + }; + const batch = msdf.buildBatches(layout, resource, 0, paint); + try { + const mesh = batch.object.children[0]; + assert.ok(mesh); + assert.equal(mesh.geometry.getAttribute('msdfOutlineWidth').getX(0), 0.5); + } finally { + batch.dispose(); + } + } finally { + msdf.dispose(resource); + } + } reports.push(result.report); } assert.ok(reports[0].gpuBytes < reports[1].gpuBytes); From 2a2379bdbb35e5051d81ccf7be4cdd39df7bcb06 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 10:07:46 -0400 Subject: [PATCH 06/13] fix(text): retain callback-only pending generations --- docs/log.md | 2 +- docs/packages/text.md | 6 ++-- packages/text/src/internal/text-properties.ts | 2 +- packages/text/src/text.ts | 19 +++++++--- .../tests/integration/text-object.test.mjs | 36 +++++++++++++++++++ 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/docs/log.md b/docs/log.md index 5e19ceae..52f2d52f 100644 --- a/docs/log.md +++ b/docs/log.md @@ -3,7 +3,7 @@ ## 2026-08-01 - **Configured MTSDF outline authority** — Qualified the public four-atlas-pixel outline constant as the 64/8 default and proved a real 32/4 artifact derives its two-pixel limit from authenticated runtime metadata. -- **Pending Text no-op lifecycle** — Semantic no-op `Text.setProperties` calls now preserve the active cold generation, abort signal, and readiness promise. A deterministic delayed-decode regression proves repeated no-ops neither restart nor cancel initial raster preparation. +- **Pending Text no-op lifecycle** — Semantic no-op `Text.setProperties` calls now preserve the active cold generation, abort signal, and readiness promise, including callback-only updates that publish to the latest `onLayout`. Failed generations clear their pending ownership so the same semantic input can retry. Deterministic delayed-decode and failure regressions prove both paths without timers. - **Thin baker diagnostics boundary** — Moved direct Wasm timing and memory observation behind a private diagnostic-only TypeScript entry while retaining the Rust phase observer behind its non-default `profiling` feature. The production package-size build now rejects diagnostic module or symbol reachability, clock calls in thin baker hosts, and profiling/timing Wasm imports or exports; packed consumers receive no diagnostic module, and the package concept records exact thin-build, diagnostic-run, and evidence-refresh commands. - **MTSDF phase attribution** — Added a package-owned small, medium, complete, and combined profiler over the shared optimized native, direct-Wasm, and serial-Worker bake paths. Authenticated measurements isolate texel generation as the dominant phase while separately recording selection, outlines, packing, KTX2, GLB, transfer, and memory high-water evidence. The profiling-only TypeScript and Rust entry points stay outside production execution; the rebased Darwin arm64 production MTSDF baker measures 552,025 raw / 215,027 gzip / 169,041 Brotli bytes. - **Bounded-coverage size remediation** — Removed the second derived Serde serialization graph from Bitmap and MTSDF coverage-capable bakers while preserving strict seed validation and byte-identical canonical descriptors. The measured Darwin arm64 Wasm payloads now occupy 626,940 raw / 234,735 gzip / 180,503 Brotli bytes for Bitmap and 553,190 raw / 215,142 gzip / 169,365 Brotli bytes for MTSDF. Dedicated pre-coverage growth gates bound the remaining strict decoder, cmap-resolution, and canonical-policy cost. Bitmap runtime decode now also derives the canonical policy key from authenticated strikes and coverage before creating GPU resources, matching the existing MTSDF boundary. diff --git a/docs/packages/text.md b/docs/packages/text.md index 9484816f..dc36feef 100644 --- a/docs/packages/text.md +++ b/docs/packages/text.md @@ -5,7 +5,7 @@ description: Implements public font loading, shaping, paragraph measurement, sta resource: ../../packages/text workspace_package: '@pmndrs/text' documentation_type: reference -source_digest: 'sha256:1350bb3315a46cfd6ddc24ced5bc53854a31581436eb6aefd5b10949c718a517' +source_digest: 'sha256:989892d65803fe5e21d066e0d11b1eadc475b2817e7f8e6a861790c43d6c578c' tags: [package, public-api, typescript, contracts] sources: - id: manifest @@ -142,7 +142,7 @@ sources: title: Unicode analysis implementation generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T13:45:00Z' + at: '2026-08-01T14:06:37Z' --- # Package reference: `@pmndrs/text` @@ -201,7 +201,7 @@ One instanced batch family handles fill, outline, opacity, and translated hard s The checked SIMD comparison builds scalar, compiler-auto-vectorized, and explicit-four-lane kernels from isolated target directories. Every variant preserves all seven corrected native-oracle hashes and the complete Inter result of 2,915 generated glyphs, 22 non-rendering rejected slots, checksum `a5a6aa6e`, and composite SHA-256 `f6381c2f…eef6`; an instrumented warm seven-call corpus records seven request allocations, zero reallocations, and seven deallocations, one owned output copy occurs per call, and Wasm memory does not grow after the cold corpus. On Node 24, scalar measured 46.462 milliseconds for seven warm calls versus 47.079 milliseconds for explicit SIMD. Chromium 149 measured 47.6 versus 48.1 milliseconds. Explicit SIMD improves the complete Inter warm pass from 48.13 to 45.38 seconds, a 5.7% stress/offline win, and saves 297 Brotli bytes. Because the supported runtime default is bounded interactive baking and the target feature would require an alternate artifact, scalar remains the only shipped baker kernel; item 8.6 retains the explicit variant as evidence for phase-led optimization rather than exposing a toggle now. The repository-local Vitexec capture and full-font request emitter preserve the experiment as repeatable evidence rather than product complexity. -The framework-neutral `Text` object is now a real Three.js `Group` rather than a contract shim. It validates one complete candidate state before committing a patch, resolves every distinct root/span font through registry-scoped loader and HarfRust caches, shares decoded raster resources through `RasterRuntime`, and owns the resulting paragraph and raster batches as one generation. The first incomplete generation stays hidden; a later load keeps the prior complete generation visible until the replacement can swap atomically. Revision-scoped cancellation prevents stale work from publishing. Semantic no-op updates preserve an in-flight cold generation, its abort signal, and its original readiness observation instead of restarting raster decode. Every committed replacement releases its superseded font-disposal subscription while preserving a shared paragraph when only constraints changed. Text edits synchronously revalidate span and feature ranges; a paragraph-wide feature over empty text normalizes to a no-op. Paint-only updates reuse the positioned layout and retain one glyph-to-span paint-index plan while the text and normalized shaping ranges are unchanged. Validation and batch upload receive the same resolved `GlyphPaint` value, and repeated same-range updates reuse its `Uint16Array` index storage instead of rebuilding a code-unit map, palette-key map, and glyph-index array. One reusable Three color converter removes transient `Color` objects without weakening public color validation. Semantic no-op paint updates skip instance uploads, width updates reuse paragraph shaping, shaping changes replace the paragraph, and disposal releases every owned batch and paragraph. MTSDF batches retain outline-width and shadow-offset structure per instance: color-only updates write only paint attributes, while structural paint changes take the full geometry/UV path. Direct scalar attribute writes avoid short temporary arrays on both paths. Runtime performance instrumentation does not ship in this package; the benchmark measures public `Text` scheduling and readiness externally. Integration evidence covers changing a span's color while retaining the exact committed layout, draw-batch object, and paint-index storage; MTSDF coverage separately proves color-only updates preserve origin/size/UV structure and structural changes recompute it. A raw span font inherits the root raster definition but resolves its own font-local resource, preventing cross-font atlas reuse. +The framework-neutral `Text` object is now a real Three.js `Group` rather than a contract shim. It validates one complete candidate state before committing a patch, resolves every distinct root/span font through registry-scoped loader and HarfRust caches, shares decoded raster resources through `RasterRuntime`, and owns the resulting paragraph and raster batches as one generation. The first incomplete generation stays hidden; a later load keeps the prior complete generation visible until the replacement can swap atomically. Revision-scoped cancellation prevents stale work from publishing. Semantic no-op updates preserve an in-flight cold generation, its abort signal, and its original readiness observation instead of restarting raster decode. Callback-only updates also retain that work and the latest `onLayout` observes the committed layout; after a genuine generation failure, the same semantic input explicitly retries rather than becoming permanently inert. Every committed replacement releases its superseded font-disposal subscription while preserving a shared paragraph when only constraints changed. Text edits synchronously revalidate span and feature ranges; a paragraph-wide feature over empty text normalizes to a no-op. Paint-only updates reuse the positioned layout and retain one glyph-to-span paint-index plan while the text and normalized shaping ranges are unchanged. Validation and batch upload receive the same resolved `GlyphPaint` value, and repeated same-range updates reuse its `Uint16Array` index storage instead of rebuilding a code-unit map, palette-key map, and glyph-index array. One reusable Three color converter removes transient `Color` objects without weakening public color validation. Semantic no-op paint updates skip instance uploads, width updates reuse paragraph shaping, shaping changes replace the paragraph, and disposal releases every owned batch and paragraph. MTSDF batches retain outline-width and shadow-offset structure per instance: color-only updates write only paint attributes, while structural paint changes take the full geometry/UV path. Direct scalar attribute writes avoid short temporary arrays on both paths. Runtime performance instrumentation does not ship in this package; the benchmark measures public `Text` scheduling and readiness externally. Integration evidence covers changing a span's color while retaining the exact committed layout, draw-batch object, and paint-index storage; MTSDF coverage separately proves color-only updates preserve origin/size/UV structure and structural changes recompute it. A raw span font inherits the root raster definition but resolves its own font-local resource, preventing cross-font atlas reuse. The `@pmndrs/text/react` export now provides the thin runtime described by the accepted API. It flattens nested text nodes into one UTF-16 string plus ordered inherited spans, rejects nested object/layout props and non-text children, creates one core object only after React 19 dependencies resolve, forwards that object through its ref, and reconciles ordinary R3F transforms separately from core text properties. The forbidden source `text` and `spans` props remain explicit `never` fields because R3F v10's wider intrinsic-element types would otherwise weaken that public boundary. Semantic feature and inline-paint comparison prevents fresh-but-equal React values from scheduling layout or glyph-buffer work; a fresh `onLayout` callback updates ownership without repainting. `useFont`, `.preload`, `.clear`, and `lazyRaster` reuse the same loader, shaper, and raster dependencies as the core. A deterministic microtask-delayed disposal distinguishes React Strict Mode's setup/cleanup/setup cycle without sleeps or timer cushions. diff --git a/packages/text/src/internal/text-properties.ts b/packages/text/src/internal/text-properties.ts index 9b843d42..34f35e65 100644 --- a/packages/text/src/internal/text-properties.ts +++ b/packages/text/src/internal/text-properties.ts @@ -436,7 +436,7 @@ export function samePaintInput(left: TextState, right: TextState): boolean { } export function sameTextInput(left: TextState, right: TextState): boolean { - return sameLayoutInput(left, right) && samePaintInput(left, right) && left.onLayout === right.onLayout; + return sameLayoutInput(left, right) && samePaintInput(left, right); } export function samePaintProperties(left: ComparablePaintProperties, right: ComparablePaintProperties): boolean { diff --git a/packages/text/src/text.ts b/packages/text/src/text.ts index f1c812f8..8f4bbb7c 100644 --- a/packages/text/src/text.ts +++ b/packages/text/src/text.ts @@ -156,7 +156,7 @@ interface OwnedBatch { } interface TextGeneration { - readonly state: TextState; + state: TextState; readonly paragraph: Paragraph; /** True only when this uncommitted generation acquired the paragraph it carries. */ readonly createdParagraph: boolean; @@ -206,7 +206,15 @@ export class Text extends THREE.Group { setProperties(properties: TextUpdateProperties): void { this.#assertActive(); const next = normalizeTextState(this.#state, properties, false); - if (sameTextInput(this.#state, next)) return; + if (sameTextInput(this.#state, next)) { + this.#state = next; + if (this.#pending !== undefined) return; + if (this.#generation !== undefined && sameTextInput(this.#generation.state, next)) { + this.#generation.state = next; + return; + } + if (next.font === undefined) return; + } let prevalidatedPaint: GlyphPaint | undefined; if (this.#generation !== undefined && sameLayoutInput(this.#generation.state, next)) { prevalidatedPaint = resolveGlyphPaint(next, this.#generation.paintPlan); @@ -266,6 +274,7 @@ export class Text extends THREE.Group { const previous = this.#generation; for (const update of generation.batchUpdates) update.commit(); generation.batchUpdates.length = 0; + generation.state = this.#state; this.#generation = generation; previous?.releaseFontDisposal(); for (const owned of previous?.batches ?? []) { @@ -279,11 +288,13 @@ export class Text extends THREE.Group { for (const owned of generation.batches) { if (owned.batch.object.parent !== this) this.add(owned.batch.object); } - generation.state.onLayout?.(generation.layout); + this.#state.onLayout?.(generation.layout); }); // `ready` remains an observation channel that rejects on failure or cancellation. The // internal branch prevents an abandoned generation from becoming an unhandled rejection. - void ready.catch(() => undefined); + void ready.catch(() => { + if (this.#pending === controller) this.#pending = undefined; + }); this.#ready = ready; } diff --git a/packages/text/tests/integration/text-object.test.mjs b/packages/text/tests/integration/text-object.test.mjs index 15e78196..180fd478 100644 --- a/packages/text/tests/integration/text-object.test.mjs +++ b/packages/text/tests/integration/text-object.test.mjs @@ -117,6 +117,8 @@ test('Text no-op updates preserve one pending initial generation', async () => { await decodeStarted.promise; text.setProperties({}); text.setProperties({ opacity: 0.5 }); + let committedLayout; + text.setProperties({ onLayout: (layout) => (committedLayout = layout) }); assert.equal(text.ready, initialReady, 'semantic no-ops retain the original readiness observation'); assert.equal(decodeCount, 1, 'semantic no-ops do not restart raster decoding'); @@ -126,6 +128,7 @@ test('Text no-op updates preserve one pending initial generation', async () => { await initialReady; assert.equal(text.children.length, 1); assert.equal(decodeCount, 1); + assert.equal(committedLayout, text.layout, 'the latest callback observes the pending generation at commit'); } finally { releaseDecode.resolve(); text.dispose(); @@ -134,6 +137,39 @@ test('Text no-op updates preserve one pending initial generation', async () => { } }); +test('Text semantic no-ops retry a failed generation', async () => { + const restoreFetch = installFileFetch(); + const registry = new FontRegistry(); + const font = await registry.registerAsset(await readFile(fixtureUrl)); + const request = bitmap({ strikes: [16] }); + let decodeCount = 0; + const raster = defineRaster({ + ...request.module, + async decode(...arguments_) { + decodeCount += 1; + if (decodeCount === 1) throw new Error('synthetic decode failure'); + return request.module.decode(...arguments_); + }, + }); + const text = new Text({ + text: 'retry the same generation', + font, + raster: { module: raster, options: request.options }, + fontSize: 16, + }); + try { + await assert.rejects(text.ready, /synthetic decode failure/); + text.setProperties({}); + await text.ready; + assert.equal(decodeCount, 2); + assert.equal(text.children.length, 1); + } finally { + text.dispose(); + font.dispose(); + restoreFetch(); + } +}); + test('bitmap glyph-position transitions preserve authoritative layouts and pixel-snap inputs', async () => { const restoreFetch = installFileFetch(); const registry = new FontRegistry(); From b35decd26d90f23a3b62a55341715f6f55531de1 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 10:08:49 -0400 Subject: [PATCH 07/13] test(text): discriminate configured MTSDF outline limits --- docs/log.md | 2 +- docs/packages/text.md | 6 +++--- packages/text/tests/integration/mtsdf-baker.test.mjs | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/log.md b/docs/log.md index 52f2d52f..dddda7da 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,7 +2,7 @@ ## 2026-08-01 -- **Configured MTSDF outline authority** — Qualified the public four-atlas-pixel outline constant as the 64/8 default and proved a real 32/4 artifact derives its two-pixel limit from authenticated runtime metadata. +- **Configured MTSDF outline authority** — Qualified the public four-atlas-pixel outline constant as the 64/8 default and proved a real 32/4 artifact accepts exactly two atlas pixels but rejects `2.0001`, discriminating the authenticated runtime limit from the exported default. - **Pending Text no-op lifecycle** — Semantic no-op `Text.setProperties` calls now preserve the active cold generation, abort signal, and readiness promise, including callback-only updates that publish to the latest `onLayout`. Failed generations clear their pending ownership so the same semantic input can retry. Deterministic delayed-decode and failure regressions prove both paths without timers. - **Thin baker diagnostics boundary** — Moved direct Wasm timing and memory observation behind a private diagnostic-only TypeScript entry while retaining the Rust phase observer behind its non-default `profiling` feature. The production package-size build now rejects diagnostic module or symbol reachability, clock calls in thin baker hosts, and profiling/timing Wasm imports or exports; packed consumers receive no diagnostic module, and the package concept records exact thin-build, diagnostic-run, and evidence-refresh commands. - **MTSDF phase attribution** — Added a package-owned small, medium, complete, and combined profiler over the shared optimized native, direct-Wasm, and serial-Worker bake paths. Authenticated measurements isolate texel generation as the dominant phase while separately recording selection, outlines, packing, KTX2, GLB, transfer, and memory high-water evidence. The profiling-only TypeScript and Rust entry points stay outside production execution; the rebased Darwin arm64 production MTSDF baker measures 552,025 raw / 215,027 gzip / 169,041 Brotli bytes. diff --git a/docs/packages/text.md b/docs/packages/text.md index dc36feef..54c720b5 100644 --- a/docs/packages/text.md +++ b/docs/packages/text.md @@ -5,7 +5,7 @@ description: Implements public font loading, shaping, paragraph measurement, sta resource: ../../packages/text workspace_package: '@pmndrs/text' documentation_type: reference -source_digest: 'sha256:989892d65803fe5e21d066e0d11b1eadc475b2817e7f8e6a861790c43d6c578c' +source_digest: 'sha256:b8e9a74c222fdf8ecd3caee73f4ce0b3b5ee78a155677222a731847d6c9e7340' tags: [package, public-api, typescript, contracts] sources: - id: manifest @@ -142,7 +142,7 @@ sources: title: Unicode analysis implementation generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T14:06:37Z' + at: '2026-08-01T14:10:00Z' --- # Package reference: `@pmndrs/text` @@ -197,7 +197,7 @@ Item 8.3 promotes `@pmndrs/text/raster/msdf` from an identity-only contract to t The runtime repeats no parallel wire-format implementation. Bitmap and MTSDF renderers plus both standalone validators consume the same dependency-light KTX2 and dense-record rules; only the standalone layer imports Khronos/Ajv. The renderers also share the lossless-atlas adapter, unit quad, parallel-array checks, and resolved-paint lookup. The MTSDF resource uploads only its authenticated base levels into one padded texture array, samples them bilinearly, sizes reconstruction with screen derivatives, and owns one material per logical array; disposal releases materials and textures transactionally. -One instanced batch family handles fill, outline, opacity, and translated hard shadow. The version-matched TSL graph reconstructs the fill edge from the RGB median and consumes alpha's true signed distance for effects. Shadow offsets expand each instance's geometric bounds and shift the same authenticated atlas sample, while clamped sampling and an explicit in-glyph mask prevent neighboring atlas cells from bleeding into the result. V0 outlines are bounded to half of the resource's authenticated full `pixelRange`; the exported `MTSDF_MAX_OUTLINE_ATLAS_PIXELS` is specifically the four-pixel limit for the default 64/8 configuration, while non-default resources derive their limit from their own authenticated range. A larger request fails instead of silently clipping. Paint updates reuse geometry and rewrite only owned instance attributes. The canonical Inter integration test decodes all ten real legacy-default pages, creates and repaints a live batch, verifies normalized effect attributes, and proves idempotent batch/resource cleanup without loading baker Wasm into the runtime graph. A real 32/4 artifact additionally proves its two-atlas-pixel boundary normalizes to half of that resource's field range. +One instanced batch family handles fill, outline, opacity, and translated hard shadow. The version-matched TSL graph reconstructs the fill edge from the RGB median and consumes alpha's true signed distance for effects. Shadow offsets expand each instance's geometric bounds and shift the same authenticated atlas sample, while clamped sampling and an explicit in-glyph mask prevent neighboring atlas cells from bleeding into the result. V0 outlines are bounded to half of the resource's authenticated full `pixelRange`; the exported `MTSDF_MAX_OUTLINE_ATLAS_PIXELS` is specifically the four-pixel limit for the default 64/8 configuration, while non-default resources derive their limit from their own authenticated range. A larger request fails instead of silently clipping. Paint updates reuse geometry and rewrite only owned instance attributes. The canonical Inter integration test decodes all ten real legacy-default pages, creates and repaints a live batch, verifies normalized effect attributes, and proves idempotent batch/resource cleanup without loading baker Wasm into the runtime graph. A real 32/4 artifact accepts its exact two-atlas-pixel boundary, normalizes it to half of that resource's field range, and rejects `2.0001`, distinguishing the configured limit from the exported default. The checked SIMD comparison builds scalar, compiler-auto-vectorized, and explicit-four-lane kernels from isolated target directories. Every variant preserves all seven corrected native-oracle hashes and the complete Inter result of 2,915 generated glyphs, 22 non-rendering rejected slots, checksum `a5a6aa6e`, and composite SHA-256 `f6381c2f…eef6`; an instrumented warm seven-call corpus records seven request allocations, zero reallocations, and seven deallocations, one owned output copy occurs per call, and Wasm memory does not grow after the cold corpus. On Node 24, scalar measured 46.462 milliseconds for seven warm calls versus 47.079 milliseconds for explicit SIMD. Chromium 149 measured 47.6 versus 48.1 milliseconds. Explicit SIMD improves the complete Inter warm pass from 48.13 to 45.38 seconds, a 5.7% stress/offline win, and saves 297 Brotli bytes. Because the supported runtime default is bounded interactive baking and the target feature would require an alternate artifact, scalar remains the only shipped baker kernel; item 8.6 retains the explicit variant as evidence for phase-led optimization rather than exposing a toggle now. The repository-local Vitexec capture and full-font request emitter preserve the experiment as repeatable evidence rather than product complexity. diff --git a/packages/text/tests/integration/mtsdf-baker.test.mjs b/packages/text/tests/integration/mtsdf-baker.test.mjs index 03607835..cd0679f8 100644 --- a/packages/text/tests/integration/mtsdf-baker.test.mjs +++ b/packages/text/tests/integration/mtsdf-baker.test.mjs @@ -205,6 +205,14 @@ test('bakes and validates authenticated 32 px/em quality policies', async () => const mesh = batch.object.children[0]; assert.ok(mesh); assert.equal(mesh.geometry.getAttribute('msdfOutlineWidth').getX(0), 0.5); + assert.throws( + () => + batch.updatePaint({ + paintIndices: Uint16Array.of(0), + palette: [{ color: [1, 1, 1, 1], outline: { color: [0, 0, 0, 1], width: 2.0001 } }], + }), + /2-atlas-pixel field limit/, + ); } finally { batch.dispose(); } From 28bd751ad9a191e971daa9185e71642e134c0b97 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 10:09:47 -0400 Subject: [PATCH 08/13] fix(text): remove ambiguous coverage validation input --- docs/log.md | 1 + docs/packages/text.md | 6 +++--- packages/text/src/bakers/bitmap-validator.ts | 1 - packages/text/src/bakers/msdf-validator.ts | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/log.md b/docs/log.md index dddda7da..58cc5edd 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,7 @@ ## 2026-08-01 +- **Single coverage validation authority** — Removed unused Bitmap and MTSDF validation-context coverage fields; standalone validators now expose only the authenticated descriptor as expected coverage authority, eliminating a public input that could silently disagree. - **Configured MTSDF outline authority** — Qualified the public four-atlas-pixel outline constant as the 64/8 default and proved a real 32/4 artifact accepts exactly two atlas pixels but rejects `2.0001`, discriminating the authenticated runtime limit from the exported default. - **Pending Text no-op lifecycle** — Semantic no-op `Text.setProperties` calls now preserve the active cold generation, abort signal, and readiness promise, including callback-only updates that publish to the latest `onLayout`. Failed generations clear their pending ownership so the same semantic input can retry. Deterministic delayed-decode and failure regressions prove both paths without timers. - **Thin baker diagnostics boundary** — Moved direct Wasm timing and memory observation behind a private diagnostic-only TypeScript entry while retaining the Rust phase observer behind its non-default `profiling` feature. The production package-size build now rejects diagnostic module or symbol reachability, clock calls in thin baker hosts, and profiling/timing Wasm imports or exports; packed consumers receive no diagnostic module, and the package concept records exact thin-build, diagnostic-run, and evidence-refresh commands. diff --git a/docs/packages/text.md b/docs/packages/text.md index 54c720b5..681491e3 100644 --- a/docs/packages/text.md +++ b/docs/packages/text.md @@ -5,7 +5,7 @@ description: Implements public font loading, shaping, paragraph measurement, sta resource: ../../packages/text workspace_package: '@pmndrs/text' documentation_type: reference -source_digest: 'sha256:b8e9a74c222fdf8ecd3caee73f4ce0b3b5ee78a155677222a731847d6c9e7340' +source_digest: 'sha256:b56fa0c2a6926af437070aa11309897a33592af33d786d4d281eabed3ca10013' tags: [package, public-api, typescript, contracts] sources: - id: manifest @@ -142,7 +142,7 @@ sources: title: Unicode analysis implementation generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T14:10:00Z' + at: '2026-08-01T14:13:00Z' --- # Package reference: `@pmndrs/text` @@ -171,7 +171,7 @@ Milestone 8.1 adds a repository-owned `no_std + alloc` Rust MTSDF core and a non The geometry core is independent of its host boundary. A sibling `mtsdf-baker` crate owns the package allocator and seven-function generator C ABI for allocation, release, generation, and borrowed-result access. Build-only Rust generation derives the portable JSON and exact typed TypeScript contract; production Wasm does not embed or export that contract. Callers write one checked header plus fixed command records directly into Wasm memory; the module accepts only exact active pointer/length pairs and rejects a released allocation. The generator exposes a checked sampling transform for production baking: every glyph may be placed on one global plane grid with one authoritative distance range, rather than stretching each glyph independently to its rounded texture dimensions. The legacy one-em oracle path uses the same implementation and remains byte-identical. A feature-minimal admission build preserves the independently measured generator boundary, while the package publishes one full baker module containing that kernel and the artifact pipeline rather than duplicating it as a second Wasm resource. Its internal TypeScript host writes discriminated move/line/quadratic/cubic/close commands directly into linear memory, maps statuses to typed errors, verifies the exact RGBA8 length, copies borrowed output before release, and releases requests after every later failure. All seven native-oracle cases retain their independent SHA-256 identities through the host; malformed numeric/outline input, forged release ranges, stale allocations, ABI drift, and cleanup after invalid output are named regressions. The feature-minimal scalar boundary remains separately measured from its host. -Milestone 8.2 composed that kernel into the original fixed `@pmndrs/text/bakers/msdf` artifact path. One shared Fontations adapter supplies maintained unscaled line, quadratic, and cubic outlines to both admission evidence and the baker; no second parser or outline bridge exists. Its 64 px/em, full-eight-pixel-range descriptor hashes to `e944ba8d…fe93`. Item 8.6 now exposes `emSize` and full `pixelRange` as authenticated integer bake options in `1..=1022` and `1..=1020`. Omitted or partial options resolve against 64/8; explicit effective 64/8 canonicalizes to the legacy fieldless descriptor and raster key, while every non-default descriptor carries both effective values. `planeUnitsPerEm` equals `emSize`, and each glyph is evaluated only over its tight source-outline rectangle plus `ceil(pixelRange / 2)` field-padding texels on that global plane grid. Correction operates over the same glyph-local rectangle before copying into a 1024-pixel atlas page. Real 155-glyph subset bakes at 32/4 and 32/6 pass artifact validation, establishing the control path without changing the recommended default before quality and payload benchmarking. Bitmap and MTSDF descriptors may additionally authenticate bounded raster coverage while retaining the full source-local glyph namespace and dense record table. Degenerate non-rendering selected glyphs become exact absent records, while malformed command streams remain typed failures. The shared TypeScript direct-memory host owns allocation, response framing, nested metadata validation, copying, and transactional cleanup for both bitmap and MTSDF bakers. +Milestone 8.2 composed that kernel into the original fixed `@pmndrs/text/bakers/msdf` artifact path. One shared Fontations adapter supplies maintained unscaled line, quadratic, and cubic outlines to both admission evidence and the baker; no second parser or outline bridge exists. Its 64 px/em, full-eight-pixel-range descriptor hashes to `e944ba8d…fe93`. Item 8.6 now exposes `emSize` and full `pixelRange` as authenticated integer bake options in `1..=1022` and `1..=1020`. Omitted or partial options resolve against 64/8; explicit effective 64/8 canonicalizes to the legacy fieldless descriptor and raster key, while every non-default descriptor carries both effective values. `planeUnitsPerEm` equals `emSize`, and each glyph is evaluated only over its tight source-outline rectangle plus `ceil(pixelRange / 2)` field-padding texels on that global plane grid. Correction operates over the same glyph-local rectangle before copying into a 1024-pixel atlas page. Real 155-glyph subset bakes at 32/4 and 32/6 pass artifact validation, establishing the control path without changing the recommended default before quality and payload benchmarking. Bitmap and MTSDF descriptors may additionally authenticate bounded raster coverage while retaining the full source-local glyph namespace and dense record table. Standalone validation derives the expected coverage only from that authenticated descriptor; its public context has no second coverage field that could silently disagree. Degenerate non-rendering selected glyphs become exact absent records, while malformed command streams remain typed failures. The shared TypeScript direct-memory host owns allocation, response framing, nested metadata validation, copying, and transactional cleanup for both bitmap and MTSDF bakers. Direct raster-baker ABI V1 keeps ordinary responses contiguous and moves oversized results through bounded borrowed windows: the host reads metadata once, copies each window while Wasm owns it, and explicitly releases that ownership before the Worker transfers exact result buffers. Every Wasm pointer, status, length, and count is normalized as unsigned at the JavaScript boundary. The generator-only no-default-feature MTSDF module remains valid because artifact-baker fields are optional to the generator host, while the published baker requires and validates them. MTSDF quality options travel in the authenticated descriptor and do not change the low-level Wasm ABI. Build output removes obsolete ABI V0 files before packing. diff --git a/packages/text/src/bakers/bitmap-validator.ts b/packages/text/src/bakers/bitmap-validator.ts index b58e9944..af35ea6d 100644 --- a/packages/text/src/bakers/bitmap-validator.ts +++ b/packages/text/src/bakers/bitmap-validator.ts @@ -103,7 +103,6 @@ export interface BitmapArtifactValidationContext { readonly rasterKey: RasterKey | string; readonly shapingHash: Sha256Hex | string; readonly glyphCount: number; - readonly coverage?: Uint8Array; readonly glyphIdWidth: 16; readonly descriptor: BitmapDescriptorV0; readonly externalPages?: ReadonlyMap; diff --git a/packages/text/src/bakers/msdf-validator.ts b/packages/text/src/bakers/msdf-validator.ts index 5c524ed7..c373993b 100644 --- a/packages/text/src/bakers/msdf-validator.ts +++ b/packages/text/src/bakers/msdf-validator.ts @@ -78,7 +78,6 @@ export interface MtsdfArtifactValidationContext { readonly rasterKey: RasterKey | string; readonly shapingHash: Sha256Hex | string; readonly glyphCount: number; - readonly coverage?: Uint8Array; readonly glyphIdWidth: 16; readonly descriptor: MsdfDescriptorV0; readonly externalPages?: ReadonlyMap; From 85e9f3c7aaa3e4121719b6c55d4cf63839706afc Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 10:15:04 -0400 Subject: [PATCH 09/13] build: scope optional benchmark tools --- .github/workflows/ci.yml | 12 ++++++++-- .gitignore | 1 + AGENTS.md | 2 +- README.md | 14 ++++++++--- apps/benchmarks/mise.toml | 4 ++++ apps/benchmarks/package.json | 6 ++--- .../scripts/support/toolchain-scope.test.mts | 24 +++++++++++++++++++ docs/log.md | 1 + docs/packages/benchmarks.md | 6 ++--- docs/planning/version-contract.md | 8 +++---- mise.toml | 5 ---- 11 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 apps/benchmarks/mise.toml create mode 100644 apps/benchmarks/scripts/support/toolchain-scope.test.mts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf88092e..74040caf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,10 @@ jobs: version: 2026.7.13 cache: true - - name: Install HarfBuzz CLI prerequisite + - name: Install HarfBuzz workload toolchain + run: mise -C apps/benchmarks install + + - name: Install HarfBuzz native prerequisite run: | sudo apt-get update sudo apt-get install --yes --no-install-recommends libglib2.0-dev @@ -40,6 +43,9 @@ jobs: - name: Install locked dependencies run: pnpm install --frozen-lockfile + - name: Provision authenticated HarfBuzz utilities + run: mise exec -C apps/benchmarks -- pnpm provision:harfbuzz + - name: Select rolling runner Chromium shell: bash run: | @@ -58,7 +64,9 @@ jobs: printf 'PMNDRS_TEXT_CHROMIUM_EXECUTABLE_PATH=%s\n' "$chromium_executable" >> "$GITHUB_ENV" - name: Run deterministic repository checks - run: pnpm check + run: | + pnpm check + pnpm --filter @pmndrs/text-benchmarks check:japanese-showcase-subset - name: Retain Wasm failure evidence if: failure() diff --git a/.gitignore b/.gitignore index ac1ee1ea..594bb11d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store node_modules/ +.pnpm-store/ dist/ target/ *.tsbuildinfo diff --git a/AGENTS.md b/AGENTS.md index 1f64eb7b..5b6715b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ Use these canonical sources instead of creating shadow plans or duplicate status Update affected canonical documentation in the same change as source. Package source or configuration changes require reviewing the matching package concept, regenerating its `source_digest`, and running `pnpm docs:check`. -Use the exact root toolchain pins through mise. The dated nightly under `packages/font-baker/fuzz` is isolated to cargo-fuzz. Verify narrowly first, then run the relevant package and repository checks. Keep tests deterministic; do not use sleeps, timer cushions, arbitrary retries, or regenerated goldens as correctness mechanisms. +Use the exact root toolchain pins through mise. Agent commands must enter that environment explicitly with `mise exec -- pnpm ...` or `mise exec -- ...`; do not depend on `mise activate` surviving across non-interactive commands. Mise owns tool selection, while pnpm remains the only repository workflow surface. Install workload-scoped mise tools only when their documented pnpm workflow requires them. The dated nightly under `packages/font-baker/fuzz` is isolated to cargo-fuzz. Verify narrowly first, then run the relevant package and repository checks. Keep tests deterministic; do not use sleeps, timer cushions, arbitrary retries, or regenerated goldens as correctness mechanisms. Exercise repository workflows through named `pnpm` scripts from the workspace root. Prefer a short root alias for a maintainer-facing application workflow. When a repeatable build, test, profile, capture, generation, or development command is missing, add the package-owned script and root alias before running it; do not leave the working procedure as an agent-only shell recipe or temporary probe. diff --git a/README.md b/README.md index 5a336e20..1ac6f92b 100644 --- a/README.md +++ b/README.md @@ -177,9 +177,17 @@ pnpm install pnpm check ``` -Use mise to install the exact root Node.js, pnpm, stable Rust, pipx, Meson, and Ninja pins. The pinned pipx tool bootstraps the pinned Meson application; Meson and Ninja build the authenticated HarfBuzz oracle utilities used by the clean-checkout fixture gates. HarfBuzz gates those command-line utilities on GLib development metadata, so the build host must also provide `glib-2.0` through `pkg-config` (`libglib2.0-dev` on Ubuntu or `glib` plus `pkgconf` on macOS with Homebrew). CI declares that native prerequisite and prints the selected GLib version before checks run. The canonical mise versions are required; do not substitute merely compatible local toolchains. The optional coverage-guided font-baker fuzzer is isolated -under `packages/font-baker/fuzz`; its nested mise configuration provisions the exact dated nightly and -`cargo-fuzz` release required by that workspace when `fuzz:rust` runs. +The root path requires only the exact Node.js, pnpm, and stable Rust versions declared by the repository. Mise is the convenient reproducible installer, not a required task runner: contributors who already have matching versions may run the same pnpm commands directly. Non-interactive agents should use `mise exec -- pnpm ...` instead of relying on shell activation. The optional coverage-guided font-baker fuzzer is isolated under `packages/font-baker/fuzz`; its nested mise configuration provisions the exact dated nightly and `cargo-fuzz` release required by that workspace when `fuzz:rust` runs. + +The authenticated HarfBuzz fixture gate is a benchmark-specific workload, not a root prerequisite. It additionally needs Meson 1.11.1, Ninja 1.13.2, and `glib-2.0` development metadata through `pkg-config` (`libglib2.0-dev` on Ubuntu or `glib` plus `pkgconf` on macOS with Homebrew). Supply matching tools directly, or install the scoped pins and provision the utilities with: + +```sh +mise -C apps/benchmarks install +mise exec -C apps/benchmarks -- pnpm provision:harfbuzz +pnpm --filter @pmndrs/text-benchmarks check:japanese-showcase-subset +``` + +The ordinary `pnpm check` path validates committed fixtures without provisioning specialized native tooling. CI installs the scoped benchmark tools explicitly and runs the HarfBuzz freshness gate separately. Run the Figma-backed benchmark product from the monorepo app tree: diff --git a/apps/benchmarks/mise.toml b/apps/benchmarks/mise.toml new file mode 100644 index 00000000..0afc961a --- /dev/null +++ b/apps/benchmarks/mise.toml @@ -0,0 +1,4 @@ +[tools] +pipx = "1.16.5" +"pipx:meson" = "1.11.1" +"aqua:ninja-build/ninja" = "1.13.2" diff --git a/apps/benchmarks/package.json b/apps/benchmarks/package.json index 2ad283d3..bc2bd0ba 100644 --- a/apps/benchmarks/package.json +++ b/apps/benchmarks/package.json @@ -27,7 +27,7 @@ "check:paragraph-cjk-contract": "pnpm --filter @pmndrs/text build && pnpm --filter @pmndrs/text-font-baker build && node ./scripts/generate-paragraph-cjk-contract.mts --check", "check:mtsdf-render-fixture": "pnpm --filter @pmndrs/text build && pnpm --filter @pmndrs/text-font-baker build && node ./scripts/generate-mtsdf-render-fixture.mts --check", "check:slug-render-fixture": "pnpm --filter @pmndrs/text build && pnpm --filter @pmndrs/text-font-baker build && node ./scripts/generate-slug-render-fixture.mts --check", - "check:japanese-showcase-subset": "node ./scripts/provision-harfbuzz.mts && node ./scripts/generate-japanese-showcase-subset.mts --check", + "check:japanese-showcase-subset": "node ./scripts/provision-harfbuzz.mts --check && node ./scripts/generate-japanese-showcase-subset.mts --check", "check:showcase-rasters": "pnpm --filter @pmndrs/text build && pnpm --filter @pmndrs/text-font-baker build && node ./scripts/generate-showcase-raster-fixtures.mts --check", "dev": "pnpm --filter @pmndrs/text-font-baker build && vite", "format": "oxfmt .", @@ -59,7 +59,7 @@ "profile:inspect:paragraph-font-size:bitmap": "node ./scripts/summarize-cpu-profile.mts ./.cache/paragraph-font-size-bitmap.cpuprofile", "profile:icon-grid:gc": "vitexec --gpu --cpu-profile ./.cache/icon-grid.cpuprofile --performance-trace ./.cache/icon-grid-performance.json --heap-snapshot ./.cache/icon-grid-heap.txt --path '/presentation?mode=benchmark&technique=bitmap&backend=webgpu&delivery=baked&dpr=2&font=inter&workload=icon-grid' ./vitexec/icon-grid-gc.probe.ts", "profile:presentation-sweep": "vitexec --gpu --path '/presentation?mode=benchmark&technique=bitmap&backend=webgpu&delivery=baked&dpr=2&font=inter&workload=benchmark-ipsum' ./vitexec/presentation-framerate-sweep.probe.ts", - "test": "pnpm check:size && pnpm check:autoresearch-baseline && pnpm check:japanese-showcase-subset && pnpm check:paragraph-bidi-contract && pnpm check:paragraph-cjk-contract && pnpm test:unit && pnpm test:headless && pnpm test:packed", + "test": "pnpm check:size && pnpm check:autoresearch-baseline && pnpm check:paragraph-bidi-contract && pnpm check:paragraph-cjk-contract && pnpm test:unit && pnpm test:headless && pnpm test:packed", "test:headless": "pnpm --filter @pmndrs/text build && pnpm --filter @pmndrs/text-font-baker build && node ./scripts/run-headless.mts --suite conformance --dpr 1 --samples 3 --warmup 1 --port 5181", "test:packed": "pnpm --filter @pmndrs/text build && pnpm --filter @pmndrs/text-font-baker build && node ./scripts/run-packed-consumer.mts", "test:live": "node ./scripts/run-live-probe.mts", @@ -70,7 +70,7 @@ "test:raster-technique-compare:webgl": "vitexec --gpu --path '/?mode=conformance&technique=mtsdf&backend=webgl2&delivery=baked&dpr=1&font=inter&workload=mtsdf-slug-compare' ./vitexec/raster-technique-compare.probe.ts", "test:raster-technique-compare:webgpu": "vitexec --gpu --path '/?mode=conformance&technique=mtsdf&backend=webgpu&delivery=baked&dpr=1&font=inter&workload=mtsdf-slug-compare' ./vitexec/raster-technique-compare.probe.ts", "test:unit": "vitest run && pnpm test:scripts", - "test:scripts": "node --test ./scripts/support/project-chromium.test.mts", + "test:scripts": "node --test ./scripts/support/*.test.mts", "test:watch": "vitest", "typecheck": "pnpm --filter @pmndrs/text-font-baker build && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.scripts.json --noEmit" }, diff --git a/apps/benchmarks/scripts/support/toolchain-scope.test.mts b/apps/benchmarks/scripts/support/toolchain-scope.test.mts new file mode 100644 index 00000000..bce60a58 --- /dev/null +++ b/apps/benchmarks/scripts/support/toolchain-scope.test.mts @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const workspaceRoot = new URL('../../../../', import.meta.url); + +test('keeps optional HarfBuzz tools outside the root contributor toolchain', async () => { + const [rootMise, benchmarkMise, manifest, workflow] = await Promise.all([ + readFile(new URL('mise.toml', workspaceRoot), 'utf8'), + readFile(new URL('apps/benchmarks/mise.toml', workspaceRoot), 'utf8'), + readFile(new URL('apps/benchmarks/package.json', workspaceRoot), 'utf8').then(JSON.parse), + readFile(new URL('.github/workflows/ci.yml', workspaceRoot), 'utf8'), + ]); + + assert.equal(rootMise.includes('[tools]'), false, 'root mise must derive only Node, pnpm, and Rust pins'); + assert.match(benchmarkMise, /pipx = "1\.16\.5"/); + assert.match(benchmarkMise, /"pipx:meson" = "1\.11\.1"/); + assert.match(benchmarkMise, /"aqua:ninja-build\/ninja" = "1\.13\.2"/); + + assert.doesNotMatch(manifest.scripts.test, /japanese-showcase-subset/); + assert.match(manifest.scripts['check:japanese-showcase-subset'], /provision-harfbuzz\.mts --check/); + assert.match(workflow, /mise -C apps\/benchmarks install/); + assert.match(workflow, /pnpm --filter @pmndrs\/text-benchmarks check:japanese-showcase-subset/); +}); diff --git a/docs/log.md b/docs/log.md index 58cc5edd..7a658656 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,7 @@ ## 2026-08-01 +- **Scoped contributor toolchains** — Reduced the root mise install to Node, pnpm, and stable Rust; moved Meson and Ninja into the benchmark workload that provisions authenticated HarfBuzz utilities. Pnpm remains the single command surface, contributors may supply matching versions directly, non-interactive agents use `mise exec --`, and CI explicitly provisions and verifies the optional fixture gate without hiding downloads inside the ordinary check. - **Single coverage validation authority** — Removed unused Bitmap and MTSDF validation-context coverage fields; standalone validators now expose only the authenticated descriptor as expected coverage authority, eliminating a public input that could silently disagree. - **Configured MTSDF outline authority** — Qualified the public four-atlas-pixel outline constant as the 64/8 default and proved a real 32/4 artifact accepts exactly two atlas pixels but rejects `2.0001`, discriminating the authenticated runtime limit from the exported default. - **Pending Text no-op lifecycle** — Semantic no-op `Text.setProperties` calls now preserve the active cold generation, abort signal, and readiness promise, including callback-only updates that publish to the latest `onLayout`. Failed generations clear their pending ownership so the same semantic input can retry. Deterministic delayed-decode and failure regressions prove both paths without timers. diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 06a09200..94afbbcb 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/text-benchmarks' documentation_type: reference -source_digest: 'sha256:40a640a9b1804578c1b8db16a97eb1b1b98c7f61e1ee33afccc705e67f882fc1' +source_digest: 'sha256:5d9dabd66e15c53300cbb68cedcea0fd738fa5ed2e2a9492d1a4f43c7370f934' tags: [package, benchmarks, react, vite, product-e2e] sources: - id: manifest @@ -64,7 +64,7 @@ sources: title: Realtime comparison product probe generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T06:42:10Z' + at: '2026-08-01T14:20:00Z' --- # Package reference: `@pmndrs/text-benchmarks` @@ -77,7 +77,7 @@ The MSDF / Slug comparison workload owns one renderer, two equal RGBA8 render ta Font delivery is an explicit benchmark axis. **Baked asset** exercises the normal sibling asset, while **Runtime bake** passes `{ source, baked: null }`, downloads the source font, builds the core font in the serial core-baker Worker, then builds the selected Bitmap or MSDF raster in its serial lazy Worker. The inspector distinguishes the always-loaded runtime/shaper graph from the conditional core and raster baker host, Worker, and Wasm graphs; it reports source download bytes, generated core/raster CPU bytes, bake durations, and atlas GPU memory. The runtime-fallback conformance workload renders both delivery paths through the same public pipeline and requires an exact RGBA frame match. Canonical Inter matched with zero differing bytes for Bitmap and MSDF on the admitted WebGPU product probe; the observed cold MSDF raster bake was roughly 114 seconds on this host and remains an observation, not a portability threshold. -Maintainer workflows are package-owned and exposed from the workspace root. `pnpm benchmarks` starts the application, `pnpm benchmarks:check` runs its deterministic local verification, `pnpm benchmarks:test:live` runs the complete hardware-GPU product lane, `pnpm benchmarks:test:raster-technique-compare` runs the focused WebGPU/WebGL comparison and finite-job lifecycle lane, and the `pnpm benchmarks:profile:*` commands reproduce Paragraph Stress width, Bitmap rendered-size, Icon Grid allocation/frame cadence, and the complete Presentation workload sweep. The Presentation sweep waits for workload-specific committed telemetry, seeks Advanced Shaping to its complete authored specimen, rejects missing glyphs, and records RAF p95/max/slow-frame counts beside renderer CPU/GPU telemetry for all three techniques. `pnpm benchmarks:profile:icon-grid:gc` retains the full virtualized catalog traversal while recording frame intervals, heap range, pool-recycle count, and a browser performance trace. The benchmark-local React Doctor configuration keeps full live-source linting while excluding explicit Vitexec and URL-loaded entrypoints that its static reachability pass cannot discover; the accepted full scan has zero diagnostics. New repeatable benchmark workflows belong behind a package script and short root alias rather than a temporary probe or undocumented shell recipe.[^paragraph-layout-profile][^presentation-framerate-sweep] +Maintainer workflows are package-owned and exposed from the workspace root. `pnpm benchmarks` starts the application, `pnpm benchmarks:check` runs its deterministic local verification, `pnpm benchmarks:test:live` runs the complete hardware-GPU product lane, `pnpm benchmarks:test:raster-technique-compare` runs the focused WebGPU/WebGL comparison and finite-job lifecycle lane, and the `pnpm benchmarks:profile:*` commands reproduce Paragraph Stress width, Bitmap rendered-size, Icon Grid allocation/frame cadence, and the complete Presentation workload sweep. The Presentation sweep waits for workload-specific committed telemetry, seeks Advanced Shaping to its complete authored specimen, rejects missing glyphs, and records RAF p95/max/slow-frame counts beside renderer CPU/GPU telemetry for all three techniques. `pnpm benchmarks:profile:icon-grid:gc` retains the full virtualized catalog traversal while recording frame intervals, heap range, pool-recycle count, and a browser performance trace. The authenticated HarfBuzz freshness gate is deliberately separate from the ordinary repository check: its Meson, Ninja, and GLib prerequisites belong only to that benchmark fixture workload, which must be provisioned explicitly before `pnpm --filter @pmndrs/text-benchmarks check:japanese-showcase-subset`. Contributors may supply the exact versions directly or install the nested `apps/benchmarks/mise.toml` pins; pnpm remains the single workflow surface. A deterministic script test rejects optional root mise tools, hidden HarfBuzz provisioning in the ordinary test lane, missing `--check` behavior, or removal of the explicit CI gate. The benchmark-local React Doctor configuration keeps full live-source linting while excluding explicit Vitexec and URL-loaded entrypoints that its static reachability pass cannot discover; the accepted full scan has zero diagnostics. New repeatable benchmark workflows belong behind a package script and short root alias rather than a temporary probe or undocumented shell recipe.[^paragraph-layout-profile][^presentation-framerate-sweep] `pnpm benchmarks:test:presentation-demo` exercises the complete 60-second timed sequence through a focused control. Off-axis / 3D and Icon Grid each receive two seconds before Paint & Effects begins at second four; the more visual Zoom Text and returning Icon Grid scenes receive longer holds than Dynamic Layout. Advanced Shaping resets to CJK and reveals one complete five-case cycle at 180 grapheme units per second. Playing case transitions begin the next script at its first grapheme; a font-changing handoff deliberately blanks the live line until that generation commits instead of showing mismatched old-script state. Zoom Text continues its normal word cycle and cuts after three complete default-speed drops; a cancelled slot preparation clears its pending marker and retries during the same cycle instead of holding a word for another cycle. Text Ladder receives the derived 7.2 seconds required for its default-speed vertical travel and 1024 px marquee to pass completely through the left edge before the nine-second Icon Grid return. A final 8.016-second Off-axis / 3D scene supplies the closing frame. The probe requires window-capture Space handling, exact workload defaults after preload, advancing telemetry, a retained canvas, exactly one renderer, both Icon Grid entries, and the final Off-axis / 3D scene. diff --git a/docs/planning/version-contract.md b/docs/planning/version-contract.md index 7ee4b2d2..3f2af8c9 100644 --- a/docs/planning/version-contract.md +++ b/docs/planning/version-contract.md @@ -69,8 +69,8 @@ sources: resource: https://crates.io/crates/libfuzzer-sys/0.4.13 title: libfuzzer-sys 0.4.13 generated: - by: openai-codex/gpt-5 - at: '2026-07-29T17:18:36Z' + by: openai-codex/gpt-5.6 + at: '2026-08-01T14:20:00Z' --- # V0 toolchain and format version pins @@ -82,7 +82,7 @@ These values are exact fixture and provenance inputs. “Latest” is never a va | Surface | Pin | Source identity | | ------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | Rust toolchain | `1.97.1` | `rust-toolchain.toml` | -| HarfBuzz build system | Meson `1.11.1` + Ninja `1.13.2` | exact root `mise.toml` pins; used to build the authenticated HarfBuzz oracle utilities from source | +| HarfBuzz build system | Meson `1.11.1` + Ninja `1.13.2` | exact workload-scoped `apps/benchmarks/mise.toml` pins; used only to build the authenticated HarfBuzz oracle utilities from source | | HarfRust | `0.12.0` | tag commit `60b28ea22b5261710018d69c168a762bcb28794c` | | HarfBuzz oracle | `13.0.0` | tag commit `a0fc099681a69ae40665fbea74982a2e9d7a5260` | | MTSDF quality oracle | Chlumsky `msdfgen` `1.13.0` | tag `v1.13`, commit `1874bcf7d9624ccc85b4bc9a85d78116f690f35b`; source archive SHA-256 `93cd1ad8918c1a78c5c96e82d4f4c77f0eb86c2e7e8579a0967e54196c4b7167` | @@ -111,7 +111,7 @@ These values are exact fixture and provenance inputs. “Latest” is never a va | KTX2 Rust model/parser | `ktx2` `0.5.0` | compile-time R8 DFD generation plus native artifact validation | | KTX2 JavaScript parser | `ktx-parse` `1.1.0` | package-owned artifact and runtime page validation | -GLib development metadata is a native build-host prerequisite, not a fixture identity input. HarfBuzz 13.0.0 gates its `hb-shape` and `hb-subset` targets on `HAVE_GLIB`; the provisioner therefore requires `-Dglib=enabled`, and the pinned Ubuntu 24.04 CI job installs `libglib2.0-dev` explicitly and prints the resolved `glib-2.0` version. Every unrelated optional HarfBuzz backend is disabled explicitly so host-installed FreeType, Cairo, ICU, CoreText, or experimental raster/vector dependencies cannot change the source-build graph. GLib owns the utility frontend, while the authenticated HarfBuzz source and exact generated-byte comparison remain authoritative for fixture semantics. +GLib development metadata is a native build-host prerequisite for the HarfBuzz benchmark workload, not a root contributor requirement or fixture identity input. HarfBuzz 13.0.0 gates its `hb-shape` and `hb-subset` targets on `HAVE_GLIB`; the provisioner therefore requires `-Dglib=enabled`, and the pinned Ubuntu 24.04 CI job installs `libglib2.0-dev` explicitly and prints the resolved `glib-2.0` version. Every unrelated optional HarfBuzz backend is disabled explicitly so host-installed FreeType, Cairo, ICU, CoreText, or experimental raster/vector dependencies cannot change the source-build graph. GLib owns the utility frontend, while the authenticated HarfBuzz source and exact generated-byte comparison remain authoritative for fixture semantics. Contributors may supply the documented versions directly; the nested mise config is the reproducible installation option and does not create a second task surface. ## Generated contract diff --git a/mise.toml b/mise.toml index 2e7b0e23..82084f03 100644 --- a/mise.toml +++ b/mise.toml @@ -1,7 +1,2 @@ [settings] idiomatic_version_file_enable_tools = ["node", "pnpm", "rust"] - -[tools] -pipx = "1.16.5" -"pipx:meson" = "1.11.1" -"aqua:ninja-build/ninja" = "1.13.2" From d5cd17e51c5a9fb59697d5790732c00828d86875 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 10:33:49 -0400 Subject: [PATCH 10/13] fix(text): preserve generation invalidation identity --- docs/log.md | 1 + docs/packages/text.md | 6 +++--- packages/text/src/text.ts | 6 +++++- packages/text/tests/integration/text-object.test.mjs | 5 +++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/log.md b/docs/log.md index 7a658656..8bbc7b93 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,7 @@ ## 2026-08-01 +- **Committed Text invalidation identity** — Paint-only updates now mutate generation state without replacing the wrapper keyed by font-disposal listeners. A regression proves font disposal still removes painted batches and that semantic no-ops preserve terminal invalidation rather than scheduling a doomed retry. - **Scoped contributor toolchains** — Reduced the root mise install to Node, pnpm, and stable Rust; moved Meson and Ninja into the benchmark workload that provisions authenticated HarfBuzz utilities. Pnpm remains the single command surface, contributors may supply matching versions directly, non-interactive agents use `mise exec --`, and CI explicitly provisions and verifies the optional fixture gate without hiding downloads inside the ordinary check. - **Single coverage validation authority** — Removed unused Bitmap and MTSDF validation-context coverage fields; standalone validators now expose only the authenticated descriptor as expected coverage authority, eliminating a public input that could silently disagree. - **Configured MTSDF outline authority** — Qualified the public four-atlas-pixel outline constant as the 64/8 default and proved a real 32/4 artifact accepts exactly two atlas pixels but rejects `2.0001`, discriminating the authenticated runtime limit from the exported default. diff --git a/docs/packages/text.md b/docs/packages/text.md index 681491e3..c91049b6 100644 --- a/docs/packages/text.md +++ b/docs/packages/text.md @@ -5,7 +5,7 @@ description: Implements public font loading, shaping, paragraph measurement, sta resource: ../../packages/text workspace_package: '@pmndrs/text' documentation_type: reference -source_digest: 'sha256:b56fa0c2a6926af437070aa11309897a33592af33d786d4d281eabed3ca10013' +source_digest: 'sha256:57ed9e226e9cdaa9a9862370831e92d4669948595deed28ff91faba643abf017' tags: [package, public-api, typescript, contracts] sources: - id: manifest @@ -142,7 +142,7 @@ sources: title: Unicode analysis implementation generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T14:13:00Z' + at: '2026-08-01T14:35:00Z' --- # Package reference: `@pmndrs/text` @@ -201,7 +201,7 @@ One instanced batch family handles fill, outline, opacity, and translated hard s The checked SIMD comparison builds scalar, compiler-auto-vectorized, and explicit-four-lane kernels from isolated target directories. Every variant preserves all seven corrected native-oracle hashes and the complete Inter result of 2,915 generated glyphs, 22 non-rendering rejected slots, checksum `a5a6aa6e`, and composite SHA-256 `f6381c2f…eef6`; an instrumented warm seven-call corpus records seven request allocations, zero reallocations, and seven deallocations, one owned output copy occurs per call, and Wasm memory does not grow after the cold corpus. On Node 24, scalar measured 46.462 milliseconds for seven warm calls versus 47.079 milliseconds for explicit SIMD. Chromium 149 measured 47.6 versus 48.1 milliseconds. Explicit SIMD improves the complete Inter warm pass from 48.13 to 45.38 seconds, a 5.7% stress/offline win, and saves 297 Brotli bytes. Because the supported runtime default is bounded interactive baking and the target feature would require an alternate artifact, scalar remains the only shipped baker kernel; item 8.6 retains the explicit variant as evidence for phase-led optimization rather than exposing a toggle now. The repository-local Vitexec capture and full-font request emitter preserve the experiment as repeatable evidence rather than product complexity. -The framework-neutral `Text` object is now a real Three.js `Group` rather than a contract shim. It validates one complete candidate state before committing a patch, resolves every distinct root/span font through registry-scoped loader and HarfRust caches, shares decoded raster resources through `RasterRuntime`, and owns the resulting paragraph and raster batches as one generation. The first incomplete generation stays hidden; a later load keeps the prior complete generation visible until the replacement can swap atomically. Revision-scoped cancellation prevents stale work from publishing. Semantic no-op updates preserve an in-flight cold generation, its abort signal, and its original readiness observation instead of restarting raster decode. Callback-only updates also retain that work and the latest `onLayout` observes the committed layout; after a genuine generation failure, the same semantic input explicitly retries rather than becoming permanently inert. Every committed replacement releases its superseded font-disposal subscription while preserving a shared paragraph when only constraints changed. Text edits synchronously revalidate span and feature ranges; a paragraph-wide feature over empty text normalizes to a no-op. Paint-only updates reuse the positioned layout and retain one glyph-to-span paint-index plan while the text and normalized shaping ranges are unchanged. Validation and batch upload receive the same resolved `GlyphPaint` value, and repeated same-range updates reuse its `Uint16Array` index storage instead of rebuilding a code-unit map, palette-key map, and glyph-index array. One reusable Three color converter removes transient `Color` objects without weakening public color validation. Semantic no-op paint updates skip instance uploads, width updates reuse paragraph shaping, shaping changes replace the paragraph, and disposal releases every owned batch and paragraph. MTSDF batches retain outline-width and shadow-offset structure per instance: color-only updates write only paint attributes, while structural paint changes take the full geometry/UV path. Direct scalar attribute writes avoid short temporary arrays on both paths. Runtime performance instrumentation does not ship in this package; the benchmark measures public `Text` scheduling and readiness externally. Integration evidence covers changing a span's color while retaining the exact committed layout, draw-batch object, and paint-index storage; MTSDF coverage separately proves color-only updates preserve origin/size/UV structure and structural changes recompute it. A raw span font inherits the root raster definition but resolves its own font-local resource, preventing cross-font atlas reuse. +The framework-neutral `Text` object is now a real Three.js `Group` rather than a contract shim. It validates one complete candidate state before committing a patch, resolves every distinct root/span font through registry-scoped loader and HarfRust caches, shares decoded raster resources through `RasterRuntime`, and owns the resulting paragraph and raster batches as one generation. The first incomplete generation stays hidden; a later load keeps the prior complete generation visible until the replacement can swap atomically. Revision-scoped cancellation prevents stale work from publishing. Semantic no-op updates preserve an in-flight cold generation, its abort signal, and its original readiness observation instead of restarting raster decode. Callback-only updates also retain that work and the latest `onLayout` observes the committed layout; after a genuine generation failure, the same semantic input explicitly retries rather than becoming permanently inert. Terminal font-disposal invalidation is distinct: a semantic no-op preserves its rejected readiness state, while replacing the invalid input may schedule recovery. Every committed replacement releases its superseded font-disposal subscription while preserving a shared paragraph when only constraints changed. Paint-only updates mutate the committed generation state in place so its identity-keyed font-disposal listener remains attached. They reuse the positioned layout and retain one glyph-to-span paint-index plan while the text and normalized shaping ranges are unchanged. Validation and batch upload receive the same resolved `GlyphPaint` value, and repeated same-range updates reuse its `Uint16Array` index storage instead of rebuilding a code-unit map, palette-key map, and glyph-index array. One reusable Three color converter removes transient `Color` objects without weakening public color validation. Semantic no-op paint updates skip instance uploads, width updates reuse paragraph shaping, shaping changes replace the paragraph, and disposal releases every owned batch and paragraph. MTSDF batches retain outline-width and shadow-offset structure per instance: color-only updates write only paint attributes, while structural paint changes take the full geometry/UV path. Direct scalar attribute writes avoid short temporary arrays on both paths. Runtime performance instrumentation does not ship in this package; the benchmark measures public `Text` scheduling and readiness externally. Integration evidence covers changing a span's color while retaining the exact committed layout, draw-batch object, and paint-index storage; MTSDF coverage separately proves color-only updates preserve origin/size/UV structure and structural changes recompute it. A raw span font inherits the root raster definition but resolves its own font-local resource, preventing cross-font atlas reuse. The `@pmndrs/text/react` export now provides the thin runtime described by the accepted API. It flattens nested text nodes into one UTF-16 string plus ordered inherited spans, rejects nested object/layout props and non-text children, creates one core object only after React 19 dependencies resolve, forwards that object through its ref, and reconciles ordinary R3F transforms separately from core text properties. The forbidden source `text` and `spans` props remain explicit `never` fields because R3F v10's wider intrinsic-element types would otherwise weaken that public boundary. Semantic feature and inline-paint comparison prevents fresh-but-equal React values from scheduling layout or glyph-buffer work; a fresh `onLayout` callback updates ownership without repainting. `useFont`, `.preload`, `.clear`, and `lazyRaster` reuse the same loader, shaper, and raster dependencies as the core. A deterministic microtask-delayed disposal distinguishes React Strict Mode's setup/cleanup/setup cycle without sleeps or timer cushions. diff --git a/packages/text/src/text.ts b/packages/text/src/text.ts index 8f4bbb7c..37f7ab19 100644 --- a/packages/text/src/text.ts +++ b/packages/text/src/text.ts @@ -184,6 +184,7 @@ export class Text extends THREE.Group { #state: TextState; #generation: TextGeneration | undefined; #pending: AbortController | undefined; + #invalidatedState: TextState | undefined; #revision = 0; #ready: Promise = Promise.resolve(); #disposed = false; @@ -208,6 +209,7 @@ export class Text extends THREE.Group { const next = normalizeTextState(this.#state, properties, false); if (sameTextInput(this.#state, next)) { this.#state = next; + if (this.#invalidatedState !== undefined && sameTextInput(this.#invalidatedState, next)) return; if (this.#pending !== undefined) return; if (this.#generation !== undefined && sameTextInput(this.#generation.state, next)) { this.#generation.state = next; @@ -237,6 +239,7 @@ export class Text extends THREE.Group { } #schedule(prevalidatedPaint?: GlyphPaint): void { + this.#invalidatedState = undefined; this.#revision += 1; const revision = this.#revision; this.#pending?.abort(); @@ -256,7 +259,7 @@ export class Text extends THREE.Group { owned.module.updatePaint(owned.batch, paint, owned.fontSlot); } } - this.#generation = { ...this.#generation, state: this.#state }; + this.#generation.state = this.#state; this.#ready = Promise.resolve(); return; } @@ -436,6 +439,7 @@ export class Text extends THREE.Group { #invalidateGeneration(generation: TextGeneration, reason: unknown): void { if (this.#generation !== generation) return; + this.#invalidatedState = this.#state; this.#revision += 1; this.#pending?.abort(reason); this.#pending = undefined; diff --git a/packages/text/tests/integration/text-object.test.mjs b/packages/text/tests/integration/text-object.test.mjs index 180fd478..f2597a63 100644 --- a/packages/text/tests/integration/text-object.test.mjs +++ b/packages/text/tests/integration/text-object.test.mjs @@ -478,8 +478,13 @@ test('disposing a registered font invalidates live Text batches before raster re try { await text.ready; assert.equal(text.children.length, 1); + text.setProperties({ opacity: 0.5 }); + await text.ready; text.visible = false; font.dispose(); + const invalidatedReady = text.ready; + text.setProperties({}); + assert.equal(text.ready, invalidatedReady, 'semantic no-ops preserve terminal font invalidation'); assert.equal(text.children.length, 0); assert.equal(text.layout, undefined); assert.equal(text.visible, false, 'font lifecycle does not override caller visibility'); From 8d90909e949b712d39aa79a491e3206024130bf1 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 10:34:27 -0400 Subject: [PATCH 11/13] docs(benchmarks): clarify HarfBuzz prerequisites --- docs/packages/benchmarks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 94afbbcb..1a523ccd 100644 --- a/docs/packages/benchmarks.md +++ b/docs/packages/benchmarks.md @@ -64,7 +64,7 @@ sources: title: Realtime comparison product probe generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T14:20:00Z' + at: '2026-08-01T14:38:00Z' --- # Package reference: `@pmndrs/text-benchmarks` @@ -181,7 +181,7 @@ The package-size lane measures the item 8.1 MTSDF kernel separately from the cov Inter and Amiri retain their established roles. A pinned static Noto Sans Devanagari face adds the Indic lane without weakening the baker's explicit variable-font rejection. Advanced Shaping recommends a script-appropriate font for each case but exposes every baked fixture so a human can inspect coverage failures instead of having the selection silently locked. The CJK default is a reproducible HarfBuzz 13 subset of the authored Noto Sans CJK JP case; DotGothic16 remains available and explicitly labeled as pixel style. The subset is showcase evidence, not an answer to complete CJK distribution: the full 65,535-glyph Noto face remains the authoritative shaping/paragraph oracle and Milestone 13 owns chunked raster paging. -The Japanese showcase freshness check is self-contained on a declared build host: it first provisions both pinned HarfBuzz 13.0.0 utilities through the source-archive hash and executable-version gate, then rebuilds the subset in temporary storage and compares the font, license, and manifest exactly. Upstream creates `hb-shape` and `hb-subset` only when GLib development metadata is available, so the provisioner requires `-Dglib=enabled` and fails during Meson configuration when that prerequisite is absent. CI installs `libglib2.0-dev` explicitly and reports the resolved `glib-2.0` version; local macOS hosts must install both Homebrew `glib` and `pkgconf` so the equivalent metadata is discoverable through `pkg-config`. The provisioner disables unrelated optional HarfBuzz backends explicitly, keeping the source-build graph independent of other libraries installed on the host. GLib supplies the command-line frontend rather than the shaping or subset implementation, and the exact HarfBuzz version plus generated bytes remain the fixture authorities. An ignored developer cache may accelerate the check but is never an undeclared prerequisite. +The Japanese showcase freshness check is reproducible on a declared build host but intentionally does not provision tools or download source. Its explicit prerequisite step authenticates and builds both pinned HarfBuzz 13.0.0 utilities through the source-archive hash and executable-version gate; the check then requires that ignored cache, rebuilds the subset in temporary storage, and compares the font, license, and manifest exactly. Upstream creates `hb-shape` and `hb-subset` only when GLib development metadata is available, so provisioning requires `-Dglib=enabled` and fails during Meson configuration when that prerequisite is absent. CI installs `libglib2.0-dev` explicitly, reports the resolved `glib-2.0` version, provisions the utilities, and then runs the freshness check. Local macOS hosts that choose to provision must install both Homebrew `glib` and `pkgconf` so the equivalent metadata is discoverable through `pkg-config`. The provisioner disables unrelated optional HarfBuzz backends explicitly, keeping the source-build graph independent of other libraries installed on the host. GLib supplies the command-line frontend rather than the shaping or subset implementation, and the exact HarfBuzz version plus generated bytes remain the fixture authorities. The browser product also carries the React 19 subpath proofs. A shared registry target mounts public nested `` through a real React Three Fiber root backed by `WebGPURenderer`, retains one forwarded core object through width reflow and canonical restoration, matches pinned natural/narrow paragraph oracles, verifies two span paints in one draw, and submits a real renderer frame over three deterministic samples. The live pending-resource probe intercepts the exact composed Inter request behind a manually released promise, observes the Suspense fallback before publication, releases the request without a timer, then proves the registered font key and all 2,937 glyphs before deterministic cleanup. The test renderer remains confined to package integration evidence and does not enter the product registry or application dependencies. From 295f19af66033b590b36ec0defbe0a07f8fe7c1c Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 11:33:17 -0400 Subject: [PATCH 12/13] fix(text): scope terminal invalidation to generation --- .../src/benchmark/package-size-budgets.ts | 2 +- .../src/benchmark/package-sizes.test.ts | 22 +++++----- .../generated/autoresearch-baseline-v0.json | 2 +- .../src/generated/package-sizes.json | 40 +++++++++---------- docs/log.md | 2 + docs/packages/benchmarks.md | 8 ++-- docs/packages/text.md | 6 +-- packages/text/src/text.ts | 3 +- .../tests/integration/text-object.test.mjs | 29 ++++++++++++++ 9 files changed, 74 insertions(+), 40 deletions(-) diff --git a/apps/benchmarks/src/benchmark/package-size-budgets.ts b/apps/benchmarks/src/benchmark/package-size-budgets.ts index 9aa789d3..179b1115 100644 --- a/apps/benchmarks/src/benchmark/package-size-budgets.ts +++ b/apps/benchmarks/src/benchmark/package-size-budgets.ts @@ -1,6 +1,6 @@ export const packageSizeBudgets = { 'browser-core': { - rawBytes: 330_000, + rawBytes: 331_000, minifiedBytes: 252_000, gzipBytes: 74_000, brotliBytes: 57_000, diff --git a/apps/benchmarks/src/benchmark/package-sizes.test.ts b/apps/benchmarks/src/benchmark/package-sizes.test.ts index de07f58a..1d228f41 100644 --- a/apps/benchmarks/src/benchmark/package-sizes.test.ts +++ b/apps/benchmarks/src/benchmark/package-sizes.test.ts @@ -68,10 +68,10 @@ describe('independent package-size report', () => { it('bounds the accepted coverage-capability growth from its pre-coverage baseline', () => { const coverageGrowth = { 'browser-core': { - rawBytes: { baseline: 324_269, maximumGrowth: 5_400 }, - minifiedBytes: { baseline: 247_205, maximumGrowth: 4_000 }, - gzipBytes: { baseline: 72_108, maximumGrowth: 1_000 }, - brotliBytes: { baseline: 55_251, maximumGrowth: 825 }, + rawBytes: { baseline: 324_269, maximumGrowth: 6_100 }, + minifiedBytes: { baseline: 247_205, maximumGrowth: 4_400 }, + gzipBytes: { baseline: 72_108, maximumGrowth: 1_100 }, + brotliBytes: { baseline: 55_251, maximumGrowth: 1_000 }, }, 'bitmap-baker-js': { rawBytes: { baseline: 17_478, maximumGrowth: 5_700 }, @@ -86,9 +86,9 @@ describe('independent package-size report', () => { brotliBytes: { baseline: 173_552, maximumGrowth: 7_000 }, }, 'bitmap-runtime-js': { - rawBytes: { baseline: 361_809, maximumGrowth: 9_500 }, - minifiedBytes: { baseline: 271_005, maximumGrowth: 6_400 }, - gzipBytes: { baseline: 78_673, maximumGrowth: 1_400 }, + rawBytes: { baseline: 361_809, maximumGrowth: 10_100 }, + minifiedBytes: { baseline: 271_005, maximumGrowth: 6_700 }, + gzipBytes: { baseline: 78_673, maximumGrowth: 1_500 }, brotliBytes: { baseline: 60_857, maximumGrowth: 1_300 }, }, 'mtsdf-baker-wasm': { @@ -104,10 +104,10 @@ describe('independent package-size report', () => { brotliBytes: { baseline: 4_176, maximumGrowth: 800 }, }, 'mtsdf-runtime-js': { - rawBytes: { baseline: 370_255, maximumGrowth: 8_050 }, - minifiedBytes: { baseline: 275_271, maximumGrowth: 5_500 }, - gzipBytes: { baseline: 79_993, maximumGrowth: 1_300 }, - brotliBytes: { baseline: 62_081, maximumGrowth: 1_075 }, + rawBytes: { baseline: 370_255, maximumGrowth: 8_700 }, + minifiedBytes: { baseline: 275_271, maximumGrowth: 5_900 }, + gzipBytes: { baseline: 79_993, maximumGrowth: 1_400 }, + brotliBytes: { baseline: 62_081, maximumGrowth: 1_200 }, }, } as const; const fields = ['rawBytes', 'minifiedBytes', 'gzipBytes', 'brotliBytes'] as const; diff --git a/apps/benchmarks/src/generated/autoresearch-baseline-v0.json b/apps/benchmarks/src/generated/autoresearch-baseline-v0.json index bcc238a5..de22a1b9 100644 --- a/apps/benchmarks/src/generated/autoresearch-baseline-v0.json +++ b/apps/benchmarks/src/generated/autoresearch-baseline-v0.json @@ -14,7 +14,7 @@ { "id": "package-sizes", "path": "apps/benchmarks/src/generated/package-sizes.json", - "sha256": "04594bffa1d47344ac585929eb7a996caf4363958961ffcd488062994fec6333", + "sha256": "39dbcab2192df1acbc8c46c801fff1134b4bff2a17598b23c034e20c89a10fa3", "bytes": 6825 }, { diff --git a/apps/benchmarks/src/generated/package-sizes.json b/apps/benchmarks/src/generated/package-sizes.json index ecded90e..403ebbd5 100644 --- a/apps/benchmarks/src/generated/package-sizes.json +++ b/apps/benchmarks/src/generated/package-sizes.json @@ -10,11 +10,11 @@ "label": "Browser core", "status": "measured", "format": "javascript", - "sha256": "fd69489c7e7325527e6c14a9a78c66a9157b85e725e86f7acf8f9bf22699d175", - "rawBytes": 329665, - "minifiedBytes": 251133, - "gzipBytes": 73068, - "brotliBytes": 56025 + "sha256": "659d4c89e2693772d409a4d4e78d30689c66bd4b4b5a0ed66ed098b8629631ab", + "rawBytes": 330343, + "minifiedBytes": 251524, + "gzipBytes": 73143, + "brotliBytes": 56131 }, { "id": "font-validator-js", @@ -76,33 +76,33 @@ "label": "Bitmap runtime JS graph", "status": "measured", "format": "javascript", - "sha256": "2aadb2e89b913f226d95aa576f1afb77b8a9fe3dee7e94bd712d2a4a5444f515", - "rawBytes": 371218, - "minifiedBytes": 277255, - "gzipBytes": 80045, - "brotliBytes": 62080 + "sha256": "bfe69d2ac627ac4668ff5f7230a01d4ce51a36a03f58f11c244ead1ffb4994a7", + "rawBytes": 371896, + "minifiedBytes": 277647, + "gzipBytes": 80119, + "brotliBytes": 62138 }, { "id": "mtsdf-runtime-js", "label": "MTSDF runtime JS graph", "status": "measured", "format": "javascript", - "sha256": "0c659b814ce63f45f92623d1115532eafbe5c1184696b68092e8771f28ce3681", - "rawBytes": 378236, - "minifiedBytes": 280716, - "gzipBytes": 81233, - "brotliBytes": 63075 + "sha256": "f2b5c16f360d1ab760fb86791d5b2fe5ca2930c6f532fe519abc82d0b8612ba8", + "rawBytes": 378914, + "minifiedBytes": 281107, + "gzipBytes": 81311, + "brotliBytes": 63206 }, { "id": "slug-runtime-js", "label": "Slug runtime JS graph", "status": "measured", "format": "javascript", - "sha256": "7989e06e08ec7fa1d654a392e6abed7f879b772baad8f67ea63ceafa9dc850b1", - "rawBytes": 379432, - "minifiedBytes": 279956, - "gzipBytes": 81398, - "brotliBytes": 63197 + "sha256": "b4b7edb6e225b75c5284bd5b4a1f7f63c2276e14854b819ce78e470213cf0cc8", + "rawBytes": 380110, + "minifiedBytes": 280347, + "gzipBytes": 81483, + "brotliBytes": 63340 }, { "id": "bitmap-baker-wasm", diff --git a/docs/log.md b/docs/log.md index 8bbc7b93..136d897b 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,8 @@ ## 2026-08-01 +- **Scoped Text invalidation recovery** — Bound terminal font-disposal state to the invalidated generation's input rather than a newer replacement already in flight, and clear that saved state on explicit disposal. A two-registry regression proves disposing a superseded font cannot permanently suppress recovery through the healthy replacement font. +- **Lifecycle payload refresh** — Refreshed the single-build package-size identity and its fail-closed autoresearch provenance after the final `Text` invalidation fixes. Browser core grows by 678 raw / 391 minified / 75 gzip / 106 Brotli bytes; optional baker hosts and Wasm artifacts remain byte-identical. The browser core's reviewed raw ceiling moves from 330,000 to 331,000 bytes. The independent pre-coverage caps advance only for browser core and the Bitmap/MTSDF runtime closures that contain it, with 13–120 bytes of headroom over current values; every absolute minified/compressed budget and every baker/Wasm limit is unchanged. - **Committed Text invalidation identity** — Paint-only updates now mutate generation state without replacing the wrapper keyed by font-disposal listeners. A regression proves font disposal still removes painted batches and that semantic no-ops preserve terminal invalidation rather than scheduling a doomed retry. - **Scoped contributor toolchains** — Reduced the root mise install to Node, pnpm, and stable Rust; moved Meson and Ninja into the benchmark workload that provisions authenticated HarfBuzz utilities. Pnpm remains the single command surface, contributors may supply matching versions directly, non-interactive agents use `mise exec --`, and CI explicitly provisions and verifies the optional fixture gate without hiding downloads inside the ordinary check. - **Single coverage validation authority** — Removed unused Bitmap and MTSDF validation-context coverage fields; standalone validators now expose only the authenticated descriptor as expected coverage authority, eliminating a public input that could silently disagree. diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 1a523ccd..759bd24b 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/text-benchmarks' documentation_type: reference -source_digest: 'sha256:5d9dabd66e15c53300cbb68cedcea0fd738fa5ed2e2a9492d1a4f43c7370f934' +source_digest: 'sha256:cec575dd67df2ff31b9c411e88eda2e2e2ef6388ca864cd4f777450ac4ad6b09' tags: [package, benchmarks, react, vite, product-e2e] sources: - id: manifest @@ -64,7 +64,7 @@ sources: title: Realtime comparison product probe generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T14:38:00Z' + at: '2026-08-01T15:28:00Z' --- # Package reference: `@pmndrs/text-benchmarks` @@ -177,7 +177,7 @@ The CI-safe advanced-shaping target derives all 68 finite frames from that same The separate live performance observation runs the human WebGPU surface at explicit 1× DPR on Chromium 149 and an Apple `metal-3` adapter. Each paragraph-scale script lane must settle its exact authored state with zero missing glyphs and then publish twelve causal FPS and GPU-report intervals; there are no sleeps or timing thresholds. The refreshed run observed 119.46–120.16 FPS, 0.2–0.3 ms median CPU submission, 0.3–0.5 ms CPU P95, 0.679–3.457 ms median GPU time, and 3.261–5.033 ms GPU P95 across 112–278 glyphs and one to fifteen draws. Initial public `Text` readiness was 7.2–22.0 ms and total startup 17.0–122.9 ms; the first cold Inter fetch dominates the high end. `Text.ready` includes shaping, paragraph layout, and bitmap-batch publication, so it is not mislabeled as a pure shape call; the dedicated HarfRust target owns that narrower metric. These machine observations are authenticated evidence, not cross-device budgets. -The package-size lane measures the item 8.1 MTSDF kernel separately from the coverage-capable item 8.6 baker and every initial browser or unrelated raster graph. The validated generator host is 11,543 raw, 8,466 minified, 2,658 gzip, and 2,364 Brotli bytes; the corrected optimized scalar kernel is 52,633 raw, 23,115 gzip, and 19,660 Brotli bytes. The complete MTSDF baker adds Fontations, bounded face-resolved coverage, and artifact packaging behind the optional subpath and measures 552,025 raw, 215,030 gzip, and 168,758 Brotli Wasm bytes plus a 26,940 raw / 19,117 minified / 5,530 gzip / 4,908 Brotli host. The Bitmap baker with the same coverage contract measures 626,940 raw, 234,735 gzip, and 180,503 Brotli Wasm bytes. The private TypeScript diagnostic entry is neither packed nor reachable from production graphs, and Rust profiling remains a non-default feature; the size lane rejects diagnostic code in shipped baker graphs and profiling/timing Wasm boundaries. A separate pre-coverage regression table bounds the accepted growth of browser core, both optional hosts and runtimes, and both baker Wasm modules in every measured representation. Complete reviewed ceilings apply on foreign hosts, while same-host regeneration must remain byte-exact. A package-owned command additionally reports compile, initialization, cold-corpus, and warm-corpus observations only after all seven independent oracle hashes pass; it is generator evidence, not frame-rendering performance. The local `capture:mtsdf-simd` command builds isolated scalar, auto-vectorized, channel-SIMD, scalar-tile, and adjacent-texel SIMD evidence modules, executes exact-hash-gated calls in GPU-enabled Chromium, and publishes none of the four experimental variants. +The package-size lane measures the item 8.1 MTSDF kernel separately from the coverage-capable item 8.6 baker and every initial browser or unrelated raster graph. The validated generator host is 11,543 raw, 8,466 minified, 2,658 gzip, and 2,364 Brotli bytes; the corrected optimized scalar kernel is 52,633 raw, 23,115 gzip, and 19,660 Brotli bytes. The complete MTSDF baker adds Fontations, bounded face-resolved coverage, and artifact packaging behind the optional subpath and measures 552,025 raw, 215,030 gzip, and 168,758 Brotli Wasm bytes plus a 26,940 raw / 19,117 minified / 5,530 gzip / 4,908 Brotli host. The Bitmap baker with the same coverage contract measures 626,940 raw, 234,735 gzip, and 180,503 Brotli Wasm bytes. The private TypeScript diagnostic entry is neither packed nor reachable from production graphs, and Rust profiling remains a non-default feature; the size lane rejects diagnostic code in shipped baker graphs and profiling/timing Wasm boundaries. The final `Text` lifecycle remediations move the browser-core graph from 329,665 / 251,133 / 73,068 / 56,025 to 330,343 raw / 251,524 minified / 73,143 gzip / 56,131 Brotli bytes without changing any baker host or Wasm artifact. A separate pre-coverage regression table bounds the accepted growth of browser core, both optional hosts and runtimes, and both baker Wasm modules in every measured representation. Complete reviewed ceilings apply on foreign hosts, while same-host regeneration must remain byte-exact. A package-owned command additionally reports compile, initialization, cold-corpus, and warm-corpus observations only after all seven independent oracle hashes pass; it is generator evidence, not frame-rendering performance. The local `capture:mtsdf-simd` command builds isolated scalar, auto-vectorized, channel-SIMD, scalar-tile, and adjacent-texel SIMD evidence modules, executes exact-hash-gated calls in GPU-enabled Chromium, and publishes none of the four experimental variants. Inter and Amiri retain their established roles. A pinned static Noto Sans Devanagari face adds the Indic lane without weakening the baker's explicit variable-font rejection. Advanced Shaping recommends a script-appropriate font for each case but exposes every baked fixture so a human can inspect coverage failures instead of having the selection silently locked. The CJK default is a reproducible HarfBuzz 13 subset of the authored Noto Sans CJK JP case; DotGothic16 remains available and explicitly labeled as pixel style. The subset is showcase evidence, not an answer to complete CJK distribution: the full 65,535-glyph Noto face remains the authoritative shaping/paragraph oracle and Milestone 13 owns chunked raster paging. @@ -187,6 +187,8 @@ The browser product also carries the React 19 subpath proofs. A shared registry The initial deterministic browser probe is admitted with a checked-in record: 100 executions across 10 fresh GPU-friendly Chromium/Vite lifecycles, zero retries/failures, unique causal completion identities, and wrong-expectation plus withheld-completion negative controls. Probe exit status and every parsed lifecycle/environment field are validated before publication. Browser scripts navigate only through DOM readiness and then wait on the product's own completion promise or visible state; they do not use network-idle heuristics. Exact contract comparison rejects non-finite numbers, exotic objects, key-order differences, and missing or additional fields without JSON coercion. The current live probe executes the exact TSL graph on asserted WebGPU and forced WebGL2 backends before paragraph measurement, positioned-layout, bidi/policy, CJK, and mobile Playwright flows. This proves a real GPU shader workload while reserving the rendered-font claim for item 6.1. +The final lifecycle remediation revises the reviewed browser-core raw ceiling from 330,000 to 331,000 bytes. Independent pre-coverage caps advance only for browser core and the Bitmap/MTSDF runtime closures that contain it, retaining 13–120 bytes of headroom over the current values. Every absolute minified/compressed budget and every baker-host and Wasm ceiling remains unchanged. + ## Package scripts | Script | Purpose | diff --git a/docs/packages/text.md b/docs/packages/text.md index c91049b6..5f38541c 100644 --- a/docs/packages/text.md +++ b/docs/packages/text.md @@ -5,7 +5,7 @@ description: Implements public font loading, shaping, paragraph measurement, sta resource: ../../packages/text workspace_package: '@pmndrs/text' documentation_type: reference -source_digest: 'sha256:57ed9e226e9cdaa9a9862370831e92d4669948595deed28ff91faba643abf017' +source_digest: 'sha256:4b110ff822492b5303eba1b0ffcc94e5e3c10a4a4c55946d895a77005fcabc0e' tags: [package, public-api, typescript, contracts] sources: - id: manifest @@ -142,7 +142,7 @@ sources: title: Unicode analysis implementation generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T14:35:00Z' + at: '2026-08-01T15:28:00Z' --- # Package reference: `@pmndrs/text` @@ -201,7 +201,7 @@ One instanced batch family handles fill, outline, opacity, and translated hard s The checked SIMD comparison builds scalar, compiler-auto-vectorized, and explicit-four-lane kernels from isolated target directories. Every variant preserves all seven corrected native-oracle hashes and the complete Inter result of 2,915 generated glyphs, 22 non-rendering rejected slots, checksum `a5a6aa6e`, and composite SHA-256 `f6381c2f…eef6`; an instrumented warm seven-call corpus records seven request allocations, zero reallocations, and seven deallocations, one owned output copy occurs per call, and Wasm memory does not grow after the cold corpus. On Node 24, scalar measured 46.462 milliseconds for seven warm calls versus 47.079 milliseconds for explicit SIMD. Chromium 149 measured 47.6 versus 48.1 milliseconds. Explicit SIMD improves the complete Inter warm pass from 48.13 to 45.38 seconds, a 5.7% stress/offline win, and saves 297 Brotli bytes. Because the supported runtime default is bounded interactive baking and the target feature would require an alternate artifact, scalar remains the only shipped baker kernel; item 8.6 retains the explicit variant as evidence for phase-led optimization rather than exposing a toggle now. The repository-local Vitexec capture and full-font request emitter preserve the experiment as repeatable evidence rather than product complexity. -The framework-neutral `Text` object is now a real Three.js `Group` rather than a contract shim. It validates one complete candidate state before committing a patch, resolves every distinct root/span font through registry-scoped loader and HarfRust caches, shares decoded raster resources through `RasterRuntime`, and owns the resulting paragraph and raster batches as one generation. The first incomplete generation stays hidden; a later load keeps the prior complete generation visible until the replacement can swap atomically. Revision-scoped cancellation prevents stale work from publishing. Semantic no-op updates preserve an in-flight cold generation, its abort signal, and its original readiness observation instead of restarting raster decode. Callback-only updates also retain that work and the latest `onLayout` observes the committed layout; after a genuine generation failure, the same semantic input explicitly retries rather than becoming permanently inert. Terminal font-disposal invalidation is distinct: a semantic no-op preserves its rejected readiness state, while replacing the invalid input may schedule recovery. Every committed replacement releases its superseded font-disposal subscription while preserving a shared paragraph when only constraints changed. Paint-only updates mutate the committed generation state in place so its identity-keyed font-disposal listener remains attached. They reuse the positioned layout and retain one glyph-to-span paint-index plan while the text and normalized shaping ranges are unchanged. Validation and batch upload receive the same resolved `GlyphPaint` value, and repeated same-range updates reuse its `Uint16Array` index storage instead of rebuilding a code-unit map, palette-key map, and glyph-index array. One reusable Three color converter removes transient `Color` objects without weakening public color validation. Semantic no-op paint updates skip instance uploads, width updates reuse paragraph shaping, shaping changes replace the paragraph, and disposal releases every owned batch and paragraph. MTSDF batches retain outline-width and shadow-offset structure per instance: color-only updates write only paint attributes, while structural paint changes take the full geometry/UV path. Direct scalar attribute writes avoid short temporary arrays on both paths. Runtime performance instrumentation does not ship in this package; the benchmark measures public `Text` scheduling and readiness externally. Integration evidence covers changing a span's color while retaining the exact committed layout, draw-batch object, and paint-index storage; MTSDF coverage separately proves color-only updates preserve origin/size/UV structure and structural changes recompute it. A raw span font inherits the root raster definition but resolves its own font-local resource, preventing cross-font atlas reuse. +The framework-neutral `Text` object is now a real Three.js `Group` rather than a contract shim. It validates one complete candidate state before committing a patch, resolves every distinct root/span font through registry-scoped loader and HarfRust caches, shares decoded raster resources through `RasterRuntime`, and owns the resulting paragraph and raster batches as one generation. The first incomplete generation stays hidden; a later load keeps the prior complete generation visible until the replacement can swap atomically. Revision-scoped cancellation prevents stale work from publishing. Semantic no-op updates preserve an in-flight cold generation, its abort signal, and its original readiness observation instead of restarting raster decode. Callback-only updates also retain that work and the latest `onLayout` observes the committed layout; after a genuine generation failure, the same semantic input explicitly retries rather than becoming permanently inert. Terminal font-disposal invalidation is distinct: a semantic no-op matching the invalidated generation's input preserves its rejected readiness state, while replacing that input may schedule recovery. The terminal state is scoped to the invalidated generation's own input, so an already-staged replacement remains recoverable if the superseded font is disposed. Explicit disposal clears that saved terminal state along with pending and committed ownership. Every committed replacement releases its superseded font-disposal subscription while preserving a shared paragraph when only constraints changed. Paint-only updates mutate the committed generation state in place so its identity-keyed font-disposal listener remains attached. They reuse the positioned layout and retain one glyph-to-span paint-index plan while the text and normalized shaping ranges are unchanged. Validation and batch upload receive the same resolved `GlyphPaint` value, and repeated same-range updates reuse its `Uint16Array` index storage instead of rebuilding a code-unit map, palette-key map, and glyph-index array. One reusable Three color converter removes transient `Color` objects without weakening public color validation. Semantic no-op paint updates skip instance uploads, width updates reuse paragraph shaping, shaping changes replace the paragraph, and disposal releases every owned batch and paragraph. MTSDF batches retain outline-width and shadow-offset structure per instance: color-only updates write only paint attributes, while structural paint changes take the full geometry/UV path. Direct scalar attribute writes avoid short temporary arrays on both paths. Runtime performance instrumentation does not ship in this package; the benchmark measures public `Text` scheduling and readiness externally. Integration evidence covers changing a span's color while retaining the exact committed layout, draw-batch object, and paint-index storage; MTSDF coverage separately proves color-only updates preserve origin/size/UV structure and structural changes recompute it. A raw span font inherits the root raster definition but resolves its own font-local resource, preventing cross-font atlas reuse. The `@pmndrs/text/react` export now provides the thin runtime described by the accepted API. It flattens nested text nodes into one UTF-16 string plus ordered inherited spans, rejects nested object/layout props and non-text children, creates one core object only after React 19 dependencies resolve, forwards that object through its ref, and reconciles ordinary R3F transforms separately from core text properties. The forbidden source `text` and `spans` props remain explicit `never` fields because R3F v10's wider intrinsic-element types would otherwise weaken that public boundary. Semantic feature and inline-paint comparison prevents fresh-but-equal React values from scheduling layout or glyph-buffer work; a fresh `onLayout` callback updates ownership without repainting. `useFont`, `.preload`, `.clear`, and `lazyRaster` reuse the same loader, shaper, and raster dependencies as the core. A deterministic microtask-delayed disposal distinguishes React Strict Mode's setup/cleanup/setup cycle without sleeps or timer cushions. diff --git a/packages/text/src/text.ts b/packages/text/src/text.ts index 37f7ab19..a154c653 100644 --- a/packages/text/src/text.ts +++ b/packages/text/src/text.ts @@ -235,6 +235,7 @@ export class Text extends THREE.Group { this.#pending = undefined; this.#disposeGeneration(this.#generation); this.#generation = undefined; + this.#invalidatedState = undefined; this.#setCancelledReady(reason); } @@ -439,7 +440,7 @@ export class Text extends THREE.Group { #invalidateGeneration(generation: TextGeneration, reason: unknown): void { if (this.#generation !== generation) return; - this.#invalidatedState = this.#state; + this.#invalidatedState = generation.state; this.#revision += 1; this.#pending?.abort(reason); this.#pending = undefined; diff --git a/packages/text/tests/integration/text-object.test.mjs b/packages/text/tests/integration/text-object.test.mjs index f2597a63..80434019 100644 --- a/packages/text/tests/integration/text-object.test.mjs +++ b/packages/text/tests/integration/text-object.test.mjs @@ -495,6 +495,35 @@ test('disposing a registered font invalidates live Text batches before raster re } }); +test('disposing a superseded font does not terminally invalidate its pending replacement', async () => { + const restoreFetch = installFileFetch(); + const bytes = await readFile(fixtureUrl); + const registryA = new FontRegistry(); + const registryB = new FontRegistry(); + const fontA = await registryA.registerAsset(bytes); + const fontB = await registryB.registerAsset(bytes); + const text = new Text({ + text: 'font replacement lifecycle', + font: fontA, + raster: bitmap({ strikes: [16] }), + fontSize: 16, + }); + try { + await text.ready; + text.setProperties({ font: fontB }); + fontA.dispose(); + await text.ready.catch(() => undefined); + text.setProperties({}); + await text.ready; + assert.equal(text.children.length, 1); + } finally { + text.dispose(); + fontA.dispose(); + fontB.dispose(); + restoreFetch(); + } +}); + test('Text rejects a raster batch without the required Three.js lifecycle surface', async () => { const restoreFetch = installFileFetch(); const registry = new FontRegistry(); From d23cecbc59161279fad5f3142531037e7c454979 Mon Sep 17 00:00:00 2001 From: Justin Walsh Date: Sat, 1 Aug 2026 11:44:03 -0400 Subject: [PATCH 13/13] docs(roadmap): close milestones 6 and 8 --- docs/log.md | 1 + docs/roadmap/roadmap.md | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/log.md b/docs/log.md index 136d897b..439792b6 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,6 +2,7 @@ ## 2026-08-01 +- **Milestone 6/8 closure** — Closed Milestones 6 and 8 after the deferred combined adversarial review, independent remediation of every actionable lifecycle finding, exact size/provenance refresh, 182 package integration tests, 257 benchmark unit tests, complete headless conformance, packed-consumer execution, and zero-error OKF validation. Milestone 10 is now the next authorized implementation milestone. - **Scoped Text invalidation recovery** — Bound terminal font-disposal state to the invalidated generation's input rather than a newer replacement already in flight, and clear that saved state on explicit disposal. A two-registry regression proves disposing a superseded font cannot permanently suppress recovery through the healthy replacement font. - **Lifecycle payload refresh** — Refreshed the single-build package-size identity and its fail-closed autoresearch provenance after the final `Text` invalidation fixes. Browser core grows by 678 raw / 391 minified / 75 gzip / 106 Brotli bytes; optional baker hosts and Wasm artifacts remain byte-identical. The browser core's reviewed raw ceiling moves from 330,000 to 331,000 bytes. The independent pre-coverage caps advance only for browser core and the Bitmap/MTSDF runtime closures that contain it, with 13–120 bytes of headroom over current values; every absolute minified/compressed budget and every baker/Wasm limit is unchanged. - **Committed Text invalidation identity** — Paint-only updates now mutate generation state without replacing the wrapper keyed by font-disposal listeners. A regression proves font disposal still removes painted batches and that semantic no-ops preserve terminal invalidation rather than scheduling a doomed retry. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 72891c4f..91375c9d 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -16,7 +16,7 @@ sources: generated: by: openai-codex/gpt-5.6 - at: '2026-08-01T06:26:44Z' + at: '2026-08-01T15:31:00Z' --- # Canonical implementation roadmap @@ -49,13 +49,13 @@ Status key: ✅ complete · 🟡 in progress · ⬜ not started · ⛔ blocked | 3 | ✅ | Build baked-first loader and Worker fallback | L | 2 | Baked hits stay small; misses dynamically load the Worker path and reproduce canonical bytes. | | 4 | ✅ | Integrate HarfRust Wasm shaping | L | 2–3 | Coarse batch calls match pinned HarfRust fixtures and expose clusters, positions, and flags. | | 5 | ✅ | Implement paragraph reflow and validate universal shaping assumptions | L | 4 | Allocation-light layout passes Latin, bidi/complex-script, and focused CJK source/reduced-font evidence. | -| 6 | 🟡 | Prove rendering with bitmap inside the benchmark harness | L | 3, 5 | The harness produces the first real font frame on WebGPU and WebGL2 with direct bulk upload. | +| 6 | ✅ | Prove rendering with bitmap inside the benchmark harness | L | 3, 5 | The harness produces the first real font frame on WebGPU and WebGL2 with direct bulk upload. | | 7 | ✅ | Harden the integration proof | L | 1–6 | Identity, cancellation, limits, invalid data, package separation, and baselines pass review. | -| 8 | 🟡 | Implement and validate MSDF | XL | 7 | The MTSDF-backed general-purpose raster passes visual, payload, and GPU performance gates. | +| 8 | ✅ | Implement and validate MSDF | XL | 7 | The MTSDF-backed general-purpose raster passes visual, payload, and GPU performance gates. | | 9 | ✅ | Port/rewrite and validate Slug | XL | 7 | Outline-accurate text passes correctness, packing, visual, and GPU performance gates. | | 10 | ⬜ | Harden the first shippable release | L | 8–9 | Bitmap, MSDF, and Slug ship as independent modules over one shaping/layout result. | -Milestones 0–5, 7, and 9 are closed. Milestone 6 remains active pending its deferred closure review, while Milestone 8 implementation is active through item 8.6. The final Milestone 8 review also closes the remaining Milestone 6 review gates. +Milestones 0–9 are closed. Milestone 10 is the next authorized implementation milestone. Do not start a milestone before its dependencies and exit evidence exist. @@ -103,9 +103,9 @@ These rows replace the former separate backlog. Each is intended to become one f | 5.4 | ✅ | Pin one redistributable pan-CJK face and prove source/reduced HarfRust, HarfBuzz, horizontal paragraph layout, fuzz, and Node/Chromium/Vitexec evidence without renderer or paging work. | L | 5.3 | | 6.0 | ✅ | Establish the current-repository TSL compiler, shader, and live WebGPU/WebGL2 baseline without broad type erasure. | S | 3.3, 5.4 | | 6.1 | ✅ | Upload/render bitmap records and textures as the harness's first real raster target on WebGPU/WebGL2. | M | 6.0 | -| 6.2 | 🟡 | Implement the Three.js `Text` object over the bitmap proof. | M | 6.1 | -| 6.3 | 🟡 | Implement `@pmndrs/text/react` as a thin reconciliation layer. | M | 6.2 | -| 6.4 | 🟡 | Rework the harness into a benchmark-first human control plane with a separate visual conformance mode. | M | 6.1–6.3 | +| 6.2 | ✅ | Implement the Three.js `Text` object over the bitmap proof. | M | 6.1 | +| 6.3 | ✅ | Implement `@pmndrs/text/react` as a thin reconciliation layer. | M | 6.2 | +| 6.4 | ✅ | Rework the harness into a benchmark-first human control plane with a separate visual conformance mode. | M | 6.1–6.3 | | 7.1 | ✅ | Harden lifecycle, invalid input, limits, and package graphs. | M | 1–6 | | 7.2 | ✅ | Ship the advanced-shaping showcase and record end-to-end conformance/performance baselines. | M | 7.1 | | 8.1 | ✅ | Implement the repository-owned deterministic `no_std` Rust MTSDF core and pass panic, scalar/SIMD, Wasm, size, fuzz, and native-msdfgen quality gates. | L | 7.2 | @@ -113,7 +113,7 @@ These rows replace the former separate backlog. Each is intended to become one f | 8.3 | ✅ | Implement the optional MSDF runtime module, strict validation, one resource/batch family, paint effects, and disposal. | L | 8.2 | | 8.4 | ✅ | Implement one version-matched TSL MTSDF graph for WebGPU and WebGL2 with resize, transform, base-level minification, and effects scenes. | L | 8.3 | | 8.5 | ✅ | Record visual-error, atlas, upload, memory, bundle-isolation, and steady-state rendering evidence. | XL | 8.4 | -| 8.6 | 🟡 | Add configurable MTSDF quality, bounded runtime-atlas options, compiler-derived Wasm ABI layouts, and measured baker performance hardening before closing Milestone 8. | XL | 8.5 | +| 8.6 | ✅ | Add configurable MTSDF quality, bounded runtime-atlas options, compiler-derived Wasm ABI layouts, and measured baker performance hardening before closing Milestone 8. | XL | 8.5 | | 9.1 | ✅ | Port Slug outline conversion, exact normalization/bands, compact packing, deterministic baker, validator, and embedded/external resources. | XL | 7.2 | | 9.2 | ✅ | Copy and adapt the version-matched analytic TSL fill runtime, batching, lifecycle, fail-closed paint boundary, and public `Text` integration. | XL | 9.1 | | 9.3 | ✅ | Integrate Slug into the shared benchmark/conformance product, release-role scenes, source-outline matrix, and complete two-axis icon-font grid. | XL | 9.2 | @@ -460,7 +460,7 @@ Large-coverage raster paging remains in Milestone 13. Vertical-form source table - [x] Malformed language/surrogate/variation/constraint cases and fixed-seed CJK mutations are deterministic and trap-free. - [x] Node integration, Chromium 149 headless, GPU-enabled Vitexec, and the mobile Playwright flow pass through the shared benchmark registry without timers, retries, renderer metrics, or paging claims. -Item 5.4 and Milestone 5 are closed. Milestone 6 is active. +Item 5.4 and Milestones 5 and 6 are closed. ## Milestone 6 — first rendering proof: bitmap in the benchmark harness @@ -468,7 +468,7 @@ Items 6.0 and 6.1 are closed on the repository's current Three.js 0.185.1, `@typ The first real font frame travels through composed Inter GLB loading, the public framework-neutral `Text`, retained HarfRust shaping, paragraph positioning, strict bitmap record/KTX2 decode, direct R8 texture upload, order-preserving instanced batching, and one shared TSL material. Explicit 1× and 2× runs each execute three measured samples after one warmup on WebGPU and forced WebGL2. Both backends render the five-lane benchmark ipsum as 120 visible glyphs with zero missing glyphs, in one draw from 695,296 atlas bytes. Bitmap density is a hard contract rather than an implicit layout scale: CSS font size remains stable across DPR, the rendering integration supplies `rasterPixelRatio`, and the bitmap module targets `CSS size × ratio` when choosing a declared physical strike. The existing exact conformance capture deliberately uses 16 device pixels at both DPRs; the live product keeps 16 CSS pixels and exposes visible degradation until a 32 ppem strike is present at 2×. Raster records retain Zeno's actual integer placement with `planeUnitsPerEm = 16`, and the TSL graph snaps projected quad edges to physical pixels. A benchmark-only CPU compositor places authenticated atlas texels and matches every normalized GPU byte for the full frame and a resized clipped frame. WebGPU and forced WebGL2 produce identical full-frame hashes: `a47930d3…e893` at 1× and `95b20e05…a34d` at 2×. Both DPRs contain the same 3,473 half-coverage pixels; bounds are `[68, 18, 313, 112]` and `[260, 82, 505, 176]`. Framebuffer bytes are 196,608 at 1× and 786,432 at 2×; total tracked bytes are 891,904 and 1,481,728. The shared registry also owns a deterministic React reconciler scenario that retains one core object across nested-span reflow. Bitmap rejects outline/shadow rather than silently discarding them; hinted grayscale and four-phase packing remain documented research, and LCD/ClearType rendering is out of scope. -Items 6.2 and 6.3 are implemented and their first adversarial-review findings are remediated. Item 6.4 is active before the milestone closure review because the initial shell exposed internal target/scenario mechanics and presented conformance duration too prominently. The corrected human default is a continuously rendered benchmark control plane showing consumer-facing startup, retained-size, CPU-frame, FPS, and supported GPU-time evidence. Conformance is a separate finite deep-inspection surface showing candidate, reference, difference, structured evidence, and end-to-end test duration. Technique, backend, and workload are independent user controls; target and scenario remain internal runner concepts. The Figma wireframe supplies visual direction rather than prescribing product information architecture. +Items 6.2–6.4 and Milestone 6 are closed after the deferred combined Milestone 6/8 adversarial review. Every actionable lifecycle finding was reproduced and remediated, including pending semantic no-ops, callback-only ownership, failed-generation retry, paint-generation identity, terminal invalidation scope, and explicit disposal cleanup. The corrected human default is a continuously rendered benchmark control plane showing consumer-facing startup, retained-size, CPU-frame, FPS, and supported GPU-time evidence. Conformance is a separate finite deep-inspection surface showing candidate, reference, difference, structured evidence, and end-to-end test duration. Technique, backend, and workload are independent user controls; target and scenario remain internal runner concepts. The Figma wireframe supplies visual direction rather than prescribing product information architecture. The five-line, 120-glyph text above is now named the diagnostic conformance specimen. The separate paragraph-scale live benchmark renders 1,151 glyphs through the same single-draw bitmap path. @@ -478,7 +478,7 @@ The five-line, 120-glyph text above is now named the diagnostic conformance spec - [x] Initial readiness, generation replacement, stale cancellation, paint-only updates, width reflow, shaping invalidation, and disposal have deterministic integration tests over the canonical Inter GLB. - [x] Distinct span fonts resolve independent raster resources while sharing registry-scoped loader, shaper, and raster caches. - [x] Every raster draw batch exposes its Three.js object and deterministic disposal contract. -- [ ] The Milestone 6 adversarial review has no unresolved actionable finding against item 6.2. +- [x] The Milestone 6 adversarial review has no unresolved actionable finding against item 6.2. ### 6.3 closure checklist @@ -486,7 +486,7 @@ The five-line, 120-glyph text above is now named the diagnostic conformance spec - [x] Nested `` nodes flatten into one string plus ordered inherited spans; non-text children and nested object/layout properties fail explicitly. - [x] `useFont`, `.preload`, `.clear`, and `lazyRaster` share core dependency caches and expose deterministic Suspense boundaries. - [x] Resolved R3F reconciliation and browser-pending Suspense have executable evidence without sleeps, retries, or timer cushions. -- [ ] The Milestone 6 adversarial review has no unresolved actionable finding against item 6.3. +- [x] The Milestone 6 adversarial review has no unresolved actionable finding against item 6.3. ### 6.4 closure checklist @@ -643,7 +643,9 @@ Runtime baking is a supported delivery path, not merely a missing-asset recovery - [x] Instrument the baker by phase and publish small, medium, and complete-face results for glyph selection, outline extraction, MTSDF texel generation, packing, texture-payload encoding, container serialization, Wasm-to-Worker copying, peak memory, and output bytes. Reports include glyphs, generated texels, edges visited, and throughput rather than one opaque wall-clock duration; direct Wasm and the real serial Worker retain exact artifact identity. - [x] Optimize the measured dominant phase without weakening native-msdfgen quality or deterministic artifact gates. Texel generation dominates, so an equivalent four-texel scalar tile and an adjacent-texel SIMD line-distance kernel were compared against the unchanged scalar quadratic/cubic lane fallback. Every exact oracle and complete-Inter identity remains unchanged. Adjacent SIMD improves the bounded Node and Chromium corpora by 2.4% and 0.9%, but is indistinguishable from scalar over complete Inter warm execution while adding 20.7% optimized and 11.4% Brotli bytes. Scalar tile improves bounded Node by 10.1% but regresses Chromium by 1.5% and complete Inter warm by 1.4% while adding 20.0% optimized and 10.9% Brotli bytes. Machine-checked structured observations retain those tradeoffs. Both candidates are rejected as universal runtime defaults, remain non-shipping experiment features, and scalar Wasm remains the single shipped kernel. TypeGPU/WebGPU compute remains research until identical-work evidence can justify its device and readback complexity. - [x] Select pinned dynamic Talc from the complete optimized Wasm corpus: byte-identical behavior retains the existing ownership/error/reused-Worker tests while saving 46,610 raw, 15,121 gzip, and 12,121 Brotli bytes versus `dlmalloc`. Reject a 128 MiB global arena because it raises initial memory to about 129 MiB for no meaningful transfer saving; keep request-local scratch arenas as profiling-led future work only. -- [ ] Complete the final adversarial Milestone 6/8 review with no unresolved actionable findings. +- [x] Complete the final adversarial Milestone 6/8 review with no unresolved actionable findings. + +Item 8.6 and Milestone 8 are closed. The combined closure review retained complete traces under the ignored repository review cache, every actionable finding was independently reproduced and remediated, the exact package-size identity and fail-closed provenance are current, and the complete package, benchmark-unit, headless conformance, packed-consumer, and documentation gates pass on the recorded host. ## Milestone 9 — Slug release renderer