diff --git a/apps/benchmarks/src/benchmark/package-size-budgets.ts b/apps/benchmarks/src/benchmark/package-size-budgets.ts index 759bd98d..55eb3c6d 100644 --- a/apps/benchmarks/src/benchmark/package-size-budgets.ts +++ b/apps/benchmarks/src/benchmark/package-size-budgets.ts @@ -7,11 +7,17 @@ export const packageSizeBudgets = { }, // The renderer-neutral core subpath (D-249) must stay integration-free; the graph // assertion in measure-package-sizes.mts already rejects any three/tsl/react pull. + // Grew with the technique-schema authority layer (D-251): declarations, validation, + // and the schema-typed store path. The review-closure pass added +3,525 raw / + // +1,686 minified / +351 Brotli of real validation, provenance, and derivation + // code (schema normalization and freezing, DSL session provenance, + // schemaPolicyBuffers/schemaFieldTable). Re-based when tsdown bundling lands per + // the technique contract plan. 'core-subpath-js': { - rawBytes: 215_000, - minifiedBytes: 148_000, - gzipBytes: 38_000, - brotliBytes: 32_500, + rawBytes: 226_000, + minifiedBytes: 155_000, + gzipBytes: 39_400, + brotliBytes: 33_900, }, 'tsl-subpath-js': { rawBytes: 27_000, @@ -47,11 +53,16 @@ export const packageSizeBudgets = { gzipBytes: 429_000, brotliBytes: 339_500, }, + // Raw rose for the policy-DSL authoring layer riding the Three bundle (D-250), + // then the review-closure pass added +3,535 raw / +1,711 minified / +439 Brotli + // of schema-derived executor lookups, program buffer derivation, and the + // glyph-origin schema map replacing literal id ranges. Real code, not comments; + // the compressed ceilings hold with tight headroom by design. 'three-runtime-js': { - rawBytes: 350_000, - minifiedBytes: 232_000, - gzipBytes: 60_000, - brotliBytes: 51_000, + rawBytes: 366_000, + minifiedBytes: 239_000, + gzipBytes: 61_800, + brotliBytes: 52_200, }, 'font-inter-bitmap-16-32': { rawBytes: 3_200_000, diff --git a/apps/benchmarks/src/generated/package-sizes.json b/apps/benchmarks/src/generated/package-sizes.json index c68b819b..d58414f6 100644 --- a/apps/benchmarks/src/generated/package-sizes.json +++ b/apps/benchmarks/src/generated/package-sizes.json @@ -10,11 +10,11 @@ "label": "Renderer-neutral core JS", "status": "measured", "format": "javascript", - "sha256": "bdbdb8a9112a9c600cd9020f816c9cbe30296d3298f0ae9c1f8e0f1de55c6f6e", - "rawBytes": 207759, - "minifiedBytes": 142016, - "gzipBytes": 36140, - "brotliBytes": 30853 + "sha256": "54a83a8595d3756eee428549ceea7077f546372f655f5d091be5829737cefc4d", + "rawBytes": 225596, + "minifiedBytes": 154330, + "gzipBytes": 39021, + "brotliBytes": 33248 }, { "id": "tsl-subpath-js", @@ -54,11 +54,11 @@ "label": "Three.js adapter JS", "status": "measured", "format": "javascript", - "sha256": "0a231f0b3b658eecc1ed7eb1765e66b5f1b4ce53b01fc4405540351af1830df4", - "rawBytes": 347795, - "minifiedBytes": 227757, - "gzipBytes": 59005, - "brotliBytes": 49764 + "sha256": "59fd78dbb413502ac3b9014bc51a0912eafb15cc588aa4732c459ca5a738cf19", + "rawBytes": 365690, + "minifiedBytes": 238911, + "gzipBytes": 61533, + "brotliBytes": 52080 }, { "id": "font-inter-bitmap-16-32", @@ -164,33 +164,33 @@ "label": "Bitmap runtime JS graph", "status": "measured", "format": "javascript", - "sha256": "8350a9c6113b00b0b3b6ad1acad12b88ffdeeff2f020075ee8a3f63f1a7f6421", - "rawBytes": 337047, - "minifiedBytes": 220443, - "gzipBytes": 57609, - "brotliBytes": 48186 + "sha256": "7f2f6b109b49c430e8b70a35ae43e450d7c49477e8583cb0dff07a7ef604de21", + "rawBytes": 354942, + "minifiedBytes": 231588, + "gzipBytes": 60389, + "brotliBytes": 50563 }, { "id": "mtsdf-runtime-js", "label": "MSDF runtime JS graph", "status": "measured", "format": "javascript", - "sha256": "c05d0beaebc0d0aa81d79b9b7f7c5693b4d1d5c3e3757bfe8cf24ed76526b052", - "rawBytes": 337043, - "minifiedBytes": 220425, - "gzipBytes": 57590, - "brotliBytes": 48260 + "sha256": "fcfc3e1cd4da4f7969cae2c4258c58cac52f3e9bff7c4a45748290a371e81180", + "rawBytes": 354938, + "minifiedBytes": 231569, + "gzipBytes": 60449, + "brotliBytes": 50503 }, { "id": "slug-runtime-js", "label": "Slug runtime JS graph", "status": "measured", "format": "javascript", - "sha256": "99d66c65a3c7c3a68ac5b8d0ec7dbfedc5d40bfc344ddc1b70f12d3495fbede2", - "rawBytes": 337045, - "minifiedBytes": 220509, - "gzipBytes": 57484, - "brotliBytes": 48145 + "sha256": "9d938b57d027a7c910ef4355ebfb696462d562a4a545cb96f03fa4dfcd79484b", + "rawBytes": 354940, + "minifiedBytes": 231663, + "gzipBytes": 60303, + "brotliBytes": 50565 }, { "id": "bitmap-baker-wasm", diff --git a/docs/log.md b/docs/log.md index b6e3aad0..4ebec086 100644 --- a/docs/log.md +++ b/docs/log.md @@ -13,6 +13,46 @@ ## 2026-08-11 +- **Technique-contract review closure (D-250/D-251)** — Closed all four adversarial-review findings on the + schema-authority stack. Policy-DSL values now carry session provenance: a value loaded from one program's input + table throws when stored through another builder instead of silently reading a shifted input. Technique schemas + deep-freeze at definition, making the documented immutability true at runtime. The remaining smear sites now + derive from schemas: `schemaPolicyBuffers` replaces hand-rolled width lists in the Three programs, + `schemaFieldTable` orders binding-table readers by the schema's declared names so a misspelled or missing reader + is a compile error, the executor resolves draw buffers by schema name instead of literal id ranges, and the + plan-program registry references the transform system buffer instead of restating `15`. Glyph-origin + augmentation became schema-declared opt-in metadata (`glyphOrigin`) rather than assuming Bitmap's buffer layout + for every technique. The structural gate now also rejects literal-width buffer builders, literal id ranges, and + restated system ids. Every policy and binding byte golden stayed pinned — the derivations reproduce the + hand-rolled bytes exactly, decided by the existing decoded-equivalence proof. The adversarial re-review then + confirmed provenance and glyph-origin closed and surfaced follow-up defects, fixed in a second pass: session + provenance is now stamped at node construction and combined in O(1) — a shared expression DAG no longer costs an + exponential graph walk, and mixing sessions fails at the combinator itself; schema definition validates the + caller's input first and returns an owned, deeply frozen copy, so rejection leaves caller data untouched and a + hostile lanes accessor can never change a validated width; the size-budget notes now record the measured deltas + (+3.5 KB raw / +1.7 KB minified per surface of real validation and derivation code) instead of claiming + comment-dominated growth; and the package reference gained `/core` and `/tsl` rows with `/three/{bitmap,msdf,slug}` + described as the compatibility aliases they are. The re-review's remaining structural finding — TSL shader lane + meaning and the external example still restate schema knowledge — is migration layers 3–4 of the technique + contract plan, scheduled with the audit. + +- **Technique schema authority (D-251)** — Buffer ids, lanes, and binding fields are declared once per technique + by colocated schemas; programs store through schema handles, the executor reads declared ids, and a repository + gate forbids literal buffer identity anywhere else. Policy bytes proven byte-identical across the change. The + full contract plan — shader-interface derivation, data origins including the reserved `pretext` fallback, and + the tsdown build — is recorded in docs/planning/raster-technique-contract.md. + +- **Policy interpreter tail measurement** — Driving the kernel-lab explicit artifact at controlled record counts: + a 4-record vector iteration costs ~50–75 ns and each scalar tail record ~50 ns, so a span of 7 records + (1 vector + 3 scalar, 282 ns) costs more than the 8-record two-vector shape (181 ns) that a tail-overlap + rewrite would produce. Overlap would therefore save roughly 100 ns per tailed draw-span — real but bounded: + a frame needs thousands of tailed spans before it reaches microseconds. Not wired per the D-245 admission + bar; revisit if a workload profile ever shows many small draw-spans dominating the packing pass. + +- **Policy authoring DSL (D-250)** — Policy programs are written against named semantic and binding handles with + automatic register allocation; the four Three programs ported with a decoded-bytes equivalence proof against the + hand-numbered fixtures and re-pinned goldens. Wire format and interpreter unchanged. + - **Core API surface (D-249)** — The renderer-neutral engine publishes as `@pmndrs/glyph/core` and the technique shader library as `@pmndrs/glyph/tsl`. Three's first-party policy and the Slug shader tree leave core internals, and a scoped import lint holds the first-party integrations to the same public surface a third party gets. diff --git a/docs/packages/benchmarks.md b/docs/packages/benchmarks.md index 53b0498b..8694ecc7 100644 --- a/docs/packages/benchmarks.md +++ b/docs/packages/benchmarks.md @@ -5,7 +5,7 @@ description: Provides the shared interactive and automated benchmark product sur resource: ../../apps/benchmarks workspace_package: '@pmndrs/glyph-benchmarks' documentation_type: reference -source_digest: 'sha256:e9af38649ef27133babebf9a6503bc2297f5e8cd187c246da59e5b9611e49a45' +source_digest: 'sha256:66cf967802ebd6088bbb585004d67e03c894cb79c8776c0bd1f5a2a7c2c2bc1e' tags: [package, benchmarks, react, vite, product-e2e] sources: - id: manifest diff --git a/docs/packages/glyph.md b/docs/packages/glyph.md index c1a61344..060e9412 100644 --- a/docs/packages/glyph.md +++ b/docs/packages/glyph.md @@ -5,7 +5,7 @@ description: Implements portable font loading, retained Rust shaping and layout, resource: ../../packages/glyph workspace_package: '@pmndrs/glyph' documentation_type: reference -source_digest: 'sha256:a05cfbb2b9c43ac971c71259b6d93e962d4550dd0c44a424b21c30f1b0980e59' +source_digest: 'sha256:165fd636d138709fe954feaea7b79188e6fb6271d45a9efdceb55f420e430b37' tags: [package, public-api, rust, wasm, threejs, typography] sources: - id: manifest @@ -100,27 +100,21 @@ TypeScript does not independently shape, lay out, or pack paragraphs. ## Public package surfaces -| Subpath | Purpose | -| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Subpath | Purpose | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `@pmndrs/glyph` | Font/raster contracts, loading, fallback stacks, formatting helpers, paragraph inputs, layout-query values, and portable bakers. | +| `@pmndrs/glyph/core` | Renderer-neutral engine host, frame wire, plan/layout-query views, technique schemas, policy-program DSL, and binding compiler. | +| `@pmndrs/glyph/tsl` | Canonical TSL shader realizations of the first-party technique interfaces; no scene integration. | | `@pmndrs/glyph/three` | Three `FontLoader`, `Text`, `TextGroup`, material factories, and policy registration. | -| `@pmndrs/glyph/three/bitmap` | Bitmap technique, policy program, and canonical TSL shader. | -| `@pmndrs/glyph/three/msdf` | MSDF technique, policy program, and canonical TSL shader. | -| `@pmndrs/glyph/three/slug` | Slug technique, policy program, and canonical TSL shader. | +| `@pmndrs/glyph/three/bitmap` | Compatibility alias re-exporting the renderer-neutral Bitmap raster module. | +| `@pmndrs/glyph/three/msdf` | Compatibility alias re-exporting the renderer-neutral MSDF raster module. | +| `@pmndrs/glyph/three/slug` | Compatibility alias re-exporting the renderer-neutral Slug raster module. | | `@pmndrs/glyph/react` | React ``, ``, and `useFont`, reconciled through React Three Fiber. | -| `@pmndrs/glyph/bake` | Node programmatic font baking, glyph selection, and font inspection used by the `glyph` CLI. | +| `@pmndrs/glyph/bake` | Node programmatic font baking, glyph selection, and font inspection used by the `text` CLI. | | `@pmndrs/glyph/runtime-bake` | Explicit browser Worker host for optional runtime baking. | | `@pmndrs/glyph/raster/*` | Renderer-neutral Bitmap, MSDF, and Slug decoding and raster-technique contracts. | | `@pmndrs/glyph/bakers/*` | Optional portable raster bakers and validators. | -## Canary releases - -Pushes to `main` run the package's complete `check` lane and publish `@pmndrs/glyph` to npm under the `canary` dist-tag. -The workflow derives an immutable prerelease version from the manifest's base version, the first eight commit SHA -characters, and the UTC release date. npm trusted publishing authenticates the GitHub-hosted job with OIDC; the package -manifest pins the public npm registry and repository identity, and npm generates provenance for the public package. -The workflow carries no long-lived npm token. - The font-baker Rust source, direct-memory wrapper, schemas, tests, build pipeline, optimized Wasm, and generated ABI are owned by this package. There is no separately published font-baker package. The root entry has no static edge to the baker, its `std`-enabled dependencies, Ajv, glTF Validator, or the baker Wasm; only explicit bake/runtime-bake surfaces can diff --git a/docs/planning/benchmark-plan.md b/docs/planning/benchmark-plan.md index 5a374e7b..5302d427 100644 --- a/docs/planning/benchmark-plan.md +++ b/docs/planning/benchmark-plan.md @@ -73,7 +73,7 @@ Status key: ✅ specified or available · 🟡 partial or conditional · ⬜ not | Harness gate | Status | Evidence required to advance | | -------------------------------------------- | :----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Canonical architecture and scenario contract | ✅ | This plan owns one target registry, one scenario registry, and one runner contract for interactive and headless surfaces. | -| Portable baker target | ✅ | `packages/glyph/rust/font-baker` and the app run immutable Inter 4.1 bytes through the direct-memory Wasm API with deterministic GLB evidence. | +| Portable baker target | ✅ | `packages/glyph/rust/font-baker` and the app run immutable Inter 4.1 bytes through the direct-memory Wasm API with deterministic GLB evidence. | | Lab shell under `apps/benchmarks` | ✅ | The responsive token/component shell defaults to the human-facing live benchmark with mode, technique, backend, and workload URL state; finite visual conformance is separate. Fixed histories report renderer-callback CPU time, FPS, and real WebGPU/WebGL2 GPU timestamps when supported, while capture/export snapshots the live contract on demand. Causal product checks own label fit, control density, horizontal overflow, and mobile/tablet/desktop flow at 390, 1,024, and 1,280 CSS pixels. | | Headless product E2E | 🟡 | A browser CLI, Vitexec, and Playwright call the same strict registry execution module. The bounded CI-safe conformance suite includes synthetic, forced-WebGL2 TSL and bitmap rendering, public React `Text` reconciliation, direct-baker, loader/Worker, HarfRust, paragraph, bidi/policy/uikit, and item-5.4 CJK lanes. Hardware-WebGPU and pending-Suspense probes remain maintainer-local, and Milestone 6 awaits its closure review. | | Package-size lane | ✅ | Independent library-mode entries produce nonzero raw/minified/gzip/Brotli initial-core, Unicode 17 analysis, lazy-validator, runtime-host, runtime-Worker, baker, and shaper JavaScript sizes plus raw/gzip/Brotli Wasm. Rollup static closures exclude dynamic chunks; the browser-core lane externalizes declared `three`, React, and R3F peers, while Worker and shaper JavaScript exclude separately measured Wasm assets. The record names its measurement host: same-host output stays exact, while every foreign-host entry must satisfy the shared complete reviewed budgets. Unicode analysis is 139,936 bytes minified and the Darwin arm64 shaper record is 32,778 bytes minified JavaScript plus 680,312 bytes optimized Wasm. | diff --git a/docs/planning/decision-register.md b/docs/planning/decision-register.md index 6fddc5d6..08cda3ad 100644 --- a/docs/planning/decision-register.md +++ b/docs/planning/decision-register.md @@ -327,6 +327,8 @@ The [architecture](architecture.md) owns loading behavior and dependency rules. | D-239 | A known local font is directly expressible through `text bake --input --output` without authoring a discovery module or custom baking script. The package exposes one `text` executable with command-specific help and version output. First-party `--bitmap`, `--msdf`, and `--slug` flags select embedded raster resources; `--unicodes` invokes the package-owned Fontations/Skera baker Wasm before the shared `bakeFont` path so one prepared source feeds the shaping font and every raster; and `--check` performs a temporary byte-exact rebuild. `text glyphs` uses the same Wasm and Skrifa to surface Unicode mappings, exact glyph IDs, and retained `post`/CFF names as JSON or a bake-ready Unicode set without inventing semantic names. Product baking has no HarfBuzz executable dependency; pinned HarfBuzz remains an internal correctness oracle only. Runtime and R3F loading accept one nonempty tuple of raster requests for one input and return a position-preserving typed tuple of `LoadedFont` values. The font artifact is fetched and registered once while each declared raster still performs its required independent decode. Required per-technique options remain compile-time enforced. | Accepted | | D-240 | CLI, Node, and runtime Worker baking share one prepare-once pipeline. A runtime request carries normalized Unicode ranges and the complete ordered raster plan; the Worker feeds the exact prepared source to the shaping bake and every selected first-party raster, composes one canonical GLB, validates it, and transfers one artifact. Only that final artifact is eligible for Worker-owned `CacheStorage`, keyed by source, face, ranges, exact raster descriptors/keys, and contract versions. Persistence inherits the source response's reusable freshness (`max-age` or `Expires`); `no-store`, `no-cache`, missing freshness, and expired responses remain memory-only. Browser quota eviction owns storage pressure, and storage failures remain transparent misses. Every GLB producer records `asset.generator` as the publishing package identity `@pmndrs/glyph`. | Accepted | | D-241 | The package exposes its React integration as `@pmndrs/glyph/react`, matching the original public API, roadmap, and ecosystem convention. React Three Fiber remains the internal reconciler and a peer dependency, but is not encoded into the public subpath name. The stale `/r3f` export and generated entry are removed rather than retained as a second alias before publication. | Accepted | +| D-251 | Each raster technique's physical shape is declared once by a colocated, exported schema (`defineTechniqueSchema` in core; `bitmapSchema`/`msdfSchema`/`slugSchema` beside their techniques, `decorationSchema` and the Three policy's system buffers in the Three policy): buffer ids, scalar kinds, lane meanings, binding field names, and resource kinds. Policy programs build with `techniqueProgram(schema)` and store through schema buffer handles; the plan executor looks buffers up by declared id; a repository gate rejects any literal buffer lookup, literal attribute name, or parallel id const outside the declaration sites. The compiled policy bytes are proven byte-identical across the change — the schema layer is pure naming with an owner. The full cleanup plan, including shader-interface derivation, the data-origin axis with the reserved `pmndrs.pretext` fallback technique, and the tsdown build change, is recorded in [the technique contract plan](raster-technique-contract.md). | Accepted | +| D-250 | Policy programs are authored through a compile-time expression DSL in `@pmndrs/glyph/core` (`policyProgram`, `addF32`/`subtractF32`/`multiplyF32`/`u32ToF32`, typed constants) instead of hand-numbered registers. Authors reference named semantic handles (`inlineOrigin`, `fontSize`, `color.red`) and declared binding fields (`bearingX`, `uvOriginX`, `page`); `compile()` lowers the expression graph to the same forward-only `PolicyOperation` records, allocating registers automatically with use-before-write and exhaustion as errors and deduplicating reused values. The wire format, Rust validator, and interpreter are untouched, and the u32/f32 distinction remains wire-level per operation and buffer schema — the DSL's branded value types exist only at authoring time. The four Three programs are ported with per-technique named buffer ids; a semantic-equivalence test decodes old and new bytes and proves identical input tables, buffer schemas, metadata, and per-lane store dataflow against the hand-numbered fixtures, and the byte goldens are re-pinned once over that proof. | Accepted | | D-249 | The renderer-neutral core publishes as `@pmndrs/glyph/core` and the technique shader library as `@pmndrs/glyph/tsl`. Core carries runtime shaper creation, the engine host and sessions, frame-wire serialization, render-plan and layout views, font-binding compilation, the versioned ABI, and the policy-authoring toolkit; the runtime-to-shaper bridge is public. Three-specific policy — per-technique programs, capability set, first-party buffer ids — moves from core internals to `three/render-policy.ts` and is built with the same public toolkit a third party uses. The four technique TSL node graphs, including the Slug shader tree formerly in core internals, move to `src/tsl/` under Tsl-prefixed names; the Three entry stops re-exporting shader symbols. First-party integration rigor is enforced by a scoped `no-restricted-imports` lint denying the three, tsl, and react surfaces any import from `internal/` or `generated/`. Wire contracts and behavior are unchanged; the moves are type-level, pinned by subpath type tests. | Accepted | | D-248 | Text decoration rendering lands as the first 11.18 slice, pulled forward for visual proof. Spans declare `decoration` (underline, overline, line-through; solid only — other line styles are rejected at the boundary rather than silently rendered solid). The engine cascade stamps the CSS decorating box: the declaring span's resolved font size rides the resolved decoration group, positioning derives one continuous line per decorating box from the baked `post`/`OS/2` metrics (top-of-stroke semantics), and adjacent runs with one decoration identity merge across nested font-size changes. Decoration records flow as a reserved resource-free `pmndrs.decoration` technique: plan programs declare a primitive kind in the former reserved wire field, planners admit resource-free rows and emit `PRIMITIVE_DECORATION` with zero-resource draws, underline/overline rows append before the paragraph's glyphs and line-through after so draw order matches CSS paint order, and Three realizes one shared flat-quad TSL material with no texture, decoding the packed sRGB paint through the sRGB EOTF so a text-colored line is byte-identical to its glyph ink at the framebuffer. Decorated sessions rebuild their gather output; the undecorated fast path is unchanged, verified by a same-window interleaved A/B against the pre-decoration checkpoint (−0.3%/+0.6%/−0.7%/+1.6% on cold/font-size/suffix-edit/splice; earlier apparent regressions reproduced on the checkpoint under ambient load). Retained decoration diffing and patterned line styles remain 11.18 work. | Accepted | | D-247 | The shaping and layout contract represents a break-inserted hyphen glyph without a source cluster, proven by an exact positioning test rather than a contract change. Any flow fragment — not only an ellipsized final line — may reference a boundary-shape record whose source span is empty and whose inserted span holds independently shaped glyphs: the inserted glyph positions after the fragment's retained clusters, carries its own stable glyph identity from the boundary arena, publishes the boundary text position as its semantic cluster (`SemanticGlyph.cluster = text_end`, the "no source cluster" representation), inherits style and paint from the neighbor cluster, participates in alignment through the composed line advance, and receives a content revision; the following line is unaffected. Nothing in the path is ellipsis-specific, so 11.14's hyphenation-adjacent work composes onto this record shape. Language patterns, break selection, and justification quality controls remain later work. | Accepted | diff --git a/docs/planning/font-baker-implementation.md b/docs/planning/font-baker-implementation.md index f91458a8..7d714e22 100644 --- a/docs/planning/font-baker-implementation.md +++ b/docs/planning/font-baker-implementation.md @@ -43,7 +43,7 @@ Status key: ✅ complete for the declared slice · 🟡 in progress · ⬜ not s | Area | Status | Current evidence | Next gate | | ------------------------ | :----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| Package placement | ✅ | Rust/Wasm lives in `packages/glyph/rust/font-baker`; its TypeScript bridge, build support, and tests live in the matching `packages/glyph` ownership tree. `@pmndrs/glyph/bake` is the sole programmatic product surface and no second font-baker package is published. | Keep future baker artifacts inside `@pmndrs/glyph` or a baker-only source crate. | +| Package placement | ✅ | Rust/Wasm lives in `packages/glyph/rust/font-baker`; its TypeScript bridge, build support, and tests live in the matching `packages/text` ownership tree. `@pmndrs/glyph/bake` is the sole programmatic product surface and no second font-baker package is published. | Keep future baker artifacts inside `@pmndrs/glyph` or a baker-only source crate. | | Portable Rust core | ✅ | Delegates SFNT/TTC and typed-table parsing to Fontations `read-fonts`, and metrics/bounds interpretation to `skrifa`; public fixtures cover container/table policy, face selection, deterministic reduction, dense extents, shaping identity, and exact Inter 4.1 output.[^fontations] | Keep every new policy branch paired with a focused regression. | | Source preparation | ✅ | Feature-gated Skera 0.5.1 prepares canonical Unicode subsets and Skrifa enumerates exact cmap/glyph-name facts through generated `prepare` and `inspect` ABI exports. CLI, Node, and runtime Worker hosts share one functional pipeline: one normalized preparation result feeds the shaping bake and every requested Bitmap, MSDF, and Slug bake before one canonical GLB is composed and validated. An actual Inter ASCII Worker integration proves its output byte-identical to Node, and a request-boundary regression proves all three raster plans cross once. The capability is confined to baker paths: runtime/shared crates remain `no_std`, while baker-only shared crates may reuse it. | Keep new producer paths on this shared pipeline and preserve byte-identical Node/Worker output. | | Stable Wasm ABI | ✅ | Fixed-width `#[repr(C)]` types are the sole layout authority. Build-only Rust generation derives size, alignment, and offsets with `size_of`/`align_of`/`offset_of!`, publishes portable JSON, and emits an exact typed `as const` TypeScript module. CI rejects stale generated source; production Wasm embeds no contract and exports no ABI bootstrap. | Keep compiler-derived JSON/TypeScript identity and absent-Wasm-contract checks mandatory as the ABI evolves. | diff --git a/docs/planning/raster-technique-contract.md b/docs/planning/raster-technique-contract.md new file mode 100644 index 00000000..b85324fb --- /dev/null +++ b/docs/planning/raster-technique-contract.md @@ -0,0 +1,236 @@ +--- +type: Research Concept +title: Raster technique contract and single-authority cleanup +description: Defines one authoritative, colocated declaration per raster technique — schema, shader interface, binding, data origin, and policy program — with package subpaths as the reasoning and tree-shaking boundaries. +documentation_type: explanation +status: draft +tags: [planning, raster, technique, policy, tsl, typegpu, boundaries] +sources: + - id: raster-technique-api + resource: raster-technique-api.md + title: Raster technique and engine resource API + - id: typegpu-shader-authority + resource: typegpu-first-shader-authority.md + title: TypeGPU-first shader authority + - id: gpucat-integration + resource: gpucat-integration.md + title: External gpucat integration proof + - id: technique-example + resource: ../../packages/glyph-example-raster/src/raster.ts + title: External raster technique example + - id: core-policy-dsl + resource: ../../packages/glyph/src/core/policy-program.ts + title: Policy-program expression DSL + - id: three-policy + resource: ../../packages/glyph/src/three/render-policy.ts + title: Three render policy programs + - id: font-binding + resource: ../../packages/glyph/src/core/font-binding.ts + title: Font-binding compiler and per-technique tables + - id: plan-executor + resource: ../../packages/glyph/src/three/engine-plan-target.ts + title: Three command-buffer executor +generated: + by: anthropic-claude/fable-5 + at: '2026-08-12T00:00:00Z' +--- + +# Raster technique contract and single-authority cleanup + +A raster technique is the unit of extensibility this library promises: Bitmap, MSDF, and Slug are the first-party +proofs, `glyph-example-raster` is the external proof, and TypeGPU integrations are the next consumer. Today a +technique passes every test while its definition is smeared across six sites that agree only by convention. This +plan makes each technique one colocated, authoritative declaration; makes package subpaths the reasoning and +tree-shaking boundaries; and states the proof obligations that keep the contract honest. + +## The sin, measured + +Where "Bitmap" lives today — six sites, four of which repeat schema knowledge the others cannot see: + +```mermaid +flowchart TB + subgraph raster ["@pmndrs/glyph/raster/bitmap"] + contract["technique object\ndescriptor · rasterKey · decode"] + end + subgraph core ["@pmndrs/glyph/core"] + binding["font-binding.ts\ncompileBitmap: field lambdas\n(order = implicit schema)"] + end + subgraph tsl ["@pmndrs/glyph/tsl"] + shader["bitmap-shader.ts\nreads uvOrigin, uvSize, color\n(names = implicit schema)"] + end + subgraph three ["@pmndrs/glyph/three"] + policy["render-policy.ts\nBITMAP_COLOR = 5\n(ids = declared here)"] + exec["engine-plan-target.ts\nbyPolicyId.get(1)\n(ids = known again, by hand)"] + material["material wiring\n_pmndrsText_5 → color node\n(ids = known a third time)"] + end + subgraph bakers ["@pmndrs/glyph/bakers/bitmap"] + baker["baker + validator\n(artifact schema)"] + end + contract -.-> binding + binding -. "field order must match" .-> policy + policy -. "buffer ids must match" .-> exec + policy -. "buffer ids must match" .-> material + shader -. "lane meaning must match" .-> policy + baker -.-> contract +``` + +Every dotted edge is an agreement with no owner. The D-250 DSL fixed this _inside_ a policy program; the same +disease persists at every seam between packages. The decoration slice's sRGB bug and the gather-row leak both grew +in these seams: the information existed, but no single artifact carried it. + +## Boundaries: subpaths are the reasoning units + +The rule this plan adopts: **you can reason about the software path by path, and each subpath is a tree-shaking +point.** A subpath owns its concepts, exports its contracts, and consumes other subpaths only through their public +entries — enforced by lint (D-249) and measured by per-subpath size entries. + +| subpath | owns | must never know | +| ------------------------- | ---------------------------------------------------------------------- | ----------------------------------------- | +| `@pmndrs/glyph` | fonts, text, styles, runtime | GPUs, shaders, plans | +| `@pmndrs/glyph/core` | engine host, frame wire, plan view, policy authoring, binding compiler | any renderer, any shader language | +| `@pmndrs/glyph/raster/` | **the technique declaration** (this plan's construct) | Three, TSL node graphs | +| `@pmndrs/glyph/tsl` | TSL realizations of declared shader interfaces | buffer ids, binding field order | +| `@pmndrs/glyph/three` | scene lifecycle, plan execution, material realization | field meanings beyond the declared schema | +| `@pmndrs/glyph/bakers/` | artifact production and validation | rendering | + +## The construct: one technique, one declaration + +The authority question — "if the shaders define it, we need a construct for shaders and buffers; if the engine +defines it, it goes there" — resolves to neither: **the technique defines it**, because the schema is precisely +the meeting point of what the technique's programs produce and what its shaders consume. The engine stays agnostic +(it validates shape and carries ids opaquely — correct today, unchanged). The construct: + +```ts +// @pmndrs/glyph/raster/bitmap — the ONLY place bitmap's shape is stated. +export const bitmapSchema = defineTechniqueSchema({ + technique: 'pmndrs.bitmap', + scope: 'strike', + binding: { + f32: ['bearingX', 'bearingY', 'width', 'height', 'uvOriginX', 'uvOriginY', 'uvSizeX', 'uvSizeY'], + u32: ['page'], + }, + buffers: { + origin: { id: 1, scalar: 'f32', lanes: ['inlineOrigin', 'blockOrigin'] }, + size: { id: 2, scalar: 'f32', lanes: ['width', 'height'] }, + uvOrigin: { id: 3, scalar: 'f32', lanes: ['u', 'v'] }, + uvSize: { id: 4, scalar: 'f32', lanes: ['uSpan', 'vSpan'] }, + color: { id: 5, scalar: 'f32', lanes: ['red', 'green', 'blue', 'alpha'] }, + page: { id: 6, scalar: 'u32', lanes: ['page'] }, + }, + resources: { atlas: { kind: 'texture-array', format: 'r8unorm' } }, +}); +``` + +Everything else _derives_ from the schema instead of restating it: + +```ts +// Policy program (any renderer's policy file): stores take schema buffers, not integers. +p.store(bitmapSchema.buffers.origin, [left, top]); + +// Binding compiler: fields are declared by the same names the program loads. +compileFontBinding(bitmapSchema, { bearingX: (row) => …, uvOriginX: (row) => … }); + +// Executor: lookups are named, not remembered. +buffers.get(bitmapSchema.buffers.origin); // replaces byPolicyId.get(1) + +// Shader interface: the technique declares WHAT a shader receives and must return — +// independent of shader language. TSL and TypeGPU are two realizations of one interface. +export interface BitmapShaderInterface { + instance: SchemaNodes; // typed per-buffer/lane nodes + resources: { atlas: TextureHandle }; + output: { position; color; opacity; coverage }; +} +``` + +A wrong id, a renamed lane, or a program/shader mismatch becomes a type error at the declaration site — the same +move D-250 made for registers, applied to the seams. + +## Shader authority: who owns the data under TypeGPU + +With the schema owned by the technique, the shader-language question becomes small: **a shader library owns only +the realization of a declared interface.** `@pmndrs/glyph/tsl` implements `BitmapShaderInterface` with TSL nodes; +a future `@pmndrs/glyph/typegpu` implements the same interface with TypeGPU; both import the technique's schema and +neither owns any data. The data path (binding tables, storage buffers, patch application) is core + the renderer +integration; the shader receives typed views it did not define. This is the answer typegpu-first-shader-authority +needs and the reason the shader library moved out of `three/`: shader realizations are per-language, schemas are +per-technique, and they must not be the same file. + +## Data origin: the axis the contract is missing + +`decode(font, raster: RegisteredRaster)` hard-codes one origin: an artifact raster entry. The offscreen-canvas +technique — rasterize glyphs with the platform text stack, render from that texture — is rendering-expressible +today but **data-inexpressible**: there is no artifact entry to resolve. The contract gains an explicit origin +axis: + +| origin | source | examples | +| ----------------- | ---------------------------------------- | ------------------------------ | +| `artifact` | raster entry in the baked GLB | Bitmap, MSDF, Slug today | +| `worker-baked` | runtime bake producing a GLB | existing opt-in path | +| `runtime-sourced` | technique-supplied provider, no artifact | the planned `pretext` fallback | + +`runtime-sourced` techniques implement `provide(font, signal) → Data` instead of `decode`; the loader treats the +provider as the raster resolution step. + +### The planned `pretext` technique + +The canonical `runtime-sourced` case, named now so the contract is designed against it: **pretext** is the +old-school fallback — the browser shapes and renders whole lines to an offscreen canvas, and rendering samples +line UVs from that texture. It bypasses the Wasm shaper entirely, for consumers who do not want to pay for full +shaping. That makes it the most demanding test of the contract on two axes at once: records are per-line rather +than per-glyph (the schema construct must not assume glyph granularity), and its data origin is pure runtime. Not +required today; this plan reserves the name `pmndrs.pretext` and keeps both axes explicit so pretext lands as a +technique declaration, not a special case. + +## Build: bundle the boundaries we reason about + +Sizes are currently measured over `tsc`-emitted files, so comments and identifier length leak into raw +measurements (the D-250 budget bump was comment growth, not code). The dist should be what we measure and ship: + +- **tsdown** bundles each export-map entry to one ESM file with source maps; comments stripped from output. +- The export map keeps exactly the boundary table above — one bundle per subpath, so the size entries measure the + true tree-shaking units and `raw ≈ minified` stops lying about growth. +- Declaration output remains per-entry `.d.ts` (the dist-declarations gate stays). + +## DSL: more functional, not bigger + +The policy DSL stays the authoring layer, tightened along the review's direction: free combinators over builder +methods where the seams allow (`store(schemaBuffer, values)` as a pure description, `compileProgram(schema, +stores)` as the single effectful step), and schema-typed stores replacing raw buffer integers. The size cost of +the DSL is then carried by `core` only; renderers that register a precompiled policy byte blob ship none of it. + +## Proof obligations + +The contract is sound when these hold, each as a permanent gate: + +1. **Byte goldens + decoded equivalence** (exists, D-250): schema-derived programs compile to pinned bytes; the + equivalence decoder proves dataflow when goldens re-pin. +2. **Subpath isolation sizes** (exists, D-249): per-subpath bundles with graph assertions — core pulls no + renderer, tsl pulls no scene integration. +3. **The external example compiles from the contract alone**: `glyph-example-raster` is rebuilt on the construct + and must import nothing undocumented — it _is_ the documentation's test. +4. **The canvas technique exists**: a `runtime-sourced` technique rendering platform-rasterized glyphs, proving + the data-origin axis end-to-end in the browser lane. +5. **The schema is the only witness**: grep-level gate — no `_pmndrsText_`, no `byPolicyId.get()`, + no parallel id consts outside technique declarations, no literal-width buffer builders outside core, no literal + id ranges mapped into buffer lookups, and no restated system-buffer ids. + +## Migration stack + +Dependency-ordered layers, each green standalone: + +1. ✅ `core`: `defineTechniqueSchema` + schema-typed `store`/binding/lookup APIs. Landed with the D-250/D-251 + stack, then hardened by the adversarial-review closure: schemas deep-freeze at definition, DSL values carry + session provenance (a value from one program throws when stored into another), and `schemaPolicyBuffers` / + `schemaFieldTable` derive wire buffer lists and binding-table order from the declaration. +2. ✅ `raster/`: bitmap, msdf, slug, decoration schemas declared and consumed by programs, binding compilers, + and the executor; hardcoded id ranges, positional field tables, and width lists are gone, and the byte goldens + stayed pinned — the derivations reproduce the hand-rolled bytes exactly. Schemas also carry opt-in + `glyphOrigin` metadata naming the buffer whose first two lanes hold the glyph origin; the executor augments + only techniques that declare it instead of assuming Bitmap's layout everywhere. +3. `tsl`: shader interfaces derived from schemas; TSL graphs become explicit realizations. +4. `glyph-example-raster` rebuilt on the construct; docs rewritten from it. +5. `runtime-sourced` origin + canvas-texture technique with a browser proof. +6. Build: tsdown bundling, comment-stripped dist, per-subpath size re-pinning. + +Each layer updates this document's status and the decision register; the roadmap's 11.8–11.10 items consume the +result (TypeGPU realizations and the external-package proof both sit directly on this contract). diff --git a/docs/planning/tooling-fixtures.md b/docs/planning/tooling-fixtures.md index fd2ce102..9519138a 100644 --- a/docs/planning/tooling-fixtures.md +++ b/docs/planning/tooling-fixtures.md @@ -49,7 +49,7 @@ Use one statically selected, redistributable OpenType font for the first complet | Exact source, version, license, and SHA-256 | ✅ | The checked-in manifest binds the upstream release/commit, archive member, OFL-1.1 text, byte sizes, and hashes. | | Portable baker local real-font lane | ✅ | The package E2E verifies the canonical bytes and cannot skip or substitute an environment font. | | Required pull-request real-font lane | ✅ | The checked-in font and license run without network access or ambient machine state. | -| Benchmark-app product scenario | ✅ | Interactive and browser-headless paths run the canonical bytes through `@pmndrs/glyph/bake`; local upload is an explicit override. | +| Benchmark-app product scenario | ✅ | Interactive and browser-headless paths run the canonical bytes through `@pmndrs/glyph/bake`; local upload is an explicit override. | | Font-baker fuzzing | ✅ | Fixed-seed Rust and validator-mutation smoke tests run hermetically; longer mutation drivers and pinned cargo-fuzz/libFuzzer exercise the public boundaries, with minimized failures promoted into the checked-in malformed corpus. | | GLB-to-HarfRust shaping | ✅ | Canonical Inter is independently validated, registered through `FontRegistry`, contributes exactly its retained 147,192-byte SFNT, 23,496-byte extents, and 368-byte availability views, and matches every pinned HarfRust field through both public batch calls. | | Complex-script source/GLB equivalence | ✅ | Amiri Regular 1.002 is pinned by immutable Google Fonts and upstream commits. Source HarfRust equals GLB-extracted reduced-SFNT HarfRust exactly; pinned HarfBuzz 13 independently agrees on every Arabic/Latin glyph field. | diff --git a/packages/glyph/src/core.ts b/packages/glyph/src/core.ts index d0188760..0f438f6a 100644 --- a/packages/glyph/src/core.ts +++ b/packages/glyph/src/core.ts @@ -45,6 +45,7 @@ export { TextEngineRenderPlanView, type RenderPlanTable } from './core/plan-view export { readTextEngineLayouts, readTextEngineMeasurements } from './core/layout-query-view.js'; export { compileFontBinding, + schemaFieldTable, emptyFontBindingTable, loadedFontBindingBytes, fontBindingResources, @@ -74,4 +75,33 @@ export { type PolicyTransformMode, type ProgramContext, } from './core/render-policy.js'; +export { + definePolicyBuffers, + defineTechniqueSchema, + schemaPolicyBuffers, + type PolicyBufferDeclaration, + type PolicyBufferDeclarations, + type PolicyScalarKind, + type TechniqueBindingDeclaration, + type TechniqueResourceDeclaration, + type TechniqueSchema, + type TechniqueSchemaDeclaration, +} from './core/technique-schema.js'; +export { + addF32, + constantF32, + constantU32, + multiplyF32, + policyProgram, + techniqueProgram, + subtractF32, + u32ToF32, + type CompiledPolicyProgramBody, + type PolicyColorChannels, + type PolicyF32Value, + type PolicyProgramBuilder, + type PolicyProgramOptions, + type PolicyProgramSemantics, + type PolicyU32Value, +} from './core/policy-program.js'; export { textShaperAbi } from './generated/text-shaper-abi.js'; diff --git a/packages/glyph/src/core/font-binding.ts b/packages/glyph/src/core/font-binding.ts index 08781367..3d207f94 100644 --- a/packages/glyph/src/core/font-binding.ts +++ b/packages/glyph/src/core/font-binding.ts @@ -1,8 +1,8 @@ import { textShaperAbi } from '../generated/text-shaper-abi.js'; import type { LoadedFont } from '../loaded-font.js'; -import { bitmap, type BitmapData } from '../raster/bitmap-technique.js'; -import { msdf, type MsdfData } from '../raster/msdf.js'; -import { slug, type SlugData } from '../raster/slug-technique.js'; +import { bitmap, bitmapSchema, type BitmapData } from '../raster/bitmap-technique.js'; +import { msdf, msdfSchema, type MsdfData } from '../raster/msdf.js'; +import { slug, slugSchema, type SlugData } from '../raster/slug-technique.js'; import type { AnyRasterTechnique, RasterResourceId } from '../raster-technique.js'; import { RenderWireIdentityRegistry, type TechniqueWireIds } from './render-policy.js'; @@ -23,6 +23,19 @@ export interface FontBindingFieldTable { readonly fields: readonly ((row: number) => number)[]; } +/** + * Order a binding table by the schema's declared field names. The same name + * list drives the policy program's input table, so a missing, extra, or + * misspelled reader is a compile error instead of a silently shifted column. + */ +export function schemaFieldTable( + names: Names, + rows: number, + readers: { readonly [Name in Names[number]]: (row: number) => number }, +): FontBindingFieldTable { + return { rows, fields: names.map((name: Names[number]) => readers[name]) }; +} + export interface FontBindingDescriptor { readonly techniqueId: number; readonly programVariant: number; @@ -143,44 +156,36 @@ function compileBitmap( }, glyphF32: emptyFontBindingTable(glyphCount), glyphU32: emptyFontBindingTable(glyphCount), - strikeF32: { - rows, - fields: [ - (row) => { - const { view, record, strike } = strikeRecord(row); - return view.getInt16(record, true) / data.strikes[strike]!.planeUnitsPerEm; - }, - (row) => { - const { view, record, strike } = strikeRecord(row); - return view.getInt16(record + 6, true) / data.strikes[strike]!.planeUnitsPerEm; - }, - (row) => { - const { view, record, strike } = strikeRecord(row); - return ( - (view.getInt16(record + 4, true) - view.getInt16(record, true)) / data.strikes[strike]!.planeUnitsPerEm - ); - }, - (row) => { - const { view, record, strike } = strikeRecord(row); - return ( - (view.getInt16(record + 6, true) - view.getInt16(record + 2, true)) / data.strikes[strike]!.planeUnitsPerEm - ); - }, - (row) => atlas(row, 8, 'width'), - (row) => atlas(row, 10, 'height'), - (row) => span(row, 8, 12, 'width'), - (row) => span(row, 10, 14, 'height'), - ], - }, - strikeU32: { - rows, - fields: [ - (row) => { - const { view, record } = strikeRecord(row); - return view.getUint16(record + 16, true); - }, - ], - }, + strikeF32: schemaFieldTable(bitmapSchema.binding.f32, rows, { + bearingX: (row) => { + const { view, record, strike } = strikeRecord(row); + return view.getInt16(record, true) / data.strikes[strike]!.planeUnitsPerEm; + }, + bearingY: (row) => { + const { view, record, strike } = strikeRecord(row); + return view.getInt16(record + 6, true) / data.strikes[strike]!.planeUnitsPerEm; + }, + width: (row) => { + const { view, record, strike } = strikeRecord(row); + return (view.getInt16(record + 4, true) - view.getInt16(record, true)) / data.strikes[strike]!.planeUnitsPerEm; + }, + height: (row) => { + const { view, record, strike } = strikeRecord(row); + return ( + (view.getInt16(record + 6, true) - view.getInt16(record + 2, true)) / data.strikes[strike]!.planeUnitsPerEm + ); + }, + uvOriginX: (row) => atlas(row, 8, 'width'), + uvOriginY: (row) => atlas(row, 10, 'height'), + uvSizeX: (row) => span(row, 8, 12, 'width'), + uvSizeY: (row) => span(row, 10, 14, 'height'), + }), + strikeU32: schemaFieldTable(bitmapSchema.binding.u32, rows, { + page: (row) => { + const { view, record } = strikeRecord(row); + return view.getUint16(record + 16, true); + }, + }), resourceF32: emptyFontBindingTable(resources.length), resourceU32: emptyFontBindingTable(resources.length), }); @@ -216,23 +221,21 @@ function compileMsdf( resourceIndex(row) { return pageAt(row) === ABSENT_PAGE ? MISSING_RESOURCE : indexFor(data.resource); }, - glyphF32: { - rows: glyphCount, - fields: [ - (row) => view.getInt16(rowRecord(row), true) / data.planeUnitsPerEm, - (row) => view.getInt16(rowRecord(row) + 6, true) / data.planeUnitsPerEm, - (row) => (view.getInt16(rowRecord(row) + 4, true) - view.getInt16(rowRecord(row), true)) / data.planeUnitsPerEm, - (row) => - (view.getInt16(rowRecord(row) + 6, true) - view.getInt16(rowRecord(row) + 2, true)) / data.planeUnitsPerEm, - (row) => atlas(row, 8, 'width'), - (row) => atlas(row, 10, 'height'), - (row) => span(row, 8, 12, 'width'), - (row) => span(row, 10, 14, 'height'), - (row) => atlas(row, 12, 'width'), - (row) => atlas(row, 14, 'height'), - ], - }, - glyphU32: { rows: glyphCount, fields: [(row) => pageAt(row)] }, + glyphF32: schemaFieldTable(msdfSchema.binding.f32, glyphCount, { + bearingX: (row) => view.getInt16(rowRecord(row), true) / data.planeUnitsPerEm, + bearingY: (row) => view.getInt16(rowRecord(row) + 6, true) / data.planeUnitsPerEm, + width: (row) => + (view.getInt16(rowRecord(row) + 4, true) - view.getInt16(rowRecord(row), true)) / data.planeUnitsPerEm, + height: (row) => + (view.getInt16(rowRecord(row) + 6, true) - view.getInt16(rowRecord(row) + 2, true)) / data.planeUnitsPerEm, + uvOriginX: (row) => atlas(row, 8, 'width'), + uvOriginY: (row) => atlas(row, 10, 'height'), + uvSizeX: (row) => span(row, 8, 12, 'width'), + uvSizeY: (row) => span(row, 10, 14, 'height'), + uvMaxX: (row) => atlas(row, 12, 'width'), + uvMaxY: (row) => atlas(row, 14, 'height'), + }), + glyphU32: schemaFieldTable(msdfSchema.binding.u32, glyphCount, { page: (row) => pageAt(row) }), strikeF32: emptyFontBindingTable(glyphCount), strikeU32: emptyFontBindingTable(glyphCount), resourceF32: emptyFontBindingTable(resources.length), @@ -271,30 +274,24 @@ function compileSlug( const page = pageAt(row); return page === ABSENT_PAGE ? MISSING_RESOURCE : indexFor(data.pages[page]!.resource); }, - glyphF32: { - rows: glyphCount, - fields: [ - (row) => normalized(row, 0), - (row) => normalized(row, 6), - (row) => width(row), - (row) => height(row), - (row) => bandScaleX(row), - (row) => bandScaleY(row), - (row) => -normalized(row, 0) * bandScaleX(row), - (row) => -normalized(row, 2) * bandScaleY(row), - ], - }, - glyphU32: { - rows: glyphCount, - fields: [ - (row) => view.getUint32(record(row) + 16, true), - (row) => view.getUint32(record(row) + 24, true), - (row) => view.getUint32(record(row) + 28, true), - (row) => view.getUint32(record(row) + 32, true), - (row) => horizontalBands(row), - (row) => verticalBands(row), - ], - }, + glyphF32: schemaFieldTable(slugSchema.binding.f32, glyphCount, { + bearingX: (row) => normalized(row, 0), + bearingY: (row) => normalized(row, 6), + width: (row) => width(row), + height: (row) => height(row), + bandScaleX: (row) => bandScaleX(row), + bandScaleY: (row) => bandScaleY(row), + bandOffsetX: (row) => -normalized(row, 0) * bandScaleX(row), + bandOffsetY: (row) => -normalized(row, 2) * bandScaleY(row), + }), + glyphU32: schemaFieldTable(slugSchema.binding.u32, glyphCount, { + curveStart: (row) => view.getUint32(record(row) + 16, true), + headerStart: (row) => view.getUint32(record(row) + 24, true), + referenceStart: (row) => view.getUint32(record(row) + 28, true), + bandStart: (row) => view.getUint32(record(row) + 32, true), + horizontalBands: (row) => horizontalBands(row), + verticalBands: (row) => verticalBands(row), + }), strikeF32: emptyFontBindingTable(glyphCount), strikeU32: emptyFontBindingTable(glyphCount), resourceF32: emptyFontBindingTable(resources.length), diff --git a/packages/glyph/src/core/policy-program.ts b/packages/glyph/src/core/policy-program.ts new file mode 100644 index 00000000..bdff5cab --- /dev/null +++ b/packages/glyph/src/core/policy-program.ts @@ -0,0 +1,353 @@ +import { textShaperAbi } from '../generated/text-shaper-abi.js'; +import type { PolicyInput, PolicyInputScope, PolicyOperation } from './render-policy.js'; +import type { PolicyBufferDeclaration, TechniqueSchema } from './technique-schema.js'; + +/** + * Expression DSL over the policy-program register machine. Authors reference named + * values instead of register numbers; `compile()` lowers the expression graph to the + * same forward-only `PolicyOperation` records the hand-numbered form produced, + * allocating registers automatically and failing loudly on exhaustion. The wire + * format, validator, and interpreter are untouched — this is authoring only. + */ + +const MAX_REGISTERS = 32; + +type Node = + | { readonly kind: 'loadF32'; readonly input: number; readonly label: string; readonly session: object } + | { readonly kind: 'loadU32'; readonly input: number; readonly label: string; readonly session: object } + | { + readonly kind: 'binary'; + readonly op: 'addF32' | 'subtractF32' | 'multiplyF32'; + readonly left: Node; + readonly right: Node; + readonly session: object | undefined; + } + | { readonly kind: 'constantF32'; readonly value: number; readonly session: undefined } + | { readonly kind: 'constantU32'; readonly value: number; readonly session: undefined } + | { readonly kind: 'convertU32ToF32'; readonly source: Node; readonly session: object | undefined }; + +declare const f32Brand: unique symbol; +declare const u32Brand: unique symbol; + +/** A named or derived f32 value inside one policy program. */ +export interface PolicyF32Value { + readonly [f32Brand]: true; +} + +/** A named or derived u32 value inside one policy program. */ +export interface PolicyU32Value { + readonly [u32Brand]: true; +} + +const nodes = new WeakMap(); + +function f32Value(node: Node): PolicyF32Value { + const value = {} as PolicyF32Value; + nodes.set(value, node); + return value; +} + +function u32Value(node: Node): PolicyU32Value { + const value = {} as PolicyU32Value; + nodes.set(value, node); + return value; +} + +function nodeOf(value: PolicyF32Value | PolicyU32Value): Node { + const node = nodes.get(value); + if (node === undefined) throw new TypeError('policy value does not belong to this authoring session'); + return node; +} + +/** + * A loaded value's input index only means something inside the program that + * created it; storing it elsewhere would silently read a different field. + * Provenance is stamped at construction and combined in O(1) per node, so + * shared expression DAGs never require a graph walk: constants stay + * session-free, and mixing two sessions fails at the combinator itself. + */ +function combinedSession(left: Node, right: Node): object | undefined { + if (left.session !== undefined && right.session !== undefined && left.session !== right.session) { + throw new TypeError('policy values from different authoring sessions cannot combine'); + } + return left.session ?? right.session; +} + +function assertSession(node: Node, session: object): void { + if (node.session !== undefined && node.session !== session) { + throw new TypeError('policy value belongs to a different authoring session'); + } +} + +export function addF32(left: PolicyF32Value, right: PolicyF32Value): PolicyF32Value { + const leftNode = nodeOf(left); + const rightNode = nodeOf(right); + return f32Value({ + kind: 'binary', + op: 'addF32', + left: leftNode, + right: rightNode, + session: combinedSession(leftNode, rightNode), + }); +} + +export function subtractF32(left: PolicyF32Value, right: PolicyF32Value): PolicyF32Value { + const leftNode = nodeOf(left); + const rightNode = nodeOf(right); + return f32Value({ + kind: 'binary', + op: 'subtractF32', + left: leftNode, + right: rightNode, + session: combinedSession(leftNode, rightNode), + }); +} + +export function multiplyF32(left: PolicyF32Value, right: PolicyF32Value): PolicyF32Value { + const leftNode = nodeOf(left); + const rightNode = nodeOf(right); + return f32Value({ + kind: 'binary', + op: 'multiplyF32', + left: leftNode, + right: rightNode, + session: combinedSession(leftNode, rightNode), + }); +} + +export function u32ToF32(source: PolicyU32Value): PolicyF32Value { + const sourceNode = nodeOf(source); + return f32Value({ kind: 'convertU32ToF32', source: sourceNode, session: sourceNode.session }); +} + +export function constantF32(value: number): PolicyF32Value { + if (!Number.isFinite(value)) throw new RangeError('policy f32 constants must be finite'); + return f32Value({ kind: 'constantF32', value, session: undefined }); +} + +export function constantU32(value: number): PolicyU32Value { + if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff_ffff) { + throw new RangeError('policy u32 constants must be u32'); + } + return u32Value({ kind: 'constantU32', value, session: undefined }); +} + +/** The glyph color channels — the resolved paint; the engine has no background. */ +export interface PolicyColorChannels { + readonly red: PolicyF32Value; + readonly green: PolicyF32Value; + readonly blue: PolicyF32Value; + readonly alpha: PolicyF32Value; +} + +export interface PolicyProgramSemantics { + readonly inlineOrigin: PolicyF32Value; + readonly blockOrigin: PolicyF32Value; + readonly fontSize: PolicyF32Value; + readonly color: PolicyColorChannels; + readonly inverseFontSize: PolicyF32Value | undefined; + readonly transformIndex: PolicyU32Value; + readonly stableGlyphId: PolicyU32Value; +} + +export interface PolicyProgramOptions< + F32 extends readonly string[] = readonly string[], + U32 extends readonly string[] = readonly string[], +> { + readonly scope: PolicyInputScope; + readonly bindingF32?: F32; + readonly bindingU32?: U32; + readonly inverseFontSize?: boolean; +} + +export interface CompiledPolicyProgramBody { + readonly inputs: PolicyInput[]; + readonly operations: PolicyOperation[]; + readonly f32InputCount: number; + readonly u32InputCount: number; +} + +export interface PolicyProgramBuilder { + readonly semantics: PolicyProgramSemantics; + readonly binding: Readonly & Record>; + /** Store into a declared buffer; value kinds and lane count come from the declaration. */ + store( + buffer: Buffer, + lanes: Buffer['scalar'] extends 'f32' ? readonly PolicyF32Value[] : readonly PolicyU32Value[], + ): void; + storeF32(buffer: number, lanes: readonly PolicyF32Value[]): void; + storeU32(buffer: number, lanes: readonly PolicyU32Value[]): void; + compile(): CompiledPolicyProgramBody; +} + +type BindingNames = Names extends readonly string[] ? Names : readonly []; + +interface StoreRecord { + readonly opcode: number; + readonly buffer: number; + readonly lane: number; + readonly node: Node; +} + +/** Build a program against one technique's authoritative schema. */ +export function techniqueProgram< + const Buffers extends import('./technique-schema.js').PolicyBufferDeclarations, + const Binding extends import('./technique-schema.js').TechniqueBindingDeclaration, +>( + schema: TechniqueSchema, + options: { readonly inverseFontSize?: boolean } = {}, +): PolicyProgramBuilder, BindingNames> { + return policyProgram({ + scope: schema.scope, + bindingF32: (schema.binding.f32 ?? []) as BindingNames, + bindingU32: (schema.binding.u32 ?? []) as BindingNames, + ...(options.inverseFontSize === undefined ? {} : { inverseFontSize: options.inverseFontSize }), + }); +} + +export function policyProgram< + const F32 extends readonly string[] = readonly [], + const U32 extends readonly string[] = readonly [], +>(options: PolicyProgramOptions): PolicyProgramBuilder { + const semanticF32 = textShaperAbi.engine.semanticF32Fields; + const semanticU32 = textShaperAbi.engine.semanticU32Fields; + const bindingF32Names = options.bindingF32 ?? []; + const bindingU32Names = options.bindingU32 ?? []; + const uniqueNames = new Set([...bindingF32Names, ...bindingU32Names]); + if (uniqueNames.size !== bindingF32Names.length + bindingU32Names.length) { + throw new TypeError('policy binding field names must be unique'); + } + + // The input table mirrors the canonical order the engine validated all along: + // seven semantic f32 fields, optional inverseFontSize, then the binding's f32 + // fields; transformIndex and stableGlyphId, then the binding's u32 fields. + const inputs: PolicyInput[] = [ + { scope: 'semantic', field: semanticF32.inlineOrigin }, + { scope: 'semantic', field: semanticF32.blockOrigin }, + { scope: 'semantic', field: semanticF32.fontSize }, + { scope: 'semantic', field: semanticF32.foregroundRed }, + { scope: 'semantic', field: semanticF32.foregroundGreen }, + { scope: 'semantic', field: semanticF32.foregroundBlue }, + { scope: 'semantic', field: semanticF32.foregroundAlpha }, + ...(options.inverseFontSize === true ? [{ scope: 'semantic' as const, field: semanticF32.inverseFontSize }] : []), + ...bindingF32Names.map((_, field) => ({ scope: options.scope, field })), + { scope: 'semantic', field: semanticU32.transformIndex }, + { scope: 'semantic', field: semanticU32.stableGlyphId }, + ...bindingU32Names.map((_, field) => ({ scope: options.scope, field })), + ]; + const f32InputCount = 7 + (options.inverseFontSize === true ? 1 : 0) + bindingF32Names.length; + const u32InputCount = 2 + bindingU32Names.length; + + const session = {}; + let nextF32 = 0; + const loadF32 = (label: string): PolicyF32Value => f32Value({ kind: 'loadF32', input: nextF32++, label, session }); + const semantics: PolicyProgramSemantics = { + inlineOrigin: loadF32('inlineOrigin'), + blockOrigin: loadF32('blockOrigin'), + fontSize: loadF32('fontSize'), + color: { + red: loadF32('color.red'), + green: loadF32('color.green'), + blue: loadF32('color.blue'), + alpha: loadF32('color.alpha'), + }, + inverseFontSize: options.inverseFontSize === true ? loadF32('inverseFontSize') : undefined, + transformIndex: u32Value({ kind: 'loadU32', input: 0, label: 'transformIndex', session }), + stableGlyphId: u32Value({ kind: 'loadU32', input: 1, label: 'stableGlyphId', session }), + }; + const binding: Record = {}; + for (const name of bindingF32Names) binding[name] = loadF32(name); + for (const [index, name] of bindingU32Names.entries()) { + binding[name] = u32Value({ kind: 'loadU32', input: 2 + index, label: name, session }); + } + + const stores: StoreRecord[] = []; + const opcodes = textShaperAbi.policy.opcodes; + + return { + semantics, + binding: binding as PolicyProgramBuilder['binding'], + store(buffer, lanes) { + if (lanes.length !== buffer.lanes.length) { + throw new RangeError( + `buffer ${buffer.id} declares ${buffer.lanes.length} lanes (${buffer.lanes.join(', ')}); got ${lanes.length} values`, + ); + } + const opcode = buffer.scalar === 'f32' ? opcodes.storeF32 : opcodes.storeU32; + for (const [lane, value] of lanes.entries()) { + const node = nodeOf(value); + assertSession(node, session); + stores.push({ opcode, buffer: buffer.id, lane, node }); + } + }, + storeF32(buffer, lanes) { + for (const [lane, value] of lanes.entries()) { + const node = nodeOf(value); + assertSession(node, session); + stores.push({ opcode: opcodes.storeF32, buffer, lane, node }); + } + }, + storeU32(buffer, lanes) { + for (const [lane, value] of lanes.entries()) { + const node = nodeOf(value); + assertSession(node, session); + stores.push({ opcode: opcodes.storeU32, buffer, lane, node }); + } + }, + compile() { + const operations: PolicyOperation[] = []; + const registers = new Map(); + const emit = (node: Node): number => { + const assigned = registers.get(node); + if (assigned !== undefined) return assigned; + let operation: PolicyOperation; + switch (node.kind) { + case 'loadF32': + operation = { opcode: opcodes.loadF32, target: 0, operand0: node.input }; + break; + case 'loadU32': + operation = { opcode: opcodes.loadU32, target: 0, operand0: node.input }; + break; + case 'binary': { + const left = emit(node.left); + const right = emit(node.right); + operation = { opcode: opcodes[node.op], target: 0, operand0: left, operand1: right }; + break; + } + case 'constantF32': + operation = { opcode: opcodes.constantF32, target: 0, immediate0: f32Bits(node.value) }; + break; + case 'constantU32': + operation = { opcode: opcodes.constantU32, target: 0, immediate0: node.value }; + break; + case 'convertU32ToF32': { + const source = emit(node.source); + operation = { opcode: opcodes.convertU32ToF32, target: 0, operand0: source }; + break; + } + } + const register = registers.size; + if (register >= MAX_REGISTERS) { + throw new RangeError( + `policy program needs more than ${MAX_REGISTERS} registers; name intermediate values and reuse them`, + ); + } + registers.set(node, register); + operations.push({ ...operation, target: register }); + return register; + }; + for (const store of stores) { + const register = emit(store.node); + operations.push({ opcode: store.opcode, operand0: register, operand1: store.lane, immediate0: store.buffer }); + } + return { inputs, operations, f32InputCount, u32InputCount }; + }, + }; +} + +function f32Bits(value: number): number { + const bytes = new ArrayBuffer(4); + const view = new DataView(bytes); + view.setFloat32(0, value, true); + return view.getUint32(0, true); +} diff --git a/packages/glyph/src/core/render-policy.ts b/packages/glyph/src/core/render-policy.ts index b44662be..dd496d0e 100644 --- a/packages/glyph/src/core/render-policy.ts +++ b/packages/glyph/src/core/render-policy.ts @@ -195,10 +195,17 @@ export function programContext( }; } +export interface ProgramBody { + readonly inputs: PolicyInput[]; + readonly operations: PolicyOperation[]; + readonly f32InputCount: number; + readonly u32InputCount: number; +} + export function createProgram( techniqueId: number, programId: number, - context: ProgramContext, + context: ProgramBody, buffers: readonly PolicyBuffer[], transformMode: PolicyTransformMode, allocationMode: PolicyAllocationMode, diff --git a/packages/glyph/src/core/technique-schema.ts b/packages/glyph/src/core/technique-schema.ts new file mode 100644 index 00000000..2be18219 --- /dev/null +++ b/packages/glyph/src/core/technique-schema.ts @@ -0,0 +1,157 @@ +/** + * The single authority for a raster technique's physical shape. A schema declares — + * once, colocated with the technique — the buffer ids, scalar kinds, and lane + * meanings that its policy programs produce and its shader realizations consume. + * Policy stores, binding compilers, plan executors, and shader interfaces all + * derive from the declaration; none of them restate it. + */ + +import { textShaperAbi } from '../generated/text-shaper-abi.js'; +import type { PolicyBuffer } from './render-policy.js'; + +export type PolicyScalarKind = 'f32' | 'u32'; + +export interface PolicyBufferDeclaration { + /** Wire buffer id — nonzero, unique within the owning program. */ + readonly id: number; + readonly scalar: PolicyScalarKind; + /** One name per lane; the lane count is the buffer's vector width. */ + readonly lanes: readonly string[]; +} + +export type PolicyBufferDeclarations = Readonly>; + +/** + * Validate and freeze a named buffer set: nonzero unique ids, at least one lane + * each. The result is an owned, deeply frozen copy — caller input is never + * mutated (rejection leaves it untouched), and caller accessors are read once + * here so they can never change a validated width afterwards. + */ +export function definePolicyBuffers(buffers: Buffers): Buffers { + const seen = new Set(); + const owned: Record = {}; + for (const [name, buffer] of Object.entries(buffers)) { + const id = buffer.id; + const scalar = buffer.scalar; + const lanes = [...buffer.lanes]; + if (!Number.isSafeInteger(id) || id <= 0 || id > 0xffff) { + throw new RangeError(`policy buffer "${name}" needs a nonzero u16 id`); + } + if (seen.has(id)) throw new TypeError(`policy buffer "${name}" reuses id ${id}`); + seen.add(id); + if (scalar !== 'f32' && scalar !== 'u32') { + throw new TypeError(`policy buffer "${name}" needs an f32 or u32 scalar kind`); + } + if (lanes.length === 0 || lanes.length > 4) { + throw new RangeError(`policy buffer "${name}" needs one to four named lanes`); + } + owned[name] = Object.freeze({ id, scalar, lanes: Object.freeze(lanes) }); + } + // The copy carries exactly the declared keys read above, so it satisfies Buffers. + return Object.freeze(owned) as Buffers; +} + +export interface TechniqueBindingDeclaration { + readonly f32?: readonly string[]; + readonly u32?: readonly string[]; +} + +export interface TechniqueResourceDeclaration { + readonly kind: string; + readonly format?: string; +} + +export interface TechniqueSchemaDeclaration< + Buffers extends PolicyBufferDeclarations = PolicyBufferDeclarations, + Binding extends TechniqueBindingDeclaration = TechniqueBindingDeclaration, +> { + /** Wire identity string, e.g. `pmndrs.bitmap`. */ + readonly technique: string; + /** Binding input scope the technique's per-glyph data arrives through. */ + readonly scope: 'glyph' | 'strike' | 'resource'; + readonly binding: Binding; + readonly buffers: Buffers; + readonly resources?: Readonly>; + /** + * Opt-in glyph-origin metadata: names the declared f32 buffer whose first two + * lanes carry the glyph's inline/block origin. Renderers that augment glyph + * origins (animation retargeting) consult this instead of assuming a layout; + * techniques without it are never augmented. + */ + readonly glyphOrigin?: { readonly buffer: string }; +} + +export interface TechniqueSchema< + Buffers extends PolicyBufferDeclarations = PolicyBufferDeclarations, + Binding extends TechniqueBindingDeclaration = TechniqueBindingDeclaration, +> extends TechniqueSchemaDeclaration {} + +/** Validate and freeze one technique's authoritative schema. */ +export function defineTechniqueSchema< + const Buffers extends PolicyBufferDeclarations, + const Binding extends TechniqueBindingDeclaration, +>(declaration: TechniqueSchemaDeclaration): TechniqueSchema { + // Read every input property exactly once into owned structures, validate the + // owned data, then freeze and return the copy. Caller input is never mutated + // or frozen — a rejected declaration leaves it exactly as passed — and only + // the declared fields are carried, so no foreign reachable state survives. + const technique = declaration.technique; + if (technique.length === 0) throw new TypeError('technique schemas need a wire identity'); + const scope = declaration.scope; + if (scope !== 'glyph' && scope !== 'strike' && scope !== 'resource') { + throw new TypeError(`technique "${technique}" needs a glyph, strike, or resource binding scope`); + } + const bindingF32 = declaration.binding.f32 === undefined ? undefined : Object.freeze([...declaration.binding.f32]); + const bindingU32 = declaration.binding.u32 === undefined ? undefined : Object.freeze([...declaration.binding.u32]); + const names = [...(bindingF32 ?? []), ...(bindingU32 ?? [])]; + if (new Set(names).size !== names.length) { + throw new TypeError(`technique "${technique}" repeats a binding field name`); + } + const buffers = definePolicyBuffers(declaration.buffers); + let resources: Readonly> | undefined; + if (declaration.resources !== undefined) { + const owned: Record = {}; + for (const [name, resource] of Object.entries(declaration.resources)) { + const format = resource.format; + owned[name] = Object.freeze({ kind: resource.kind, ...(format === undefined ? {} : { format }) }); + } + resources = Object.freeze(owned); + } + let glyphOrigin: { readonly buffer: string } | undefined; + if (declaration.glyphOrigin !== undefined) { + const origin: PolicyBufferDeclaration | undefined = buffers[declaration.glyphOrigin.buffer]; + if (origin === undefined) { + throw new TypeError(`technique "${technique}" points glyphOrigin at an undeclared buffer`); + } + if (origin.scalar !== 'f32' || origin.lanes.length < 2) { + throw new TypeError(`technique "${technique}" needs an f32 glyphOrigin buffer with two origin lanes`); + } + glyphOrigin = Object.freeze({ buffer: declaration.glyphOrigin.buffer }); + } + const binding = Object.freeze({ + ...(bindingF32 === undefined ? {} : { f32: bindingF32 }), + ...(bindingU32 === undefined ? {} : { u32: bindingU32 }), + // The copies carry exactly the declared binding names read above. + }) as Binding; + return Object.freeze({ + technique, + scope, + binding, + buffers, + ...(resources === undefined ? {} : { resources }), + ...(glyphOrigin === undefined ? {} : { glyphOrigin }), + }); +} + +/** + * Derive the wire buffer list a technique's programs publish, in declaration + * order — the schema is the only witness to ids, scalar kinds, and widths. + */ +export function schemaPolicyBuffers(schema: TechniqueSchema): PolicyBuffer[] { + const scalars = textShaperAbi.policy.scalarTypes; + return Object.values(schema.buffers).map((buffer) => ({ + id: buffer.id, + scalar: buffer.scalar === 'f32' ? scalars.f32 : scalars.u32, + vectorWidth: buffer.lanes.length, + })); +} diff --git a/packages/glyph/src/raster/bitmap-technique.ts b/packages/glyph/src/raster/bitmap-technique.ts index d5285a70..54e68845 100644 --- a/packages/glyph/src/raster/bitmap-technique.ts +++ b/packages/glyph/src/raster/bitmap-technique.ts @@ -194,3 +194,50 @@ async function decodeBitmapData(font: RegisteredFont, raster: RegisteredRaster): } return { strikes, ...(coverage === undefined ? {} : { coverage: coverage.bits }) }; } + +import { defineTechniqueSchema, type TechniqueSchema } from '../core/technique-schema.js'; + +/** + * The authoritative physical shape of the Bitmap technique: binding field order matches + * the strike tables the binding compiler emits; buffer ids and lanes are the contract + * every policy program and shader realization derives from. + */ +export const bitmapSchema: TechniqueSchema< + { + readonly origin: { + readonly id: 1; + readonly scalar: 'f32'; + readonly lanes: readonly ['inlineOrigin', 'blockOrigin']; + }; + readonly size: { readonly id: 2; readonly scalar: 'f32'; readonly lanes: readonly ['width', 'height'] }; + readonly uvOrigin: { readonly id: 3; readonly scalar: 'f32'; readonly lanes: readonly ['u', 'v'] }; + readonly uvSize: { readonly id: 4; readonly scalar: 'f32'; readonly lanes: readonly ['uSpan', 'vSpan'] }; + readonly color: { + readonly id: 5; + readonly scalar: 'f32'; + readonly lanes: readonly ['red', 'green', 'blue', 'alpha']; + }; + readonly page: { readonly id: 6; readonly scalar: 'u32'; readonly lanes: readonly ['page'] }; + }, + { + readonly f32: readonly ['bearingX', 'bearingY', 'width', 'height', 'uvOriginX', 'uvOriginY', 'uvSizeX', 'uvSizeY']; + readonly u32: readonly ['page']; + } +> = defineTechniqueSchema({ + technique: 'pmndrs.bitmap', + scope: 'strike', + glyphOrigin: { buffer: 'origin' }, + binding: { + f32: ['bearingX', 'bearingY', 'width', 'height', 'uvOriginX', 'uvOriginY', 'uvSizeX', 'uvSizeY'], + u32: ['page'], + }, + buffers: { + origin: { id: 1, scalar: 'f32', lanes: ['inlineOrigin', 'blockOrigin'] }, + size: { id: 2, scalar: 'f32', lanes: ['width', 'height'] }, + uvOrigin: { id: 3, scalar: 'f32', lanes: ['u', 'v'] }, + uvSize: { id: 4, scalar: 'f32', lanes: ['uSpan', 'vSpan'] }, + color: { id: 5, scalar: 'f32', lanes: ['red', 'green', 'blue', 'alpha'] }, + page: { id: 6, scalar: 'u32', lanes: ['page'] }, + }, + resources: { atlas: { kind: 'texture-array', format: 'r8unorm' } }, +}); diff --git a/packages/glyph/src/raster/msdf.ts b/packages/glyph/src/raster/msdf.ts index 8a4b10e8..5ef59bdc 100644 --- a/packages/glyph/src/raster/msdf.ts +++ b/packages/glyph/src/raster/msdf.ts @@ -203,3 +203,80 @@ function validateMsdfPageDirectory(value: JsonValue, pageIndex: number): void { throw new TypeError('MSDF V0 pages accept only the lossless rgba8unorm baseline'); } } + +import { defineTechniqueSchema, type TechniqueSchema } from '../core/technique-schema.js'; + +/** + * The authoritative physical shape of the MSDF technique. + */ +export const msdfSchema: TechniqueSchema< + { + readonly rect: { + readonly id: 1; + readonly scalar: 'f32'; + readonly lanes: readonly ['left', 'top', 'width', 'height']; + }; + readonly uvRect: { + readonly id: 2; + readonly scalar: 'f32'; + readonly lanes: readonly ['u0', 'v0', 'uSpan', 'vSpan']; + }; + readonly uvBounds: { + readonly id: 3; + readonly scalar: 'f32'; + readonly lanes: readonly ['u0', 'v0', 'uMax', 'vMax']; + }; + readonly color: { + readonly id: 4; + readonly scalar: 'f32'; + readonly lanes: readonly ['red', 'green', 'blue', 'alpha']; + }; + readonly effectA: { readonly id: 5; readonly scalar: 'f32'; readonly lanes: readonly ['x', 'y', 'z', 'w'] }; + readonly effectB: { readonly id: 6; readonly scalar: 'f32'; readonly lanes: readonly ['x', 'y', 'z', 'w'] }; + readonly page: { readonly id: 7; readonly scalar: 'f32'; readonly lanes: readonly ['x', 'y', 'z', 'page'] }; + }, + { + readonly f32: readonly [ + 'bearingX', + 'bearingY', + 'width', + 'height', + 'uvOriginX', + 'uvOriginY', + 'uvSizeX', + 'uvSizeY', + 'uvMaxX', + 'uvMaxY', + ]; + readonly u32: readonly ['page']; + } +> = defineTechniqueSchema({ + technique: 'pmndrs.msdf', + scope: 'glyph', + glyphOrigin: { buffer: 'rect' }, + binding: { + f32: [ + 'bearingX', + 'bearingY', + 'width', + 'height', + 'uvOriginX', + 'uvOriginY', + 'uvSizeX', + 'uvSizeY', + 'uvMaxX', + 'uvMaxY', + ], + u32: ['page'], + }, + buffers: { + rect: { id: 1, scalar: 'f32', lanes: ['left', 'top', 'width', 'height'] }, + uvRect: { id: 2, scalar: 'f32', lanes: ['u0', 'v0', 'uSpan', 'vSpan'] }, + uvBounds: { id: 3, scalar: 'f32', lanes: ['u0', 'v0', 'uMax', 'vMax'] }, + color: { id: 4, scalar: 'f32', lanes: ['red', 'green', 'blue', 'alpha'] }, + effectA: { id: 5, scalar: 'f32', lanes: ['x', 'y', 'z', 'w'] }, + effectB: { id: 6, scalar: 'f32', lanes: ['x', 'y', 'z', 'w'] }, + page: { id: 7, scalar: 'f32', lanes: ['x', 'y', 'z', 'page'] }, + }, + resources: { atlas: { kind: 'texture-array', format: 'rgba8unorm' } }, +}); diff --git a/packages/glyph/src/raster/slug-technique.ts b/packages/glyph/src/raster/slug-technique.ts index 1c935e74..443c58dd 100644 --- a/packages/glyph/src/raster/slug-technique.ts +++ b/packages/glyph/src/raster/slug-technique.ts @@ -347,3 +347,86 @@ function checkedBytes(left: number, right: number): number { } return total; } + +import { defineTechniqueSchema, type TechniqueSchema } from '../core/technique-schema.js'; + +/** + * The authoritative physical shape of the Slug technique. + */ +export const slugSchema: TechniqueSchema< + { + readonly rect: { + readonly id: 1; + readonly scalar: 'f32'; + readonly lanes: readonly ['left', 'top', 'width', 'height']; + }; + readonly planeRect: { + readonly id: 2; + readonly scalar: 'f32'; + readonly lanes: readonly ['left', 'top', 'width', 'height']; + }; + readonly bandTransform: { + readonly id: 3; + readonly scalar: 'f32'; + readonly lanes: readonly ['scaleX', 'scaleY', 'offsetX', 'offsetY']; + }; + readonly color: { + readonly id: 4; + readonly scalar: 'f32'; + readonly lanes: readonly ['red', 'green', 'blue', 'alpha']; + }; + readonly inverseFontSize: { + readonly id: 5; + readonly scalar: 'f32'; + readonly lanes: readonly ['inverseFontSize', 'unused1', 'unused2', 'unused3']; + }; + readonly tableStarts: { + readonly id: 6; + readonly scalar: 'u32'; + readonly lanes: readonly ['curveStart', 'headerStart', 'referenceStart', 'bandStart']; + }; + readonly bandCounts: { + readonly id: 7; + readonly scalar: 'u32'; + readonly lanes: readonly ['horizontalBands', 'verticalBands', 'unused2', 'unused3']; + }; + }, + { + readonly f32: readonly [ + 'bearingX', + 'bearingY', + 'width', + 'height', + 'bandScaleX', + 'bandScaleY', + 'bandOffsetX', + 'bandOffsetY', + ]; + readonly u32: readonly [ + 'curveStart', + 'headerStart', + 'referenceStart', + 'bandStart', + 'horizontalBands', + 'verticalBands', + ]; + } +> = defineTechniqueSchema({ + technique: 'pmndrs.slug', + scope: 'glyph', + glyphOrigin: { buffer: 'rect' }, + binding: { + f32: ['bearingX', 'bearingY', 'width', 'height', 'bandScaleX', 'bandScaleY', 'bandOffsetX', 'bandOffsetY'], + u32: ['curveStart', 'headerStart', 'referenceStart', 'bandStart', 'horizontalBands', 'verticalBands'], + }, + buffers: { + rect: { id: 1, scalar: 'f32', lanes: ['left', 'top', 'width', 'height'] }, + planeRect: { id: 2, scalar: 'f32', lanes: ['left', 'top', 'width', 'height'] }, + bandTransform: { id: 3, scalar: 'f32', lanes: ['scaleX', 'scaleY', 'offsetX', 'offsetY'] }, + color: { id: 4, scalar: 'f32', lanes: ['red', 'green', 'blue', 'alpha'] }, + inverseFontSize: { id: 5, scalar: 'f32', lanes: ['inverseFontSize', 'unused1', 'unused2', 'unused3'] }, + tableStarts: { id: 6, scalar: 'u32', lanes: ['curveStart', 'headerStart', 'referenceStart', 'bandStart'] }, + bandCounts: { id: 7, scalar: 'u32', lanes: ['horizontalBands', 'verticalBands', 'unused2', 'unused3'] }, + }, + resources: { curves: { kind: 'texture' }, headers: { kind: 'texture' }, references: { kind: 'texture' } }, +}); diff --git a/packages/glyph/src/three/engine-plan-target.ts b/packages/glyph/src/three/engine-plan-target.ts index 5371a463..68663494 100644 --- a/packages/glyph/src/three/engine-plan-target.ts +++ b/packages/glyph/src/three/engine-plan-target.ts @@ -2,7 +2,11 @@ import * as TSL from 'three/tsl'; import * as THREE from 'three/webgpu'; import { textShaperAbi } from '../core.js'; -import { STABLE_GLYPH_BUFFER_ID, TRANSFORM_BUFFER_ID } from './render-policy.js'; +import { decorationSchema, threeSystemBuffers } from './render-policy.js'; +import { bitmapSchema } from '../raster/bitmap-technique.js'; +import { msdfSchema } from '../raster/msdf.js'; +import { slugSchema } from '../raster/slug-technique.js'; +import type { PolicyBufferDeclaration, PolicyBufferDeclarations, TechniqueSchema } from '../core.js'; import { TextEngineRenderPlanView, type RenderPlanTable, type TextEnginePublication } from '../core.js'; import { bitmap, type BitmapStrikeData } from '../raster/bitmap-technique.js'; import { msdf, type MsdfData } from '../raster/msdf.js'; @@ -461,8 +465,12 @@ export class ThreeTextRenderPlanExecutor { const material = decoration ? this.#decorationMaterial(byPolicyId, transform, addressing) : this.#material(resource!, byPolicyId, materialId, transform, addressing); - const origins = decoration ? undefined : byPolicyId.get(1); - const stableIds = decoration ? undefined : byPolicyId.get(STABLE_GLYPH_BUFFER_ID); + const originDeclaration = + decoration || resource === undefined + ? undefined + : glyphOriginBuffer(this.#coordinator.resolveResource(resource.referenceId).technique); + const origins = originDeclaration === undefined ? undefined : byPolicyId.get(originDeclaration.id); + const stableIds = decoration ? undefined : byPolicyId.get(threeSystemBuffers.stableGlyphId.id); if (origins !== undefined && stableIds !== undefined) { if (!(origins.array instanceof Float32Array) || !(stableIds.array instanceof Uint32Array)) { throw new TypeError('glyph-origin augmentation buffers have invalid scalar types'); @@ -585,7 +593,7 @@ export class ThreeTextRenderPlanExecutor { #transformRealization(buffers: ReadonlyMap, transformId: number): TransformRealization { if (transformId !== 0) return { kind: 'direct', transformId }; - const indices = buffers.get(TRANSFORM_BUFFER_ID); + const indices = buffers.get(threeSystemBuffers.transformIndex.id); if (indices === undefined || !(indices.array instanceof Uint32Array)) { throw new Error('indexed Three draw is missing its u32 transform-index buffer'); } @@ -625,11 +633,8 @@ export class ThreeTextRenderPlanExecutor { throw new Error('this Three plan target checkpoint realizes Bitmap draws only'); } const strike = bitmapStrike(resolved); - const required = [1, 2, 3, 4, 5, 6].map((id) => { - const buffer = buffers.get(id); - if (buffer === undefined) throw new Error(`Bitmap draw is missing policy buffer ${id}`); - return buffer; - }); + const part = schemaDrawBuffers(bitmapSchema, buffers, 'Bitmap'); + const required = [part.origin, part.size, part.uvOrigin, part.uvSize, part.color, part.page]; const key = `${resource.id}:${resource.generation}:${materialId}:snap=${String(this.#owner.pixelSnapping)}:${required .map((buffer) => `${buffer.id}:${buffer.generation}`) .join(',')}:${transformProgramKey(transform, this.#transformGeneration)}:${addressingProgramKey(addressing)}`; @@ -642,20 +647,14 @@ export class ThreeTextRenderPlanExecutor { const instance = physicalInstance(TSL.instanceIndex.add(runStart), addressing); const shader = bitmapShader( { - origin: TSL.storage(required[0]!.attribute, 'vec2', required[0]!.attribute.count) - .setPBO(true) - .element(instance), - size: TSL.storage(required[1]!.attribute, 'vec2', required[1]!.attribute.count).setPBO(true).element(instance), - uvOrigin: TSL.storage(required[2]!.attribute, 'vec2', required[2]!.attribute.count) - .setPBO(true) - .element(instance), - uvSize: TSL.storage(required[3]!.attribute, 'vec2', required[3]!.attribute.count) - .setPBO(true) - .element(instance), - color: TSL.storage(required[4]!.attribute, 'vec4', required[4]!.attribute.count).setPBO(true).element(instance), - pageIndex: TSL.storage(required[5]!.attribute, 'uint', required[5]!.attribute.count) + origin: TSL.storage(part.origin.attribute, 'vec2', part.origin.attribute.count).setPBO(true).element(instance), + size: TSL.storage(part.size.attribute, 'vec2', part.size.attribute.count).setPBO(true).element(instance), + uvOrigin: TSL.storage(part.uvOrigin.attribute, 'vec2', part.uvOrigin.attribute.count) .setPBO(true) .element(instance), + uvSize: TSL.storage(part.uvSize.attribute, 'vec2', part.uvSize.attribute.count).setPBO(true).element(instance), + color: TSL.storage(part.color.attribute, 'vec4', part.color.attribute.count).setPBO(true).element(instance), + pageIndex: TSL.storage(part.page.attribute, 'uint', part.page.attribute.count).setPBO(true).element(instance), }, { page: texture }, { pixelSnapping: this.#owner.pixelSnapping }, @@ -679,8 +678,8 @@ export class ThreeTextRenderPlanExecutor { transform: TransformRealization, addressing: RecordAddressing, ): THREE.NodeMaterial { - const rect = buffers.get(1); - const packed = buffers.get(2); + const rect = buffers.get(decorationSchema.buffers.rect.id); + const packed = buffers.get(decorationSchema.buffers.packed.id); if (rect === undefined || packed === undefined) { throw new Error('decoration draw is missing its rectangle or packed policy buffer'); } @@ -788,11 +787,8 @@ export class ThreeTextRenderPlanExecutor { addressing: RecordAddressing, ): THREE.NodeMaterial { const data = msdfData(this.#coordinator.resolveResource(resource.referenceId)); - const required = [1, 2, 3, 4, 5, 6, 7].map((id) => { - const buffer = buffers.get(id); - if (buffer === undefined) throw new Error(`MSDF draw is missing policy buffer ${id}`); - return buffer; - }); + const part = schemaDrawBuffers(msdfSchema, buffers, 'MSDF'); + const required = [part.rect, part.uvRect, part.uvBounds, part.color, part.effectA, part.effectB, part.page]; const key = `msdf:${resource.id}:${resource.generation}:${materialId}:${required .map((buffer) => `${buffer.id}:${buffer.generation}`) .join(',')}:${transformProgramKey(transform, this.#transformGeneration)}:${addressingProgramKey(addressing)}`; @@ -802,22 +798,24 @@ export class ThreeTextRenderPlanExecutor { ({ object }) => (object?.userData.pmndrsGlyphRunStart as number | undefined) ?? 0, ); const instance = physicalInstance(TSL.instanceIndex.add(runStart), addressing); - const fields = required.map((buffer) => - TSL.storage(buffer.attribute, 'vec4', buffer.attribute.count).setPBO(true).element(instance), - ); + const field = (buffer: RetainedBuffer) => + TSL.storage(buffer.attribute, 'vec4', buffer.attribute.count).setPBO(true).element(instance); + const rect = field(part.rect); + const uvRect = field(part.uvRect); + const page = field(part.page); const shader = msdfShader( { - origin: fields[0]!.xy, - size: fields[0]!.zw, - uvOrigin: fields[1]!.xy, - uvSize: fields[1]!.zw, - uvBounds: fields[2]!, - fillColor: fields[3]!, - outlineColor: fields[4]!, - shadowColor: fields[5]!, - shadowOffset: fields[6]!.xy, - outlineWidth: fields[6]!.z, - pageIndex: fields[6]!.w, + origin: rect.xy, + size: rect.zw, + uvOrigin: uvRect.xy, + uvSize: uvRect.zw, + uvBounds: field(part.uvBounds), + fillColor: field(part.color), + outlineColor: field(part.effectA), + shadowColor: field(part.effectB), + shadowOffset: page.xy, + outlineWidth: page.z, + pageIndex: page.w, }, { atlas: this.#msdfAtlas(resource.referenceId, data), @@ -899,11 +897,16 @@ export class ThreeTextRenderPlanExecutor { addressing: RecordAddressing, ): THREE.NodeMaterial { const page = slugPage(this.#coordinator.resolveResource(resource.referenceId)); - const required = [1, 2, 3, 4, 5, 6, 7].map((id) => { - const buffer = buffers.get(id); - if (buffer === undefined) throw new Error(`Slug draw is missing policy buffer ${id}`); - return buffer; - }); + const part = schemaDrawBuffers(slugSchema, buffers, 'Slug'); + const required = [ + part.rect, + part.planeRect, + part.bandTransform, + part.color, + part.inverseFontSize, + part.tableStarts, + part.bandCounts, + ]; const key = `slug:${resource.id}:${resource.generation}:${materialId}:${required .map((buffer) => `${buffer.id}:${buffer.generation}`) .join( @@ -915,13 +918,14 @@ export class ThreeTextRenderPlanExecutor { ({ object }) => (object?.userData.pmndrsGlyphRunStart as number | undefined) ?? 0, ); const instance = physicalInstance(TSL.instanceIndex.add(runStart), addressing); - const floatFields = required - .slice(0, 5) - .map((buffer) => TSL.storage(buffer.attribute, 'vec4', buffer.attribute.count).setPBO(true).element(instance)); - const addresses = TSL.storage(required[5]!.attribute, 'uvec4', required[5]!.attribute.count) + const field = (buffer: RetainedBuffer) => + TSL.storage(buffer.attribute, 'vec4', buffer.attribute.count).setPBO(true).element(instance); + const rect = field(part.rect); + const planeRect = field(part.planeRect); + const addresses = TSL.storage(part.tableStarts.attribute, 'uvec4', part.tableStarts.attribute.count) .setPBO(true) .element(instance); - const counts = TSL.storage(required[6]!.attribute, 'uvec4', required[6]!.attribute.count) + const counts = TSL.storage(part.bandCounts.attribute, 'uvec4', part.bandCounts.attribute.count) .setPBO(true) .element(instance); const indexedTransform = @@ -937,13 +941,13 @@ export class ThreeTextRenderPlanExecutor { ); const shader = slugShader( { - origin: floatFields[0]!.xy, - size: floatFields[0]!.zw, - emOrigin: floatFields[1]!.xy, - emSize: floatFields[1]!.zw, - bandTransform: floatFields[2]!, - color: floatFields[3]!, - inverseScale: floatFields[4]!.x, + origin: rect.xy, + size: rect.zw, + emOrigin: planeRect.xy, + emSize: planeRect.zw, + bandTransform: field(part.bandTransform), + color: field(part.color), + inverseScale: field(part.inverseFontSize).x, curveBaseTexel: addresses.x, horizontalHeaderBase: addresses.y, verticalHeaderBase: addresses.z, @@ -1112,6 +1116,36 @@ export class ThreeTextRenderPlanExecutor { } } +/** The techniques this executor realizes, keyed by wire identity. */ +const techniqueSchemas: ReadonlyMap = new Map([ + [bitmap.id, bitmapSchema], + [msdf.id, msdfSchema], + [slug.id, slugSchema], +]); + +/** Glyph-origin augmentation is schema-declared: no declaration, no augmentation. */ +function glyphOriginBuffer(technique: string): PolicyBufferDeclaration | undefined { + const schema = techniqueSchemas.get(technique); + if (schema?.glyphOrigin === undefined) return undefined; + return schema.buffers[schema.glyphOrigin.buffer]; +} + +/** Resolve a draw's retained buffers by the schema's names instead of remembered ids. */ +function schemaDrawBuffers( + schema: TechniqueSchema, + buffers: ReadonlyMap, + label: string, +): { readonly [Name in keyof Buffers]: RetainedBuffer } { + const resolved: Record = {}; + for (const [name, declaration] of Object.entries(schema.buffers)) { + const buffer = buffers.get(declaration.id); + if (buffer === undefined) throw new Error(`${label} draw is missing its "${name}" policy buffer`); + resolved[name] = buffer; + } + // The keys are exactly schema.buffers' own keys, collected in the loop above. + return resolved as { readonly [Name in keyof Buffers]: RetainedBuffer }; +} + function drawRealizationKey( programId: number, resource: RetainedResource | undefined, diff --git a/packages/glyph/src/three/plan-program-registry.ts b/packages/glyph/src/three/plan-program-registry.ts index 896ce32c..025f2612 100644 --- a/packages/glyph/src/three/plan-program-registry.ts +++ b/packages/glyph/src/three/plan-program-registry.ts @@ -13,6 +13,7 @@ import { import type { LoadedFont } from '../loaded-font.js'; import type { AnyRasterTechnique, RasterResourceId } from '../raster-technique.js'; import type { ThreeTextMaterial } from './material.js'; +import { threeSystemBuffers } from './render-policy.js'; export interface ThreePlanProgramBuffer { readonly scalarType: number; @@ -98,7 +99,7 @@ export interface ThreePolicyAbi { readonly batchFields: typeof textShaperAbi.policy.batchFields; readonly semanticF32Fields: typeof textShaperAbi.engine.semanticF32Fields; readonly semanticU32Fields: typeof textShaperAbi.engine.semanticU32Fields; - readonly transformBufferId: 15; + readonly transformBufferId: typeof threeSystemBuffers.transformIndex.id; } export const threePolicyAbi: ThreePolicyAbi = Object.freeze({ @@ -109,7 +110,7 @@ export const threePolicyAbi: ThreePolicyAbi = Object.freeze({ batchFields: textShaperAbi.policy.batchFields, semanticF32Fields: textShaperAbi.engine.semanticF32Fields, semanticU32Fields: textShaperAbi.engine.semanticU32Fields, - transformBufferId: 15, + transformBufferId: threeSystemBuffers.transformIndex.id, }); function compileProgram( diff --git a/packages/glyph/src/three/render-policy.ts b/packages/glyph/src/three/render-policy.ts index 2a60e4f2..4b99e3cc 100644 --- a/packages/glyph/src/three/render-policy.ts +++ b/packages/glyph/src/three/render-policy.ts @@ -1,22 +1,65 @@ import { + addF32, compileRenderPolicy, + constantF32, + constantU32, createProgram, - floatBuffers, - programContext, - stores, - u32Buffers, + definePolicyBuffers, + defineTechniqueSchema, + multiplyF32, + RenderWireIdentityRegistry, + schemaPolicyBuffers, + subtractF32, + techniqueProgram, + u32ToF32, + type PolicyAllocationMode, type PolicyBuffer, type PolicyCapabilitySet, type PolicyProgram, - RenderWireIdentityRegistry, - type PolicyAllocationMode, type PolicyTransformMode, + type TechniqueSchema, } from '../core.js'; +import { bitmapSchema } from '../raster/bitmap-technique.js'; +import { msdfSchema } from '../raster/msdf.js'; +import { slugSchema } from '../raster/slug-technique.js'; import { textShaperAbi } from '../core.js'; -export const TRANSFORM_BUFFER_ID = 15; +/** Buffers the Three policy itself owns, shared by every program in it. */ +export const threeSystemBuffers: { + readonly stableGlyphId: { readonly id: 14; readonly scalar: 'u32'; readonly lanes: readonly ['stableGlyphId'] }; + readonly transformIndex: { readonly id: 15; readonly scalar: 'u32'; readonly lanes: readonly ['transformIndex'] }; +} = definePolicyBuffers({ + stableGlyphId: { id: 14, scalar: 'u32', lanes: ['stableGlyphId'] }, + transformIndex: { id: 15, scalar: 'u32', lanes: ['transformIndex'] }, +}); + +export const TRANSFORM_BUFFER_ID: number = threeSystemBuffers.transformIndex.id; + +export const STABLE_GLYPH_BUFFER_ID: number = threeSystemBuffers.stableGlyphId.id; -export const STABLE_GLYPH_BUFFER_ID = 14; +/** + * Decoration is a reserved technique of the Three policy rather than a raster + * technique: rows are resource-free and fill the gather lanes directly. + */ +export const decorationSchema: TechniqueSchema< + { + readonly rect: { + readonly id: 1; + readonly scalar: 'f32'; + readonly lanes: readonly ['left', 'top', 'width', 'height']; + }; + readonly packed: { readonly id: 2; readonly scalar: 'u32'; readonly lanes: readonly ['color', 'flags'] }; + }, + { readonly u32: readonly ['color', 'flags'] } +> = defineTechniqueSchema({ + technique: 'pmndrs.decoration', + scope: 'glyph', + binding: { u32: ['color', 'flags'] }, + buffers: { + rect: { id: 1, scalar: 'f32', lanes: ['left', 'top', 'width', 'height'] }, + packed: { id: 2, scalar: 'u32', lanes: ['color', 'flags'] }, + }, +}); export type ThreeTransformMode = PolicyTransformMode; @@ -79,35 +122,25 @@ function bitmapProgram( transformMode: ThreeTransformMode, allocationMode: ThreeAllocationMode, ): PolicyProgram { - const context = programContext('strike', 8, 1); - const { loadF32, loadU32, binary, storeF32, storeU32 } = context; - loadF32(15); - loadU32(31, 0); - loadU32(30, 1); - loadU32(29, 2); - binary('multiplyF32', 15, 7, 2); - binary('addF32', 16, 0, 15); - binary('multiplyF32', 17, 8, 2); - binary('subtractF32', 18, 1, 17); - binary('multiplyF32', 19, 9, 2); - binary('multiplyF32', 20, 10, 2); - stores(storeF32, [ - [1, [16, 18]], - [2, [19, 20]], - [3, [11, 12]], - [4, [13, 14]], - [5, [3, 4, 5, 6]], + const p = techniqueProgram(bitmapSchema); + const { inlineOrigin, blockOrigin, fontSize, color, transformIndex, stableGlyphId } = p.semantics; + const { bearingX, bearingY, width, height, uvOriginX, uvOriginY, uvSizeX, uvSizeY, page } = p.binding; + p.store(bitmapSchema.buffers.origin, [ + addF32(inlineOrigin, multiplyF32(bearingX, fontSize)), + subtractF32(blockOrigin, multiplyF32(bearingY, fontSize)), ]); - if (transformMode === 'indexed') storeU32(TRANSFORM_BUFFER_ID, 0, 31); - storeU32(STABLE_GLYPH_BUFFER_ID, 0, 30); - storeU32(6, 0, 29); + p.store(bitmapSchema.buffers.size, [multiplyF32(width, fontSize), multiplyF32(height, fontSize)]); + p.store(bitmapSchema.buffers.uvOrigin, [uvOriginX, uvOriginY]); + p.store(bitmapSchema.buffers.uvSize, [uvSizeX, uvSizeY]); + p.store(bitmapSchema.buffers.color, [color.red, color.green, color.blue, color.alpha]); + if (transformMode === 'indexed') p.store(threeSystemBuffers.transformIndex, [transformIndex]); + p.store(threeSystemBuffers.stableGlyphId, [stableGlyphId]); + p.store(bitmapSchema.buffers.page, [page]); return createProgram( techniqueId, programId, - context, - transformMode === 'indexed' - ? [...floatBuffers([2, 2, 2, 2, 4]), ...u32Buffers([1], 6), stableGlyphIdBuffer(), transformIndexBuffer()] - : [...floatBuffers([2, 2, 2, 2, 4]), ...u32Buffers([1], 6), stableGlyphIdBuffer()], + p.compile(), + programBuffers(bitmapSchema, transformMode), transformMode, allocationMode, ); @@ -119,40 +152,29 @@ function msdfProgram( transformMode: ThreeTransformMode, allocationMode: ThreeAllocationMode, ): PolicyProgram { - const context = programContext('glyph', 10, 1); - const { operations, loadF32, loadU32, binary, constantF32, storeF32, storeU32 } = context; - loadF32(17); - loadU32(17, 2); - loadU32(31, 0); - loadU32(30, 1); - binary('multiplyF32', 18, 7, 2); - binary('addF32', 19, 0, 18); - binary('multiplyF32', 20, 8, 2); - binary('subtractF32', 21, 1, 20); - binary('multiplyF32', 22, 9, 2); - binary('multiplyF32', 23, 10, 2); - operations.push({ opcode: textShaperAbi.policy.opcodes.convertU32ToF32, target: 24, operand0: 17 }); - constantF32(25, 0); - stores(storeF32, [ - [1, [19, 21, 22, 23]], - [2, [11, 12, 13, 14]], - [3, [11, 12, 15, 16]], - [4, [3, 4, 5, 6]], - [5, [25, 25, 25, 25]], - [6, [25, 25, 25, 25]], - [7, [25, 25, 25, 24]], + const p = techniqueProgram(msdfSchema); + const { inlineOrigin, blockOrigin, fontSize, color, transformIndex, stableGlyphId } = p.semantics; + const { bearingX, bearingY, width, height, uvOriginX, uvOriginY, uvSizeX, uvSizeY, uvMaxX, uvMaxY, page } = p.binding; + const zero = constantF32(0); + p.store(msdfSchema.buffers.rect, [ + addF32(inlineOrigin, multiplyF32(bearingX, fontSize)), + subtractF32(blockOrigin, multiplyF32(bearingY, fontSize)), + multiplyF32(width, fontSize), + multiplyF32(height, fontSize), ]); - if (transformMode === 'indexed') storeU32(TRANSFORM_BUFFER_ID, 0, 31); - storeU32(STABLE_GLYPH_BUFFER_ID, 0, 30); + p.store(msdfSchema.buffers.uvRect, [uvOriginX, uvOriginY, uvSizeX, uvSizeY]); + p.store(msdfSchema.buffers.uvBounds, [uvOriginX, uvOriginY, uvMaxX, uvMaxY]); + p.store(msdfSchema.buffers.color, [color.red, color.green, color.blue, color.alpha]); + p.store(msdfSchema.buffers.effectA, [zero, zero, zero, zero]); + p.store(msdfSchema.buffers.effectB, [zero, zero, zero, zero]); + p.store(msdfSchema.buffers.page, [zero, zero, zero, u32ToF32(page)]); + if (transformMode === 'indexed') p.store(threeSystemBuffers.transformIndex, [transformIndex]); + p.store(threeSystemBuffers.stableGlyphId, [stableGlyphId]); return createProgram( techniqueId, programId, - context, - [ - ...floatBuffers([4, 4, 4, 4, 4, 4, 4]), - stableGlyphIdBuffer(), - ...(transformMode === 'indexed' ? [transformIndexBuffer()] : []), - ], + p.compile(), + programBuffers(msdfSchema, transformMode), transformMode, allocationMode, ); @@ -164,43 +186,47 @@ function slugProgram( transformMode: ThreeTransformMode, allocationMode: ThreeAllocationMode, ): PolicyProgram { - const context = programContext('glyph', 8, 6, true); - const { loadF32, loadU32, binary, constantF32, constantU32, storeF32, storeU32 } = context; - loadF32(16); - loadU32(31, 0); - loadU32(30, 1); - for (let field = 0; field < 6; field += 1) loadU32(21 + field, field + 2); - binary('multiplyF32', 16, 8, 2); - binary('addF32', 17, 0, 16); - binary('multiplyF32', 18, 9, 2); - binary('subtractF32', 19, 1, 18); - binary('multiplyF32', 20, 10, 2); - binary('multiplyF32', 27, 11, 2); - constantF32(28, 0); - constantU32(29, 0); - stores(storeF32, [ - [1, [17, 19, 20, 27]], - [2, [8, 9, 10, 11]], - [3, [12, 13, 14, 15]], - [4, [3, 4, 5, 6]], - [5, [7, 28, 28, 28]], - ]); - stores(storeU32, [ - [6, [21, 22, 23, 24]], - [7, [25, 26, 29, 29]], + const p = techniqueProgram(slugSchema, { inverseFontSize: true }); + const { inlineOrigin, blockOrigin, fontSize, color, transformIndex, stableGlyphId } = p.semantics; + const inverseFontSize = p.semantics.inverseFontSize; + if (inverseFontSize === undefined) throw new TypeError('the Slug program declares inverseFontSize'); + const { + bearingX, + bearingY, + width, + height, + bandScaleX, + bandScaleY, + bandOffsetX, + bandOffsetY, + curveStart, + headerStart, + referenceStart, + bandStart, + horizontalBands, + verticalBands, + } = p.binding; + const zeroF32 = constantF32(0); + const zeroU32 = constantU32(0); + p.store(slugSchema.buffers.rect, [ + addF32(inlineOrigin, multiplyF32(bearingX, fontSize)), + subtractF32(blockOrigin, multiplyF32(bearingY, fontSize)), + multiplyF32(width, fontSize), + multiplyF32(height, fontSize), ]); - if (transformMode === 'indexed') storeU32(TRANSFORM_BUFFER_ID, 0, 31); - storeU32(STABLE_GLYPH_BUFFER_ID, 0, 30); + p.store(slugSchema.buffers.planeRect, [bearingX, bearingY, width, height]); + p.store(slugSchema.buffers.bandTransform, [bandScaleX, bandScaleY, bandOffsetX, bandOffsetY]); + p.store(slugSchema.buffers.color, [color.red, color.green, color.blue, color.alpha]); + p.store(slugSchema.buffers.inverseFontSize, [inverseFontSize, zeroF32, zeroF32, zeroF32]); + p.store(slugSchema.buffers.tableStarts, [curveStart, headerStart, referenceStart, bandStart]); + p.store(slugSchema.buffers.bandCounts, [horizontalBands, verticalBands, zeroU32, zeroU32]); + if (transformMode === 'indexed') p.store(threeSystemBuffers.transformIndex, [transformIndex]); + p.store(threeSystemBuffers.stableGlyphId, [stableGlyphId]); return createProgram( techniqueId, programId, - context, - [ - ...floatBuffers([4, 4, 4, 4, 4]), - ...u32Buffers([4, 4], 6), - stableGlyphIdBuffer(), - ...(transformMode === 'indexed' ? [transformIndexBuffer()] : []), - ], + p.compile(), + programBuffers(slugSchema, transformMode), transformMode, allocationMode, ); @@ -209,8 +235,9 @@ function slugProgram( /** * Resource-free decoration quads. Decoration rows fill the gather lanes directly — * f32 lanes 0-3 carry the rectangle and u32 lanes carry transform, stable identity, - * color, then flags — so the loads below read lanes by index; the semantic input - * declarations exist to satisfy policy validation and are not sourced per glyph. + * color, then flags — so the semantic handles below address gather lanes by + * position, not per-glyph meaning: the "inlineOrigin" lane is the rect's inline + * start, and the paint arrives through the binding's packed u32 pair. */ function decorationProgram( techniqueId: number, @@ -218,26 +245,18 @@ function decorationProgram( transformMode: ThreeTransformMode, allocationMode: ThreeAllocationMode, ): PolicyProgram { - const context = programContext('glyph', 0, 2); - const { loadF32, loadU32, storeF32, storeU32 } = context; - loadF32(4); - loadU32(28, 0); - loadU32(29, 1); - loadU32(30, 2); - loadU32(31, 3); - stores(storeF32, [[1, [0, 1, 2, 3]]]); - storeU32(2, 0, 30); - storeU32(2, 1, 31); - if (transformMode === 'indexed') storeU32(TRANSFORM_BUFFER_ID, 0, 28); - storeU32(STABLE_GLYPH_BUFFER_ID, 0, 29); + const p = techniqueProgram(decorationSchema); + const { inlineOrigin, blockOrigin, fontSize, color, transformIndex, stableGlyphId } = p.semantics; + p.store(decorationSchema.buffers.rect, [inlineOrigin, blockOrigin, fontSize, color.red]); + p.store(decorationSchema.buffers.packed, [p.binding.color, p.binding.flags]); + if (transformMode === 'indexed') p.store(threeSystemBuffers.transformIndex, [transformIndex]); + p.store(threeSystemBuffers.stableGlyphId, [stableGlyphId]); return { ...createProgram( techniqueId, programId, - context, - transformMode === 'indexed' - ? [...floatBuffers([4]), ...u32Buffers([2], 2), stableGlyphIdBuffer(), transformIndexBuffer()] - : [...floatBuffers([4]), ...u32Buffers([2], 2), stableGlyphIdBuffer()], + p.compile(), + programBuffers(decorationSchema, transformMode), transformMode, allocationMode, ), @@ -246,6 +265,15 @@ function decorationProgram( }; } +/** Every Three program publishes its schema's buffers, then the policy's own system buffers. */ +function programBuffers(schema: TechniqueSchema, transformMode: ThreeTransformMode): PolicyBuffer[] { + return [ + ...schemaPolicyBuffers(schema), + stableGlyphIdBuffer(), + ...(transformMode === 'indexed' ? [transformIndexBuffer()] : []), + ]; +} + function transformIndexBuffer(): PolicyBuffer { return { id: TRANSFORM_BUFFER_ID, diff --git a/packages/glyph/tests/integration/render-policy-equivalence.test.mjs b/packages/glyph/tests/integration/render-policy-equivalence.test.mjs new file mode 100644 index 00000000..7dd8df6d --- /dev/null +++ b/packages/glyph/tests/integration/render-policy-equivalence.test.mjs @@ -0,0 +1,148 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import { textShaperAbi } from '../../dist/generated/text-shaper-abi.js'; +import { threeRenderPolicyBytes } from '../../dist/three/render-policy.js'; + +const fixtureUrl = new URL('../fixtures/render-policy/hand-numbered-policy-bytes.json', import.meta.url); + +/** + * Semantic equivalence against the hand-numbered programs. Register numbers and + * operation order are private to a program's execution — the interpreter only + * requires forward-only writes — so the DSL port may renumber freely. What must + * never drift: the input tables, buffer schemas, capability sets, program + * metadata, and the expression each buffer lane receives. This test decodes both + * byte streams and compares exactly that. + */ +test('the Three render policy is semantically identical to the hand-numbered fixture', async () => { + const fixtures = JSON.parse(await readFile(fixtureUrl, 'utf8')); + for (const transform of ['direct', 'indexed']) { + for (const allocation of ['ordered', 'stable']) { + const key = `${transform}/${allocation}`; + const fixture = decodePolicy(Buffer.from(fixtures[key], 'base64')); + const current = decodePolicy(threeRenderPolicyBytes(undefined, transform, [], allocation)); + assert.equal(current.programs.length, fixture.programs.length, `${key}: program count`); + assert.deepEqual(current.capabilitySets, fixture.capabilitySets, `${key}: capability sets`); + for (const [index, expected] of fixture.programs.entries()) { + const actual = current.programs[index]; + assert.deepEqual(actual.metadata, expected.metadata, `${key}: program ${index} metadata`); + assert.deepEqual(actual.inputs, expected.inputs, `${key}: program ${index} input table`); + assert.deepEqual(actual.buffers, expected.buffers, `${key}: program ${index} buffers`); + assert.deepEqual(actual.stores, expected.stores, `${key}: program ${index} store dataflow`); + } + } + } +}); + +function decodePolicy(bytes) { + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const request = textShaperAbi.layouts.policyRequest; + const programLayout = textShaperAbi.layouts.policyProgram; + const operationLayout = textShaperAbi.layouts.policyOperation; + const inputLayout = textShaperAbi.layouts.policyInput; + const bufferLayout = textShaperAbi.layouts.policyBuffer; + const capabilityLayout = textShaperAbi.layouts.policyCapabilitySet; + const opcodes = textShaperAbi.policy.opcodes; + const opcodeNames = new Map(Object.entries(opcodes).map(([name, value]) => [value, name])); + + const capabilitySets = []; + const capabilityOffset = view.getUint32(request.capabilitySetsOffset, true); + for (let index = 0; index < view.getUint32(request.capabilitySetCount, true); index += 1) { + const at = capabilityOffset + index * capabilityLayout.size; + capabilitySets.push(Buffer.from(bytes.slice(at, at + capabilityLayout.size)).toString('hex')); + } + + const programs = []; + const programsOffset = view.getUint32(request.programsOffset, true); + const operationsOffset = view.getUint32(request.operationsOffset, true); + const inputsOffset = view.getUint32(request.inputsOffset, true); + const buffersOffset = view.getUint32(request.buffersOffset, true); + for (let index = 0; index < view.getUint32(request.programCount, true); index += 1) { + const at = programsOffset + index * programLayout.size; + const metadata = {}; + for (const field of [ + 'techniqueId', + 'programId', + 'capabilitySetId', + 'resourceKindMask', + 'semanticViewMask', + 'storageKeyMask', + 'paintCapabilities', + 'compositingCapabilities', + 'drawKeyMask', + 'variant', + 'allocationStrategy', + 'primitiveKind', + 'f32InputCount', + 'u32InputCount', + ]) { + const offset = at + programLayout[field]; + metadata[field] = + field === 'allocationStrategy' || field === 'f32InputCount' || field === 'u32InputCount' + ? view.getUint8(offset) + : field === 'primitiveKind' + ? view.getUint16(offset, true) + : view.getUint32(offset, true); + } + const inputStart = view.getUint32(at + programLayout.inputStart, true); + const inputCount = view.getUint16(at + programLayout.inputCount, true); + const inputs = []; + for (let input = 0; input < inputCount; input += 1) { + const inputAt = inputsOffset + (inputStart + input) * inputLayout.size; + inputs.push({ + scope: view.getUint8(inputAt + inputLayout.scope), + field: view.getUint8(inputAt + inputLayout.field), + }); + } + const bufferStart = view.getUint32(at + programLayout.bufferStart, true); + const bufferCount = view.getUint16(at + programLayout.bufferCount, true); + const buffers = []; + for (let buffer = 0; buffer < bufferCount; buffer += 1) { + const bufferAt = buffersOffset + (bufferStart + buffer) * bufferLayout.size; + buffers.push(Buffer.from(bytes.slice(bufferAt, bufferAt + bufferLayout.size)).toString('hex')); + } + + const operationStart = view.getUint32(at + programLayout.operationStart, true); + const operationCount = view.getUint16(at + programLayout.operationCount, true); + const registers = new Map(); + const stores = new Map(); + const commutative = new Set(['addF32', 'multiplyF32']); + for (let op = 0; op < operationCount; op += 1) { + const opAt = operationsOffset + (operationStart + op) * operationLayout.size; + const name = opcodeNames.get(view.getUint8(opAt + operationLayout.opcode)); + const target = view.getUint8(opAt + operationLayout.target); + const operand0 = view.getUint8(opAt + operationLayout.operand0); + const operand1 = view.getUint8(opAt + operationLayout.operand1); + const immediate0 = view.getUint32(opAt + operationLayout.immediate0, true); + if (name === 'loadF32') { + const input = inputs[operand0]; + registers.set(target, `f32(${input.scope}:${input.field})`); + } else if (name === 'loadU32') { + const input = inputs[metadata.f32InputCount + operand0]; + registers.set(target, `u32(${input.scope}:${input.field})`); + } else if (name === 'constantF32' || name === 'constantU32') { + registers.set(target, `${name}:${immediate0}`); + } else if (name === 'convertU32ToF32') { + registers.set(target, `u32ToF32(${required(registers, operand0)})`); + } else if (name === 'addF32' || name === 'subtractF32' || name === 'multiplyF32') { + let left = required(registers, operand0); + let right = required(registers, operand1); + if (commutative.has(name) && right < left) [left, right] = [right, left]; + registers.set(target, `${name}(${left}, ${right})`); + } else if (name === 'storeF32' || name === 'storeU32' || name === 'storeU16') { + stores.set(`${name}:buffer${immediate0}:lane${operand1}`, required(registers, operand0)); + } else { + throw new Error(`unexpected policy opcode ${String(name)}`); + } + } + programs.push({ metadata, inputs, buffers, stores: Object.fromEntries([...stores.entries()].sort()) }); + } + return { capabilitySets, programs }; +} + +function required(registers, register) { + const value = registers.get(register); + if (value === undefined) throw new Error(`register r${register} read before it was written`); + return value; +} diff --git a/packages/glyph/tests/integration/render-policy-golden.test.mjs b/packages/glyph/tests/integration/render-policy-golden.test.mjs new file mode 100644 index 00000000..8c78b45f --- /dev/null +++ b/packages/glyph/tests/integration/render-policy-golden.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import test from 'node:test'; + +import { threeRenderPolicyBytes } from '../../dist/three/render-policy.js'; + +/** + * Byte identity for the DSL-authored Three render policy. Register numbering is + * program-private, so the DSL port re-pinned these digests once — with the + * semantic-equivalence test proving the input tables, buffer schemas, and per-lane + * store dataflow unchanged against the hand-numbered fixtures. From here, any + * digest drift means the compiled wire records changed and must be re-justified. + */ +const GOLDEN = new Map([ + ['direct/ordered', 'd8c0dc43246ec9b75a0ba38ee7d75bfd4ce38667f8f1a4a2b39fe46f6aa66964'], + ['direct/stable', '19bcd518b14608b123533eaebee66b5b6e8e509432f28b8f21d4aa16c68788c5'], + ['indexed/ordered', '2da9a64cffb939c3020dcece3368010713835e7573a29674aa2e2c9c18e717a0'], + ['indexed/stable', '9bada50f28f087b5f55df8ae502be6a059a144b0b6cb6c4f1e3bae57055fd088'], +]); + +test('the Three render policy compiles to its golden bytes for every variant', () => { + for (const transform of ['direct', 'indexed']) { + for (const allocation of ['ordered', 'stable']) { + const bytes = threeRenderPolicyBytes(undefined, transform, [], allocation); + const digest = createHash('sha256').update(bytes).digest('hex'); + assert.equal( + digest, + GOLDEN.get(`${transform}/${allocation}`), + `policy bytes changed for ${transform}/${allocation}`, + ); + } + } +}); diff --git a/packages/glyph/tests/package/policy-program-provenance.test.mjs b/packages/glyph/tests/package/policy-program-provenance.test.mjs new file mode 100644 index 00000000..459788df --- /dev/null +++ b/packages/glyph/tests/package/policy-program-provenance.test.mjs @@ -0,0 +1,54 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { addF32, constantF32, multiplyF32, policyProgram } from '@pmndrs/glyph/core'; + +const OPTIONS = { scope: 'glyph', bindingF32: ['bearingX'] }; +const BUFFER = { id: 1, scalar: 'f32', lanes: ['x', 'y'] }; + +/** + * A value loaded from one program's input table means nothing inside another + * program: the input index it carries would silently read a different field. + * Only session-free constants may cross builders. + */ +test('storing another session’s value throws instead of misreading inputs', () => { + const first = policyProgram(OPTIONS); + const second = policyProgram(OPTIONS); + assert.throws(() => second.store(BUFFER, [first.semantics.inlineOrigin, second.semantics.blockOrigin]), /session/); +}); + +test('derived values carry their session across combinators', () => { + const first = policyProgram(OPTIONS); + const second = policyProgram(OPTIONS); + const derived = multiplyF32(first.binding.bearingX, constantF32(2)); + assert.throws(() => second.store(BUFFER, [derived, second.semantics.blockOrigin]), /session/); +}); + +test('combinators reject cross-session operands at construction', () => { + const first = policyProgram(OPTIONS); + const second = policyProgram(OPTIONS); + assert.throws(() => multiplyF32(first.semantics.fontSize, second.semantics.fontSize), /session/); +}); + +test('deep shared expression DAGs stay cheap to store', () => { + const program = policyProgram(OPTIONS); + let node = addF32(program.semantics.inlineOrigin, program.binding.bearingX); + for (let depth = 0; depth < 24; depth += 1) node = addF32(node, node); + const start = performance.now(); + program.store(BUFFER, [node, program.semantics.blockOrigin]); + const elapsed = performance.now() - start; + assert.ok(elapsed < 50, `store took ${elapsed}ms over a shared DAG`); +}); + +test('constants are session-free and same-session programs still compile', () => { + const program = policyProgram(OPTIONS); + const scale = constantF32(0.5); + program.store(BUFFER, [ + addF32(program.semantics.inlineOrigin, multiplyF32(program.binding.bearingX, scale)), + multiplyF32(program.semantics.blockOrigin, scale), + ]); + const other = policyProgram(OPTIONS); + other.store(BUFFER, [multiplyF32(other.semantics.inlineOrigin, scale), scale]); + assert.equal(program.compile().operations.length > 0, true); + assert.equal(other.compile().operations.length > 0, true); +}); diff --git a/packages/glyph/tests/package/schema-authority.test.mjs b/packages/glyph/tests/package/schema-authority.test.mjs new file mode 100644 index 00000000..c6bc187e --- /dev/null +++ b/packages/glyph/tests/package/schema-authority.test.mjs @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import test from 'node:test'; + +const sourceRoot = new URL('../../src/', import.meta.url).pathname; + +/** + * The technique schema is the only witness to buffer identity. Nobody else may + * hold a literal buffer id: not executor lookups, not attribute-name strings, + * not parallel const tables. Schema declarations (raster techniques and the + * Three policy's own buffers) are the sanctioned definition sites. + */ +const DEFINITION_SITES = new Set([ + 'raster/bitmap-technique.ts', + 'raster/msdf.ts', + 'raster/slug-technique.ts', + 'three/render-policy.ts', +]); + +test('buffer ids appear only inside schema declarations', async () => { + const offenders = []; + for await (const file of walk(sourceRoot)) { + const relative = file.slice(sourceRoot.length); + const text = await readFile(file, 'utf8'); + for (const [index, line] of text.split('\n').entries()) { + const lookup = /\.get\(\s*\d+\s*\)/.exec(line); + if (lookup && /byPolicyId|buffers/.test(line)) { + offenders.push(`${relative}:${index + 1} literal buffer lookup: ${line.trim()}`); + } + if (/_pmndrsText_\d/.test(line)) { + offenders.push(`${relative}:${index + 1} literal attribute name: ${line.trim()}`); + } + if (!DEFINITION_SITES.has(relative) && /BUFFER_ID\s*=\s*\d/.test(line)) { + offenders.push(`${relative}:${index + 1} parallel id const: ${line.trim()}`); + } + // Buffer id sequences and vector widths derive from a schema, never from a + // hand-rolled numeric list: no literal-width policy-buffer builders outside + // their core definition, no literal id arrays mapped into buffer lookups, + // and no restated system-buffer ids. + if (relative !== 'core/render-policy.ts' && /(?:floatBuffers|u32Buffers)\(\s*\[/.test(line)) { + offenders.push(`${relative}:${index + 1} literal buffer widths: ${line.trim()}`); + } + if (/\[\s*\d+\s*(?:,\s*\d+\s*){2,}\]\.map\(/.test(line)) { + offenders.push(`${relative}:${index + 1} literal buffer id range: ${line.trim()}`); + } + if (/transformBufferId:\s*\d/.test(line)) { + offenders.push(`${relative}:${index + 1} restated system buffer id: ${line.trim()}`); + } + } + } + assert.deepEqual(offenders, [], 'buffer identity leaked outside schema declarations'); +}); + +async function* walk(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) yield* walk(path); + else if (entry.name.endsWith('.ts')) yield path; + } +} diff --git a/packages/glyph/tests/package/technique-schema.test.mjs b/packages/glyph/tests/package/technique-schema.test.mjs new file mode 100644 index 00000000..555fc18c --- /dev/null +++ b/packages/glyph/tests/package/technique-schema.test.mjs @@ -0,0 +1,114 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { defineTechniqueSchema, floatBuffers, schemaPolicyBuffers, textShaperAbi, u32Buffers } from '@pmndrs/glyph/core'; +import { bitmapSchema } from '@pmndrs/glyph/raster/bitmap'; +import { msdfSchema } from '@pmndrs/glyph/raster/msdf'; +import { slugSchema } from '@pmndrs/glyph/raster/slug'; + +function declaration() { + return { + technique: 'test.technique', + scope: 'glyph', + binding: { f32: ['a', 'b'], u32: ['c'] }, + buffers: { + origin: { id: 1, scalar: 'f32', lanes: ['x', 'y'] }, + flags: { id: 2, scalar: 'u32', lanes: ['flags'] }, + }, + resources: { atlas: { kind: 'texture' } }, + }; +} + +test('defineTechniqueSchema freezes the whole declaration', () => { + const schema = defineTechniqueSchema(declaration()); + assert.ok(Object.isFrozen(schema), 'schema'); + assert.ok(Object.isFrozen(schema.buffers), 'buffers'); + assert.ok(Object.isFrozen(schema.buffers.origin), 'buffer declaration'); + assert.ok(Object.isFrozen(schema.buffers.origin.lanes), 'lanes'); + assert.ok(Object.isFrozen(schema.binding), 'binding'); + assert.ok(Object.isFrozen(schema.binding.f32), 'binding names'); + assert.ok(Object.isFrozen(schema.resources), 'resources'); + assert.ok(Object.isFrozen(schema.resources.atlas), 'resource declaration'); + assert.throws(() => { + schema.buffers.origin.id = 9; + }, TypeError); +}); + +test('every first-party schema is frozen', () => { + for (const schema of [bitmapSchema, msdfSchema, slugSchema]) { + assert.ok(Object.isFrozen(schema), schema.technique); + assert.ok(Object.isFrozen(schema.buffers), schema.technique); + for (const buffer of Object.values(schema.buffers)) { + assert.ok(Object.isFrozen(buffer) && Object.isFrozen(buffer.lanes), schema.technique); + } + } +}); + +test('rejected declarations leave caller-owned input untouched', () => { + const input = { + technique: 'test.rejected', + scope: 'glyph', + binding: { f32: ['duplicate'], u32: ['duplicate'] }, + buffers: { origin: { id: 1, scalar: 'f32', lanes: ['x', 'y'] } }, + }; + assert.throws(() => defineTechniqueSchema(input), TypeError); + assert.equal(Object.isFrozen(input.buffers), false); + assert.equal(Object.isFrozen(input.buffers.origin), false); + assert.equal(Object.isFrozen(input.buffers.origin.lanes), false); +}); + +test('schemas own their data: caller accessors cannot change validated widths', () => { + let reads = 0; + const accessorInput = { + technique: 'test.accessor', + scope: 'glyph', + binding: { f32: ['a'] }, + buffers: { + sneaky: { + id: 1, + scalar: 'f32', + get lanes() { + reads += 1; + return reads > 1 ? ['x', 'y', 'z'] : ['x']; + }, + }, + }, + }; + const schema = defineTechniqueSchema(accessorInput); + assert.deepEqual([...schema.buffers.sneaky.lanes], ['x']); + assert.equal(schemaPolicyBuffers(schema)[0].vectorWidth, 1); + assert.equal(schemaPolicyBuffers(schema)[0].vectorWidth, 1); +}); + +test('glyphOrigin metadata must name a declared f32 buffer with two origin lanes', () => { + const valid = defineTechniqueSchema({ ...declaration(), glyphOrigin: { buffer: 'origin' } }); + assert.deepEqual(valid.glyphOrigin, { buffer: 'origin' }); + assert.throws(() => defineTechniqueSchema({ ...declaration(), glyphOrigin: { buffer: 'missing' } }), TypeError); + assert.throws(() => defineTechniqueSchema({ ...declaration(), glyphOrigin: { buffer: 'flags' } }), TypeError); + assert.throws( + () => + defineTechniqueSchema({ + ...declaration(), + buffers: { thin: { id: 1, scalar: 'f32', lanes: ['x'] } }, + glyphOrigin: { buffer: 'thin' }, + }), + TypeError, + ); +}); + +test('first-party techniques declare where their glyph origin lives', () => { + assert.equal(bitmapSchema.glyphOrigin?.buffer, 'origin'); + assert.equal(msdfSchema.glyphOrigin?.buffer, 'rect'); + assert.equal(slugSchema.glyphOrigin?.buffer, 'rect'); +}); + +test('schemaPolicyBuffers derives exactly the hand-rolled wire buffer list', () => { + assert.deepEqual(schemaPolicyBuffers(bitmapSchema), [...floatBuffers([2, 2, 2, 2, 4]), ...u32Buffers([1], 6)]); + assert.deepEqual(schemaPolicyBuffers(msdfSchema), floatBuffers([4, 4, 4, 4, 4, 4, 4])); + assert.deepEqual(schemaPolicyBuffers(slugSchema), [...floatBuffers([4, 4, 4, 4, 4]), ...u32Buffers([4, 4], 6)]); + const derived = schemaPolicyBuffers(defineTechniqueSchema(declaration())); + assert.deepEqual(derived, [ + { id: 1, scalar: textShaperAbi.policy.scalarTypes.f32, vectorWidth: 2 }, + { id: 2, scalar: textShaperAbi.policy.scalarTypes.u32, vectorWidth: 1 }, + ]); +}); diff --git a/packages/glyph/tests/types/policy-program-dsl.test.ts b/packages/glyph/tests/types/policy-program-dsl.test.ts new file mode 100644 index 00000000..50dce89e --- /dev/null +++ b/packages/glyph/tests/types/policy-program-dsl.test.ts @@ -0,0 +1,47 @@ +import { + addF32, + constantF32, + constantU32, + multiplyF32, + policyProgram, + subtractF32, + u32ToF32, + type PolicyF32Value, + type PolicyU32Value, +} from '@pmndrs/glyph/core'; + +// A program declares its named inputs once; every later reference is a handle, +// never a number. +const p = policyProgram({ + scope: 'strike', + bindingF32: ['bearingX', 'bearingY', 'width', 'height'] as const, + bindingU32: ['page'] as const, +}); + +const { inlineOrigin, blockOrigin, fontSize, color, transformIndex, stableGlyphId } = p.semantics; +const { bearingX, bearingY, width, height, page } = p.binding; + +const left: PolicyF32Value = addF32(inlineOrigin, multiplyF32(bearingX, fontSize)); +const top: PolicyF32Value = subtractF32(blockOrigin, multiplyF32(bearingY, fontSize)); +p.storeF32(1, [left, top, multiplyF32(width, fontSize), multiplyF32(height, fontSize)]); +p.storeF32(2, [color.red, color.green, color.blue, color.alpha]); +p.storeF32(3, [u32ToF32(page), constantF32(0), constantF32(0), constantF32(0)]); +p.storeU32(14, [stableGlyphId]); +p.storeU32(15, [transformIndex]); + +const compiled = p.compile(); +void compiled.inputs; +void compiled.operations; +const f32Count: number = compiled.f32InputCount; +const u32Count: number = compiled.u32InputCount; +void f32Count; +void u32Count; + +declare const u32Value: PolicyU32Value; +// @ts-expect-error A u32 value cannot feed f32 arithmetic without an explicit conversion. +addF32(inlineOrigin, u32Value); +// @ts-expect-error An f32 value cannot be stored into a u32 buffer lane. +p.storeU32(14, [left]); +// @ts-expect-error Binding names are declared, not invented at use sites. +void p.binding.kerning; +void constantU32; diff --git a/packages/glyph/tests/types/technique-schema.test.ts b/packages/glyph/tests/types/technique-schema.test.ts new file mode 100644 index 00000000..a930e0c5 --- /dev/null +++ b/packages/glyph/tests/types/technique-schema.test.ts @@ -0,0 +1,68 @@ +import { + definePolicyBuffers, + defineTechniqueSchema, + multiplyF32, + schemaFieldTable, + schemaPolicyBuffers, + techniqueProgram, + type FontBindingFieldTable, + type PolicyBuffer, + type PolicyF32Value, +} from '@pmndrs/glyph/core'; +import { bitmapSchema } from '@pmndrs/glyph/raster/bitmap'; + +// A technique schema is the single authority: buffer ids, scalar kinds, and lane +// meanings are declared once and every consumer derives from the declaration. +const schema = defineTechniqueSchema({ + technique: 'example.technique', + scope: 'glyph', + binding: { f32: ['bearingX', 'size'] as const, u32: ['page'] as const }, + buffers: { + rect: { id: 1, scalar: 'f32', lanes: ['left', 'top', 'width', 'height'] }, + page: { id: 2, scalar: 'u32', lanes: ['page'] }, + } as const, +}); + +const p = techniqueProgram(schema); +const { fontSize } = p.semantics; +const { bearingX, size, page } = p.binding; +const scaled: PolicyF32Value = multiplyF32(size, fontSize); +p.store(schema.buffers.rect, [multiplyF32(bearingX, fontSize), scaled, scaled, scaled]); +p.store(schema.buffers.page, [page]); +void p.compile(); + +// The id is data, not convention: consumers read it from the declaration. +const rectId: number = schema.buffers.rect.id; +void rectId; + +// System buffers use the same construct without a technique wrapper. +const system = definePolicyBuffers({ + stableGlyphId: { id: 14, scalar: 'u32', lanes: ['stableGlyphId'] }, +} as const); +p.store(system.stableGlyphId, [p.semantics.stableGlyphId]); + +// The first-party bitmap technique publishes its schema from its own subpath. +const bitmapColorId: number = bitmapSchema.buffers.color.id; +void bitmapColorId; + +// Wire buffer lists and binding tables derive from the same declaration. +const wire: PolicyBuffer[] = schemaPolicyBuffers(schema); +void wire; +const table: FontBindingFieldTable = schemaFieldTable(['bearingX', 'size'] as const, 4, { + bearingX: (row) => row, + size: (row) => row * 2, +}); +void table; + +// @ts-expect-error An f32 value cannot be stored into a u32 buffer. +p.store(schema.buffers.page, [scaled]); +// @ts-expect-error Undeclared buffers do not exist on the schema. +void schema.buffers.atlas; +// @ts-expect-error Undeclared binding fields do not exist. +void p.binding.kerning; +// @ts-expect-error A field table must provide a reader for every declared name. +schemaFieldTable(['bearingX', 'size'] as const, 4, { bearingX: (row: number) => row }); +// @ts-expect-error A misspelled reader name is a compile error, not a shifted column. +schemaFieldTable(['bearingX'] as const, 4, { bearingsX: (row: number) => row }); +// @ts-expect-error glyphOrigin must name a declared buffer at runtime; the property is read-only here. +schema.glyphOrigin = { buffer: 'rect' }; diff --git a/packages/text/tests/fixtures/render-policy/hand-numbered-policy-bytes.json b/packages/text/tests/fixtures/render-policy/hand-numbered-policy-bytes.json new file mode 100644 index 00000000..d13f6990 --- /dev/null +++ b/packages/text/tests/fixtures/render-policy/hand-numbered-policy-bytes.json @@ -0,0 +1,6 @@ +{ + "direct/ordered": "yA4AACwAAAABAAAAVAAAAAQAAABUAQAAGgAAAPQCAACrAAAApA0AAEkAAAABAAAAPQAAAAAAAAQEAAAAgAAAAAABAAAQABAAAAAIAEwdAAAAAAAAjDt1FwEAAAAAAAAAAQAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAmAAEADwMBAP8AAAAAAAAAEgAAAP3kp/kCAAAAAAAAAAEAAAAAAAAABwAAAAAAAAAAAAAABwAAACYAAAAAAAgAOQABABEDAQD/AAAAEgAAABQAAAAIeSzyAwAAAAAAAAABAAAAAAAAAAcAAAAAAAAAAAAAAA8AAABfAAAAAAAIAD0AAQAQCAEA/wAAACYAAAAYAAAAgfpVNAQAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAXAAAAnAAAAAAAAwAPAAEABwQCAP8AAAA+AAAACwAAAAEAAQIEAAgABgAAAAEAAAACAAECBAAIAAYAAAABAAAAAwABAgQACAAGAAAAAQAAAAQAAQIEAAgABgAAAAEAAAAFAAEEBAAQAAYAAAABAAAABgACAQQABAAGAAAAAQAAAA4AAgEEAAQABgAAAAEAAAABAAEEBAAQAAYAAAABAAAAAgABBAQAEAAGAAAAAQAAAAMAAQQEABAABgAAAAEAAAAEAAEEBAAQAAYAAAABAAAABQABBAQAEAAGAAAAAQAAAAYAAQQEABAABgAAAAEAAAAHAAEEBAAQAAYAAAABAAAADgACAQQABAAGAAAAAQAAAAEAAQQEABAABgAAAAEAAAACAAEEBAAQAAYAAAABAAAAAwABBAQAEAAGAAAAAQAAAAQAAQQEABAABgAAAAEAAAAFAAEEBAAQAAYAAAABAAAABgACBAQAEAAGAAAAAQAAAAcAAgQEABAABgAAAAEAAAAOAAIBBAAEAAYAAAABAAAAAQABBAQAEAAGAAAAAQAAAAIAAgIEAAgABgAAAAEAAAAOAAIBBAAEAAYAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAAAAAAAAAABAgIAAAAAAAAAAAAAAAAAAQMDAAAAAAAAAAAAAAAAAAEEBAAAAAAAAAAAAAAAAAABBQUAAAAAAAAAAAAAAAAAAQYGAAAAAAAAAAAAAAAAAAEHBwAAAAAAAAAAAAAAAAABCAgAAAAAAAAAAAAAAAAAAQkJAAAAAAAAAAAAAAAAAAEKCgAAAAAAAAAAAAAAAAABCwsAAAAAAAAAAAAAAAAAAQwMAAAAAAAAAAAAAAAAAAENDQAAAAAAAAAAAAAAAAABDg4AAAAAAAAAAAAAAAAAAh8AAAAAAAAAAAAAAAAAAAIeAQAAAAAAAAAAAAAAAAACHQIAAAAAAAAAAAAAAAAABw8HAgAAAAAAAAAAAAAAAAUQAA8AAAAAAAAAAAAAAAAHEQgCAAAAAAAAAAAAAAAABhIBEQAAAAAAAAAAAAAAAAcTCQIAAAAAAAAAAAAAAAAHFAoCAAAAAAAAAAAAAAAACwAQAAEAAAAAAAAAAAAAAAsAEgEBAAAAAAAAAAAAAAALABMAAgAAAAAAAAAAAAAACwAUAQIAAAAAAAAAAAAAAAsACwADAAAAAAAAAAAAAAALAAwBAwAAAAAAAAAAAAAACwANAAQAAAAAAAAAAAAAAAsADgEEAAAAAAAAAAAAAAALAAMABQAAAAAAAAAAAAAACwAEAQUAAAAAAAAAAAAAAAsABQIFAAAAAAAAAAAAAAALAAYDBQAAAAAAAAAAAAAADAAeAA4AAAAAAAAAAAAAAAwAHQAGAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAECAgAAAAAAAAAAAAAAAAABAwMAAAAAAAAAAAAAAAAAAQQEAAAAAAAAAAAAAAAAAAEFBQAAAAAAAAAAAAAAAAABBgYAAAAAAAAAAAAAAAAAAQcHAAAAAAAAAAAAAAAAAAEICAAAAAAAAAAAAAAAAAABCQkAAAAAAAAAAAAAAAAAAQoKAAAAAAAAAAAAAAAAAAELCwAAAAAAAAAAAAAAAAABDAwAAAAAAAAAAAAAAAAAAQ0NAAAAAAAAAAAAAAAAAAEODgAAAAAAAAAAAAAAAAABDw8AAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAIRAgAAAAAAAAAAAAAAAAACHwAAAAAAAAAAAAAAAAAAAh4BAAAAAAAAAAAAAAAAAAcSBwIAAAAAAAAAAAAAAAAFEwASAAAAAAAAAAAAAAAABxQIAgAAAAAAAAAAAAAAAAYVARQAAAAAAAAAAAAAAAAHFgkCAAAAAAAAAAAAAAAABxcKAgAAAAAAAAAAAAAAAAoYEQAAAAAAAAAAAAAAAAADGQAAAAAAAAAAAAAAAAAACwATAAEAAAAAAAAAAAAAAAsAFQEBAAAAAAAAAAAAAAALABYCAQAAAAAAAAAAAAAACwAXAwEAAAAAAAAAAAAAAAsACwACAAAAAAAAAAAAAAALAAwBAgAAAAAAAAAAAAAACwANAgIAAAAAAAAAAAAAAAsADgMCAAAAAAAAAAAAAAALAAsAAwAAAAAAAAAAAAAACwAMAQMAAAAAAAAAAAAAAAsADwIDAAAAAAAAAAAAAAALABADAwAAAAAAAAAAAAAACwADAAQAAAAAAAAAAAAAAAsABAEEAAAAAAAAAAAAAAALAAUCBAAAAAAAAAAAAAAACwAGAwQAAAAAAAAAAAAAAAsAGQAFAAAAAAAAAAAAAAALABkBBQAAAAAAAAAAAAAACwAZAgUAAAAAAAAAAAAAAAsAGQMFAAAAAAAAAAAAAAALABkABgAAAAAAAAAAAAAACwAZAQYAAAAAAAAAAAAAAAsAGQIGAAAAAAAAAAAAAAALABkDBgAAAAAAAAAAAAAACwAZAAcAAAAAAAAAAAAAAAsAGQEHAAAAAAAAAAAAAAALABkCBwAAAAAAAAAAAAAACwAYAwcAAAAAAAAAAAAAAAwAHgAOAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAECAgAAAAAAAAAAAAAAAAABAwMAAAAAAAAAAAAAAAAAAQQEAAAAAAAAAAAAAAAAAAEFBQAAAAAAAAAAAAAAAAABBgYAAAAAAAAAAAAAAAAAAQcHAAAAAAAAAAAAAAAAAAEICAAAAAAAAAAAAAAAAAABCQkAAAAAAAAAAAAAAAAAAQoKAAAAAAAAAAAAAAAAAAELCwAAAAAAAAAAAAAAAAABDAwAAAAAAAAAAAAAAAAAAQ0NAAAAAAAAAAAAAAAAAAEODgAAAAAAAAAAAAAAAAABDw8AAAAAAAAAAAAAAAAAAh8AAAAAAAAAAAAAAAAAAAIeAQAAAAAAAAAAAAAAAAACFQIAAAAAAAAAAAAAAAAAAhYDAAAAAAAAAAAAAAAAAAIXBAAAAAAAAAAAAAAAAAACGAUAAAAAAAAAAAAAAAAAAhkGAAAAAAAAAAAAAAAAAAIaBwAAAAAAAAAAAAAAAAAHEAgCAAAAAAAAAAAAAAAABREAEAAAAAAAAAAAAAAAAAcSCQIAAAAAAAAAAAAAAAAGEwESAAAAAAAAAAAAAAAABxQKAgAAAAAAAAAAAAAAAAcbCwIAAAAAAAAAAAAAAAADHAAAAAAAAAAAAAAAAAAABB0AAAAAAAAAAAAAAAAAAAsAEQABAAAAAAAAAAAAAAALABMBAQAAAAAAAAAAAAAACwAUAgEAAAAAAAAAAAAAAAsAGwMBAAAAAAAAAAAAAAALAAgAAgAAAAAAAAAAAAAACwAJAQIAAAAAAAAAAAAAAAsACgICAAAAAAAAAAAAAAALAAsDAgAAAAAAAAAAAAAACwAMAAMAAAAAAAAAAAAAAAsADQEDAAAAAAAAAAAAAAALAA4CAwAAAAAAAAAAAAAACwAPAwMAAAAAAAAAAAAAAAsAAwAEAAAAAAAAAAAAAAALAAQBBAAAAAAAAAAAAAAACwAFAgQAAAAAAAAAAAAAAAsABgMEAAAAAAAAAAAAAAALAAcABQAAAAAAAAAAAAAACwAcAQUAAAAAAAAAAAAAAAsAHAIFAAAAAAAAAAAAAAALABwDBQAAAAAAAAAAAAAADAAVAAYAAAAAAAAAAAAAAAwAFgEGAAAAAAAAAAAAAAAMABcCBgAAAAAAAAAAAAAADAAYAwYAAAAAAAAAAAAAAAwAGQAHAAAAAAAAAAAAAAAMABoBBwAAAAAAAAAAAAAADAAdAgcAAAAAAAAAAAAAAAwAHQMHAAAAAAAAAAAAAAAMAB4ADgAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAAAAAAAAAABAgIAAAAAAAAAAAAAAAAAAQMDAAAAAAAAAAAAAAAAAAIcAAAAAAAAAAAAAAAAAAACHQEAAAAAAAAAAAAAAAAAAh4CAAAAAAAAAAAAAAAAAAIfAwAAAAAAAAAAAAAAAAALAAAAAQAAAAAAAAAAAAAACwABAQEAAAAAAAAAAAAAAAsAAgIBAAAAAAAAAAAAAAALAAMDAQAAAAAAAAAAAAAADAAeAAIAAAAAAAAAAAAAAAwAHwECAAAAAAAAAAAAAAAMAB0ADgAAAAAAAAAAAAAAAQYAAAEHAAABBAAAAQgAAAEJAAABCgAAAQsAAAQAAAAEAQAABAIAAAQDAAAEBAAABAUAAAQGAAAEBwAAAQQAAAEFAAAEAAAAAQYAAAEHAAABBAAAAQgAAAEJAAABCgAAAQsAAAIAAAACAQAAAgIAAAIDAAACBAAAAgUAAAIGAAACBwAAAggAAAIJAAABBAAAAQUAAAIAAAABBgAAAQcAAAEEAAABCAAAAQkAAAEKAAABCwAAAQwAAAIAAAACAQAAAgIAAAIDAAACBAAAAgUAAAIGAAACBwAAAQQAAAEFAAACAAAAAgEAAAICAAACAwAAAgQAAAIFAAABBgAAAQcAAAEEAAABCAAAAQkAAAEKAAABCwAAAQQAAAEFAAACAAAAAgEAAA==", + "direct/stable": "yA4AACwAAAABAAAAVAAAAAQAAABUAQAAGgAAAPQCAACrAAAApA0AAEkAAAABAAAAPQAAAAAAAAQEAAAAgAAAAAABAAAQABAAAAAIAEwdAAAAAAAAjDt1FwEAAAAAAAAAAQAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAmAAIADwMBAP8AAAAAAAAAEgAAAP3kp/kCAAAAAAAAAAEAAAAAAAAABwAAAAAAAAAAAAAABwAAACYAAAAAAAgAOQACABEDAQD/AAAAEgAAABQAAAAIeSzyAwAAAAAAAAABAAAAAAAAAAcAAAAAAAAAAAAAAA8AAABfAAAAAAAIAD0AAgAQCAEA/wAAACYAAAAYAAAAgfpVNAQAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAXAAAAnAAAAAAAAwAPAAIABwQCAP8AAAA+AAAACwAAAAEAAQIEAAgABgAAAAEAAAACAAECBAAIAAYAAAABAAAAAwABAgQACAAGAAAAAQAAAAQAAQIEAAgABgAAAAEAAAAFAAEEBAAQAAYAAAABAAAABgACAQQABAAGAAAAAQAAAA4AAgEEAAQABgAAAAEAAAABAAEEBAAQAAYAAAABAAAAAgABBAQAEAAGAAAAAQAAAAMAAQQEABAABgAAAAEAAAAEAAEEBAAQAAYAAAABAAAABQABBAQAEAAGAAAAAQAAAAYAAQQEABAABgAAAAEAAAAHAAEEBAAQAAYAAAABAAAADgACAQQABAAGAAAAAQAAAAEAAQQEABAABgAAAAEAAAACAAEEBAAQAAYAAAABAAAAAwABBAQAEAAGAAAAAQAAAAQAAQQEABAABgAAAAEAAAAFAAEEBAAQAAYAAAABAAAABgACBAQAEAAGAAAAAQAAAAcAAgQEABAABgAAAAEAAAAOAAIBBAAEAAYAAAABAAAAAQABBAQAEAAGAAAAAQAAAAIAAgIEAAgABgAAAAEAAAAOAAIBBAAEAAYAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAAAAAAAAAABAgIAAAAAAAAAAAAAAAAAAQMDAAAAAAAAAAAAAAAAAAEEBAAAAAAAAAAAAAAAAAABBQUAAAAAAAAAAAAAAAAAAQYGAAAAAAAAAAAAAAAAAAEHBwAAAAAAAAAAAAAAAAABCAgAAAAAAAAAAAAAAAAAAQkJAAAAAAAAAAAAAAAAAAEKCgAAAAAAAAAAAAAAAAABCwsAAAAAAAAAAAAAAAAAAQwMAAAAAAAAAAAAAAAAAAENDQAAAAAAAAAAAAAAAAABDg4AAAAAAAAAAAAAAAAAAh8AAAAAAAAAAAAAAAAAAAIeAQAAAAAAAAAAAAAAAAACHQIAAAAAAAAAAAAAAAAABw8HAgAAAAAAAAAAAAAAAAUQAA8AAAAAAAAAAAAAAAAHEQgCAAAAAAAAAAAAAAAABhIBEQAAAAAAAAAAAAAAAAcTCQIAAAAAAAAAAAAAAAAHFAoCAAAAAAAAAAAAAAAACwAQAAEAAAAAAAAAAAAAAAsAEgEBAAAAAAAAAAAAAAALABMAAgAAAAAAAAAAAAAACwAUAQIAAAAAAAAAAAAAAAsACwADAAAAAAAAAAAAAAALAAwBAwAAAAAAAAAAAAAACwANAAQAAAAAAAAAAAAAAAsADgEEAAAAAAAAAAAAAAALAAMABQAAAAAAAAAAAAAACwAEAQUAAAAAAAAAAAAAAAsABQIFAAAAAAAAAAAAAAALAAYDBQAAAAAAAAAAAAAADAAeAA4AAAAAAAAAAAAAAAwAHQAGAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAECAgAAAAAAAAAAAAAAAAABAwMAAAAAAAAAAAAAAAAAAQQEAAAAAAAAAAAAAAAAAAEFBQAAAAAAAAAAAAAAAAABBgYAAAAAAAAAAAAAAAAAAQcHAAAAAAAAAAAAAAAAAAEICAAAAAAAAAAAAAAAAAABCQkAAAAAAAAAAAAAAAAAAQoKAAAAAAAAAAAAAAAAAAELCwAAAAAAAAAAAAAAAAABDAwAAAAAAAAAAAAAAAAAAQ0NAAAAAAAAAAAAAAAAAAEODgAAAAAAAAAAAAAAAAABDw8AAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAIRAgAAAAAAAAAAAAAAAAACHwAAAAAAAAAAAAAAAAAAAh4BAAAAAAAAAAAAAAAAAAcSBwIAAAAAAAAAAAAAAAAFEwASAAAAAAAAAAAAAAAABxQIAgAAAAAAAAAAAAAAAAYVARQAAAAAAAAAAAAAAAAHFgkCAAAAAAAAAAAAAAAABxcKAgAAAAAAAAAAAAAAAAoYEQAAAAAAAAAAAAAAAAADGQAAAAAAAAAAAAAAAAAACwATAAEAAAAAAAAAAAAAAAsAFQEBAAAAAAAAAAAAAAALABYCAQAAAAAAAAAAAAAACwAXAwEAAAAAAAAAAAAAAAsACwACAAAAAAAAAAAAAAALAAwBAgAAAAAAAAAAAAAACwANAgIAAAAAAAAAAAAAAAsADgMCAAAAAAAAAAAAAAALAAsAAwAAAAAAAAAAAAAACwAMAQMAAAAAAAAAAAAAAAsADwIDAAAAAAAAAAAAAAALABADAwAAAAAAAAAAAAAACwADAAQAAAAAAAAAAAAAAAsABAEEAAAAAAAAAAAAAAALAAUCBAAAAAAAAAAAAAAACwAGAwQAAAAAAAAAAAAAAAsAGQAFAAAAAAAAAAAAAAALABkBBQAAAAAAAAAAAAAACwAZAgUAAAAAAAAAAAAAAAsAGQMFAAAAAAAAAAAAAAALABkABgAAAAAAAAAAAAAACwAZAQYAAAAAAAAAAAAAAAsAGQIGAAAAAAAAAAAAAAALABkDBgAAAAAAAAAAAAAACwAZAAcAAAAAAAAAAAAAAAsAGQEHAAAAAAAAAAAAAAALABkCBwAAAAAAAAAAAAAACwAYAwcAAAAAAAAAAAAAAAwAHgAOAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAECAgAAAAAAAAAAAAAAAAABAwMAAAAAAAAAAAAAAAAAAQQEAAAAAAAAAAAAAAAAAAEFBQAAAAAAAAAAAAAAAAABBgYAAAAAAAAAAAAAAAAAAQcHAAAAAAAAAAAAAAAAAAEICAAAAAAAAAAAAAAAAAABCQkAAAAAAAAAAAAAAAAAAQoKAAAAAAAAAAAAAAAAAAELCwAAAAAAAAAAAAAAAAABDAwAAAAAAAAAAAAAAAAAAQ0NAAAAAAAAAAAAAAAAAAEODgAAAAAAAAAAAAAAAAABDw8AAAAAAAAAAAAAAAAAAh8AAAAAAAAAAAAAAAAAAAIeAQAAAAAAAAAAAAAAAAACFQIAAAAAAAAAAAAAAAAAAhYDAAAAAAAAAAAAAAAAAAIXBAAAAAAAAAAAAAAAAAACGAUAAAAAAAAAAAAAAAAAAhkGAAAAAAAAAAAAAAAAAAIaBwAAAAAAAAAAAAAAAAAHEAgCAAAAAAAAAAAAAAAABREAEAAAAAAAAAAAAAAAAAcSCQIAAAAAAAAAAAAAAAAGEwESAAAAAAAAAAAAAAAABxQKAgAAAAAAAAAAAAAAAAcbCwIAAAAAAAAAAAAAAAADHAAAAAAAAAAAAAAAAAAABB0AAAAAAAAAAAAAAAAAAAsAEQABAAAAAAAAAAAAAAALABMBAQAAAAAAAAAAAAAACwAUAgEAAAAAAAAAAAAAAAsAGwMBAAAAAAAAAAAAAAALAAgAAgAAAAAAAAAAAAAACwAJAQIAAAAAAAAAAAAAAAsACgICAAAAAAAAAAAAAAALAAsDAgAAAAAAAAAAAAAACwAMAAMAAAAAAAAAAAAAAAsADQEDAAAAAAAAAAAAAAALAA4CAwAAAAAAAAAAAAAACwAPAwMAAAAAAAAAAAAAAAsAAwAEAAAAAAAAAAAAAAALAAQBBAAAAAAAAAAAAAAACwAFAgQAAAAAAAAAAAAAAAsABgMEAAAAAAAAAAAAAAALAAcABQAAAAAAAAAAAAAACwAcAQUAAAAAAAAAAAAAAAsAHAIFAAAAAAAAAAAAAAALABwDBQAAAAAAAAAAAAAADAAVAAYAAAAAAAAAAAAAAAwAFgEGAAAAAAAAAAAAAAAMABcCBgAAAAAAAAAAAAAADAAYAwYAAAAAAAAAAAAAAAwAGQAHAAAAAAAAAAAAAAAMABoBBwAAAAAAAAAAAAAADAAdAgcAAAAAAAAAAAAAAAwAHQMHAAAAAAAAAAAAAAAMAB4ADgAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAAAAAAAAAABAgIAAAAAAAAAAAAAAAAAAQMDAAAAAAAAAAAAAAAAAAIcAAAAAAAAAAAAAAAAAAACHQEAAAAAAAAAAAAAAAAAAh4CAAAAAAAAAAAAAAAAAAIfAwAAAAAAAAAAAAAAAAALAAAAAQAAAAAAAAAAAAAACwABAQEAAAAAAAAAAAAAAAsAAgIBAAAAAAAAAAAAAAALAAMDAQAAAAAAAAAAAAAADAAeAAIAAAAAAAAAAAAAAAwAHwECAAAAAAAAAAAAAAAMAB0ADgAAAAAAAAAAAAAAAQYAAAEHAAABBAAAAQgAAAEJAAABCgAAAQsAAAQAAAAEAQAABAIAAAQDAAAEBAAABAUAAAQGAAAEBwAAAQQAAAEFAAAEAAAAAQYAAAEHAAABBAAAAQgAAAEJAAABCgAAAQsAAAIAAAACAQAAAgIAAAIDAAACBAAAAgUAAAIGAAACBwAAAggAAAIJAAABBAAAAQUAAAIAAAABBgAAAQcAAAEEAAABCAAAAQkAAAEKAAABCwAAAQwAAAIAAAACAQAAAgIAAAIDAAACBAAAAgUAAAIGAAACBwAAAQQAAAEFAAACAAAAAgEAAAICAAACAwAAAgQAAAIFAAABBgAAAQcAAAEEAAABCAAAAQkAAAEKAAABCwAAAQQAAAEFAAACAAAAAgEAAA==", + "indexed/ordered": "SA8AACwAAAABAAAAVAAAAAQAAABUAQAAHgAAADQDAACvAAAAJA4AAEkAAAABAAAAPQAAAAAAAAQEAAAAgAAAAAABAAAQABAAAAAIAEwdAAAAAAAAjDt1FwEAAAAAAAAAAQAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAnAAEADwMBAH8AAAAAAAAAEgAAAP3kp/kCAAAAAAAAAAEAAAAAAAAABwAAAAAAAAAAAAAACAAAACcAAAAAAAkAOgABABEDAQB/AAAAEgAAABQAAAAIeSzyAwAAAAAAAAABAAAAAAAAAAcAAAAAAAAAAAAAABEAAABhAAAAAAAJAD4AAQAQCAEAfwAAACYAAAAYAAAAgfpVNAQAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAaAAAAnwAAAAAABAAQAAEABwQCAH8AAAA+AAAACwAAAAEAAQIEAAgABgAAAAEAAAACAAECBAAIAAYAAAABAAAAAwABAgQACAAGAAAAAQAAAAQAAQIEAAgABgAAAAEAAAAFAAEEBAAQAAYAAAABAAAABgACAQQABAAGAAAAAQAAAA4AAgEEAAQABgAAAAEAAAAPAAIBBAAEAAYAAAABAAAAAQABBAQAEAAGAAAAAQAAAAIAAQQEABAABgAAAAEAAAADAAEEBAAQAAYAAAABAAAABAABBAQAEAAGAAAAAQAAAAUAAQQEABAABgAAAAEAAAAGAAEEBAAQAAYAAAABAAAABwABBAQAEAAGAAAAAQAAAA4AAgEEAAQABgAAAAEAAAAPAAIBBAAEAAYAAAABAAAAAQABBAQAEAAGAAAAAQAAAAIAAQQEABAABgAAAAEAAAADAAEEBAAQAAYAAAABAAAABAABBAQAEAAGAAAAAQAAAAUAAQQEABAABgAAAAEAAAAGAAIEBAAQAAYAAAABAAAABwACBAQAEAAGAAAAAQAAAA4AAgEEAAQABgAAAAEAAAAPAAIBBAAEAAYAAAABAAAAAQABBAQAEAAGAAAAAQAAAAIAAgIEAAgABgAAAAEAAAAOAAIBBAAEAAYAAAABAAAADwACAQQABAAGAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAQEAAAAAAAAAAAAAAAAAAQICAAAAAAAAAAAAAAAAAAEDAwAAAAAAAAAAAAAAAAABBAQAAAAAAAAAAAAAAAAAAQUFAAAAAAAAAAAAAAAAAAEGBgAAAAAAAAAAAAAAAAABBwcAAAAAAAAAAAAAAAAAAQgIAAAAAAAAAAAAAAAAAAEJCQAAAAAAAAAAAAAAAAABCgoAAAAAAAAAAAAAAAAAAQsLAAAAAAAAAAAAAAAAAAEMDAAAAAAAAAAAAAAAAAABDQ0AAAAAAAAAAAAAAAAAAQ4OAAAAAAAAAAAAAAAAAAIfAAAAAAAAAAAAAAAAAAACHgEAAAAAAAAAAAAAAAAAAh0CAAAAAAAAAAAAAAAAAAcPBwIAAAAAAAAAAAAAAAAFEAAPAAAAAAAAAAAAAAAABxEIAgAAAAAAAAAAAAAAAAYSAREAAAAAAAAAAAAAAAAHEwkCAAAAAAAAAAAAAAAABxQKAgAAAAAAAAAAAAAAAAsAEAABAAAAAAAAAAAAAAALABIBAQAAAAAAAAAAAAAACwATAAIAAAAAAAAAAAAAAAsAFAECAAAAAAAAAAAAAAALAAsAAwAAAAAAAAAAAAAACwAMAQMAAAAAAAAAAAAAAAsADQAEAAAAAAAAAAAAAAALAA4BBAAAAAAAAAAAAAAACwADAAUAAAAAAAAAAAAAAAsABAEFAAAAAAAAAAAAAAALAAUCBQAAAAAAAAAAAAAACwAGAwUAAAAAAAAAAAAAAAwAHwAPAAAAAAAAAAAAAAAMAB4ADgAAAAAAAAAAAAAADAAdAAYAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAQEAAAAAAAAAAAAAAAAAAQICAAAAAAAAAAAAAAAAAAEDAwAAAAAAAAAAAAAAAAABBAQAAAAAAAAAAAAAAAAAAQUFAAAAAAAAAAAAAAAAAAEGBgAAAAAAAAAAAAAAAAABBwcAAAAAAAAAAAAAAAAAAQgIAAAAAAAAAAAAAAAAAAEJCQAAAAAAAAAAAAAAAAABCgoAAAAAAAAAAAAAAAAAAQsLAAAAAAAAAAAAAAAAAAEMDAAAAAAAAAAAAAAAAAABDQ0AAAAAAAAAAAAAAAAAAQ4OAAAAAAAAAAAAAAAAAAEPDwAAAAAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAhECAAAAAAAAAAAAAAAAAAIfAAAAAAAAAAAAAAAAAAACHgEAAAAAAAAAAAAAAAAABxIHAgAAAAAAAAAAAAAAAAUTABIAAAAAAAAAAAAAAAAHFAgCAAAAAAAAAAAAAAAABhUBFAAAAAAAAAAAAAAAAAcWCQIAAAAAAAAAAAAAAAAHFwoCAAAAAAAAAAAAAAAAChgRAAAAAAAAAAAAAAAAAAMZAAAAAAAAAAAAAAAAAAALABMAAQAAAAAAAAAAAAAACwAVAQEAAAAAAAAAAAAAAAsAFgIBAAAAAAAAAAAAAAALABcDAQAAAAAAAAAAAAAACwALAAIAAAAAAAAAAAAAAAsADAECAAAAAAAAAAAAAAALAA0CAgAAAAAAAAAAAAAACwAOAwIAAAAAAAAAAAAAAAsACwADAAAAAAAAAAAAAAALAAwBAwAAAAAAAAAAAAAACwAPAgMAAAAAAAAAAAAAAAsAEAMDAAAAAAAAAAAAAAALAAMABAAAAAAAAAAAAAAACwAEAQQAAAAAAAAAAAAAAAsABQIEAAAAAAAAAAAAAAALAAYDBAAAAAAAAAAAAAAACwAZAAUAAAAAAAAAAAAAAAsAGQEFAAAAAAAAAAAAAAALABkCBQAAAAAAAAAAAAAACwAZAwUAAAAAAAAAAAAAAAsAGQAGAAAAAAAAAAAAAAALABkBBgAAAAAAAAAAAAAACwAZAgYAAAAAAAAAAAAAAAsAGQMGAAAAAAAAAAAAAAALABkABwAAAAAAAAAAAAAACwAZAQcAAAAAAAAAAAAAAAsAGQIHAAAAAAAAAAAAAAALABgDBwAAAAAAAAAAAAAADAAfAA8AAAAAAAAAAAAAAAwAHgAOAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAECAgAAAAAAAAAAAAAAAAABAwMAAAAAAAAAAAAAAAAAAQQEAAAAAAAAAAAAAAAAAAEFBQAAAAAAAAAAAAAAAAABBgYAAAAAAAAAAAAAAAAAAQcHAAAAAAAAAAAAAAAAAAEICAAAAAAAAAAAAAAAAAABCQkAAAAAAAAAAAAAAAAAAQoKAAAAAAAAAAAAAAAAAAELCwAAAAAAAAAAAAAAAAABDAwAAAAAAAAAAAAAAAAAAQ0NAAAAAAAAAAAAAAAAAAEODgAAAAAAAAAAAAAAAAABDw8AAAAAAAAAAAAAAAAAAh8AAAAAAAAAAAAAAAAAAAIeAQAAAAAAAAAAAAAAAAACFQIAAAAAAAAAAAAAAAAAAhYDAAAAAAAAAAAAAAAAAAIXBAAAAAAAAAAAAAAAAAACGAUAAAAAAAAAAAAAAAAAAhkGAAAAAAAAAAAAAAAAAAIaBwAAAAAAAAAAAAAAAAAHEAgCAAAAAAAAAAAAAAAABREAEAAAAAAAAAAAAAAAAAcSCQIAAAAAAAAAAAAAAAAGEwESAAAAAAAAAAAAAAAABxQKAgAAAAAAAAAAAAAAAAcbCwIAAAAAAAAAAAAAAAADHAAAAAAAAAAAAAAAAAAABB0AAAAAAAAAAAAAAAAAAAsAEQABAAAAAAAAAAAAAAALABMBAQAAAAAAAAAAAAAACwAUAgEAAAAAAAAAAAAAAAsAGwMBAAAAAAAAAAAAAAALAAgAAgAAAAAAAAAAAAAACwAJAQIAAAAAAAAAAAAAAAsACgICAAAAAAAAAAAAAAALAAsDAgAAAAAAAAAAAAAACwAMAAMAAAAAAAAAAAAAAAsADQEDAAAAAAAAAAAAAAALAA4CAwAAAAAAAAAAAAAACwAPAwMAAAAAAAAAAAAAAAsAAwAEAAAAAAAAAAAAAAALAAQBBAAAAAAAAAAAAAAACwAFAgQAAAAAAAAAAAAAAAsABgMEAAAAAAAAAAAAAAALAAcABQAAAAAAAAAAAAAACwAcAQUAAAAAAAAAAAAAAAsAHAIFAAAAAAAAAAAAAAALABwDBQAAAAAAAAAAAAAADAAVAAYAAAAAAAAAAAAAAAwAFgEGAAAAAAAAAAAAAAAMABcCBgAAAAAAAAAAAAAADAAYAwYAAAAAAAAAAAAAAAwAGQAHAAAAAAAAAAAAAAAMABoBBwAAAAAAAAAAAAAADAAdAgcAAAAAAAAAAAAAAAwAHQMHAAAAAAAAAAAAAAAMAB8ADwAAAAAAAAAAAAAADAAeAA4AAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAQEAAAAAAAAAAAAAAAAAAQICAAAAAAAAAAAAAAAAAAEDAwAAAAAAAAAAAAAAAAACHAAAAAAAAAAAAAAAAAAAAh0BAAAAAAAAAAAAAAAAAAIeAgAAAAAAAAAAAAAAAAACHwMAAAAAAAAAAAAAAAAACwAAAAEAAAAAAAAAAAAAAAsAAQEBAAAAAAAAAAAAAAALAAICAQAAAAAAAAAAAAAACwADAwEAAAAAAAAAAAAAAAwAHgACAAAAAAAAAAAAAAAMAB8BAgAAAAAAAAAAAAAADAAcAA8AAAAAAAAAAAAAAAwAHQAOAAAAAAAAAAAAAAABBgAAAQcAAAEEAAABCAAAAQkAAAEKAAABCwAABAAAAAQBAAAEAgAABAMAAAQEAAAEBQAABAYAAAQHAAABBAAAAQUAAAQAAAABBgAAAQcAAAEEAAABCAAAAQkAAAEKAAABCwAAAgAAAAIBAAACAgAAAgMAAAIEAAACBQAAAgYAAAIHAAACCAAAAgkAAAEEAAABBQAAAgAAAAEGAAABBwAAAQQAAAEIAAABCQAAAQoAAAELAAABDAAAAgAAAAIBAAACAgAAAgMAAAIEAAACBQAAAgYAAAIHAAABBAAAAQUAAAIAAAACAQAAAgIAAAIDAAACBAAAAgUAAAEGAAABBwAAAQQAAAEIAAABCQAAAQoAAAELAAABBAAAAQUAAAIAAAACAQAA", + "indexed/stable": "SA8AACwAAAABAAAAVAAAAAQAAABUAQAAHgAAADQDAACvAAAAJA4AAEkAAAABAAAAPQAAAAAAAAQEAAAAgAAAAAABAAAQABAAAAAIAEwdAAAAAAAAjDt1FwEAAAAAAAAAAQAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAnAAIADwMBAH8AAAAAAAAAEgAAAP3kp/kCAAAAAAAAAAEAAAAAAAAABwAAAAAAAAAAAAAACAAAACcAAAAAAAkAOgACABEDAQB/AAAAEgAAABQAAAAIeSzyAwAAAAAAAAABAAAAAAAAAAcAAAAAAAAAAAAAABEAAABhAAAAAAAJAD4AAgAQCAEAfwAAACYAAAAYAAAAgfpVNAQAAAAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAaAAAAnwAAAAAABAAQAAIABwQCAH8AAAA+AAAACwAAAAEAAQIEAAgABgAAAAEAAAACAAECBAAIAAYAAAABAAAAAwABAgQACAAGAAAAAQAAAAQAAQIEAAgABgAAAAEAAAAFAAEEBAAQAAYAAAABAAAABgACAQQABAAGAAAAAQAAAA4AAgEEAAQABgAAAAEAAAAPAAIBBAAEAAYAAAABAAAAAQABBAQAEAAGAAAAAQAAAAIAAQQEABAABgAAAAEAAAADAAEEBAAQAAYAAAABAAAABAABBAQAEAAGAAAAAQAAAAUAAQQEABAABgAAAAEAAAAGAAEEBAAQAAYAAAABAAAABwABBAQAEAAGAAAAAQAAAA4AAgEEAAQABgAAAAEAAAAPAAIBBAAEAAYAAAABAAAAAQABBAQAEAAGAAAAAQAAAAIAAQQEABAABgAAAAEAAAADAAEEBAAQAAYAAAABAAAABAABBAQAEAAGAAAAAQAAAAUAAQQEABAABgAAAAEAAAAGAAIEBAAQAAYAAAABAAAABwACBAQAEAAGAAAAAQAAAA4AAgEEAAQABgAAAAEAAAAPAAIBBAAEAAYAAAABAAAAAQABBAQAEAAGAAAAAQAAAAIAAgIEAAgABgAAAAEAAAAOAAIBBAAEAAYAAAABAAAADwACAQQABAAGAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAQEAAAAAAAAAAAAAAAAAAQICAAAAAAAAAAAAAAAAAAEDAwAAAAAAAAAAAAAAAAABBAQAAAAAAAAAAAAAAAAAAQUFAAAAAAAAAAAAAAAAAAEGBgAAAAAAAAAAAAAAAAABBwcAAAAAAAAAAAAAAAAAAQgIAAAAAAAAAAAAAAAAAAEJCQAAAAAAAAAAAAAAAAABCgoAAAAAAAAAAAAAAAAAAQsLAAAAAAAAAAAAAAAAAAEMDAAAAAAAAAAAAAAAAAABDQ0AAAAAAAAAAAAAAAAAAQ4OAAAAAAAAAAAAAAAAAAIfAAAAAAAAAAAAAAAAAAACHgEAAAAAAAAAAAAAAAAAAh0CAAAAAAAAAAAAAAAAAAcPBwIAAAAAAAAAAAAAAAAFEAAPAAAAAAAAAAAAAAAABxEIAgAAAAAAAAAAAAAAAAYSAREAAAAAAAAAAAAAAAAHEwkCAAAAAAAAAAAAAAAABxQKAgAAAAAAAAAAAAAAAAsAEAABAAAAAAAAAAAAAAALABIBAQAAAAAAAAAAAAAACwATAAIAAAAAAAAAAAAAAAsAFAECAAAAAAAAAAAAAAALAAsAAwAAAAAAAAAAAAAACwAMAQMAAAAAAAAAAAAAAAsADQAEAAAAAAAAAAAAAAALAA4BBAAAAAAAAAAAAAAACwADAAUAAAAAAAAAAAAAAAsABAEFAAAAAAAAAAAAAAALAAUCBQAAAAAAAAAAAAAACwAGAwUAAAAAAAAAAAAAAAwAHwAPAAAAAAAAAAAAAAAMAB4ADgAAAAAAAAAAAAAADAAdAAYAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAQEAAAAAAAAAAAAAAAAAAQICAAAAAAAAAAAAAAAAAAEDAwAAAAAAAAAAAAAAAAABBAQAAAAAAAAAAAAAAAAAAQUFAAAAAAAAAAAAAAAAAAEGBgAAAAAAAAAAAAAAAAABBwcAAAAAAAAAAAAAAAAAAQgIAAAAAAAAAAAAAAAAAAEJCQAAAAAAAAAAAAAAAAABCgoAAAAAAAAAAAAAAAAAAQsLAAAAAAAAAAAAAAAAAAEMDAAAAAAAAAAAAAAAAAABDQ0AAAAAAAAAAAAAAAAAAQ4OAAAAAAAAAAAAAAAAAAEPDwAAAAAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAhECAAAAAAAAAAAAAAAAAAIfAAAAAAAAAAAAAAAAAAACHgEAAAAAAAAAAAAAAAAABxIHAgAAAAAAAAAAAAAAAAUTABIAAAAAAAAAAAAAAAAHFAgCAAAAAAAAAAAAAAAABhUBFAAAAAAAAAAAAAAAAAcWCQIAAAAAAAAAAAAAAAAHFwoCAAAAAAAAAAAAAAAAChgRAAAAAAAAAAAAAAAAAAMZAAAAAAAAAAAAAAAAAAALABMAAQAAAAAAAAAAAAAACwAVAQEAAAAAAAAAAAAAAAsAFgIBAAAAAAAAAAAAAAALABcDAQAAAAAAAAAAAAAACwALAAIAAAAAAAAAAAAAAAsADAECAAAAAAAAAAAAAAALAA0CAgAAAAAAAAAAAAAACwAOAwIAAAAAAAAAAAAAAAsACwADAAAAAAAAAAAAAAALAAwBAwAAAAAAAAAAAAAACwAPAgMAAAAAAAAAAAAAAAsAEAMDAAAAAAAAAAAAAAALAAMABAAAAAAAAAAAAAAACwAEAQQAAAAAAAAAAAAAAAsABQIEAAAAAAAAAAAAAAALAAYDBAAAAAAAAAAAAAAACwAZAAUAAAAAAAAAAAAAAAsAGQEFAAAAAAAAAAAAAAALABkCBQAAAAAAAAAAAAAACwAZAwUAAAAAAAAAAAAAAAsAGQAGAAAAAAAAAAAAAAALABkBBgAAAAAAAAAAAAAACwAZAgYAAAAAAAAAAAAAAAsAGQMGAAAAAAAAAAAAAAALABkABwAAAAAAAAAAAAAACwAZAQcAAAAAAAAAAAAAAAsAGQIHAAAAAAAAAAAAAAALABgDBwAAAAAAAAAAAAAADAAfAA8AAAAAAAAAAAAAAAwAHgAOAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAAAAAAECAgAAAAAAAAAAAAAAAAABAwMAAAAAAAAAAAAAAAAAAQQEAAAAAAAAAAAAAAAAAAEFBQAAAAAAAAAAAAAAAAABBgYAAAAAAAAAAAAAAAAAAQcHAAAAAAAAAAAAAAAAAAEICAAAAAAAAAAAAAAAAAABCQkAAAAAAAAAAAAAAAAAAQoKAAAAAAAAAAAAAAAAAAELCwAAAAAAAAAAAAAAAAABDAwAAAAAAAAAAAAAAAAAAQ0NAAAAAAAAAAAAAAAAAAEODgAAAAAAAAAAAAAAAAABDw8AAAAAAAAAAAAAAAAAAh8AAAAAAAAAAAAAAAAAAAIeAQAAAAAAAAAAAAAAAAACFQIAAAAAAAAAAAAAAAAAAhYDAAAAAAAAAAAAAAAAAAIXBAAAAAAAAAAAAAAAAAACGAUAAAAAAAAAAAAAAAAAAhkGAAAAAAAAAAAAAAAAAAIaBwAAAAAAAAAAAAAAAAAHEAgCAAAAAAAAAAAAAAAABREAEAAAAAAAAAAAAAAAAAcSCQIAAAAAAAAAAAAAAAAGEwESAAAAAAAAAAAAAAAABxQKAgAAAAAAAAAAAAAAAAcbCwIAAAAAAAAAAAAAAAADHAAAAAAAAAAAAAAAAAAABB0AAAAAAAAAAAAAAAAAAAsAEQABAAAAAAAAAAAAAAALABMBAQAAAAAAAAAAAAAACwAUAgEAAAAAAAAAAAAAAAsAGwMBAAAAAAAAAAAAAAALAAgAAgAAAAAAAAAAAAAACwAJAQIAAAAAAAAAAAAAAAsACgICAAAAAAAAAAAAAAALAAsDAgAAAAAAAAAAAAAACwAMAAMAAAAAAAAAAAAAAAsADQEDAAAAAAAAAAAAAAALAA4CAwAAAAAAAAAAAAAACwAPAwMAAAAAAAAAAAAAAAsAAwAEAAAAAAAAAAAAAAALAAQBBAAAAAAAAAAAAAAACwAFAgQAAAAAAAAAAAAAAAsABgMEAAAAAAAAAAAAAAALAAcABQAAAAAAAAAAAAAACwAcAQUAAAAAAAAAAAAAAAsAHAIFAAAAAAAAAAAAAAALABwDBQAAAAAAAAAAAAAADAAVAAYAAAAAAAAAAAAAAAwAFgEGAAAAAAAAAAAAAAAMABcCBgAAAAAAAAAAAAAADAAYAwYAAAAAAAAAAAAAAAwAGQAHAAAAAAAAAAAAAAAMABoBBwAAAAAAAAAAAAAADAAdAgcAAAAAAAAAAAAAAAwAHQMHAAAAAAAAAAAAAAAMAB8ADwAAAAAAAAAAAAAADAAeAA4AAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAQEAAAAAAAAAAAAAAAAAAQICAAAAAAAAAAAAAAAAAAEDAwAAAAAAAAAAAAAAAAACHAAAAAAAAAAAAAAAAAAAAh0BAAAAAAAAAAAAAAAAAAIeAgAAAAAAAAAAAAAAAAACHwMAAAAAAAAAAAAAAAAACwAAAAEAAAAAAAAAAAAAAAsAAQEBAAAAAAAAAAAAAAALAAICAQAAAAAAAAAAAAAACwADAwEAAAAAAAAAAAAAAAwAHgACAAAAAAAAAAAAAAAMAB8BAgAAAAAAAAAAAAAADAAcAA8AAAAAAAAAAAAAAAwAHQAOAAAAAAAAAAAAAAABBgAAAQcAAAEEAAABCAAAAQkAAAEKAAABCwAABAAAAAQBAAAEAgAABAMAAAQEAAAEBQAABAYAAAQHAAABBAAAAQUAAAQAAAABBgAAAQcAAAEEAAABCAAAAQkAAAEKAAABCwAAAgAAAAIBAAACAgAAAgMAAAIEAAACBQAAAgYAAAIHAAACCAAAAgkAAAEEAAABBQAAAgAAAAEGAAABBwAAAQQAAAEIAAABCQAAAQoAAAELAAABDAAAAgAAAAIBAAACAgAAAgMAAAIEAAACBQAAAgYAAAIHAAABBAAAAQUAAAIAAAACAQAAAgIAAAIDAAACBAAAAgUAAAEGAAABBwAAAQQAAAEIAAABCQAAAQoAAAELAAABBAAAAQUAAAIAAAACAQAA" +}