Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

The backend seam has two incompatible PresentCtx types and three disagreeing schema_fingerprint values

Источник: dev/fera-research/docs/oc2/backend-seam-duplicated-ctx-and-fingerprint-disagreement.md (read-only; сгенерировано scripts/sync-docs.mjs)

The backend seam has two incompatible PresentCtx types and three disagreeing schema_fingerprint values

Section titled “The backend seam has two incompatible PresentCtx types and three disagreeing schema_fingerprint values”
  • kind: problem
  • area: ui
  • severity: high
  • evidence:
    • ~/dev/fera/chroix-vello/src/backend.rs:21-24 — pub struct PresentCtx<'a> { pub brushes: &'a [Rgba], pub transforms: &'a [Transform] } (borrowed)
    • ~/dev/fera/chroix-headless/src/lib.rs:26-30 — pub struct PresentCtx { pub brushes: Vec<Rgba>, pub transforms: Vec<Transform> } (owned)
    • ~/dev/fera/chroix-vello/src/backend.rs:41,55 — pub trait Backend / fn present(&mut self, buf: &[u8], ctx: &mut PresentCtx)
    • ~/dev/fera/chroix-headless/src/lib.rs:53,64 — a second, separate pub trait Backend with the same signature text
    • ~/dev/fera/chroix-vello/src/cpu.rs:209-215 — fn schema_fingerprint(&self) -> u64 { u64::from(ABI_VERSION) }
    • ~/dev/fera/chroix-vello/src/gpu.rs:98-100 — fn schema_fingerprint(&self) -> u64 { u64::from(ABI_VERSION) }
    • ~/dev/fera/chroix-headless/src/lib.rs:369-382 — fn schema_fingerprint computes FNV-1a over MAGIC ‖ Version::to_abi()
    • ~/dev/fera/chroix-vello/src/backend.rs:3-5 — “This will eventually move into chroix-core; it lives here for now”
    • ~/dev/fera/chroix-headless/src/lib.rs:52 — “Defined here for v0; will move to chroix-core once that crate exists”
    • ~/dev/fera/chroix/TODO.md:47-50 (G5) — records where the trait lives, not that the copies disagree
    • ~/dev/fera/chroix-project/CHROIX-DISPLAY-LIST.md:288,295 — spec’s present/schema_fingerprint

chroix/TODO.md G5 already flags that “Backend/PresentCtx live in chroix-headless … A trait only one backend can see is not a seam.” That is the placement problem. The deeper problem is that the two copies are not interchangeable, and the one mechanism the contract provides to detect a mismatch already reports a mismatch — and no test can see it.

1. Two PresentCtx types, incompatible lifetimes

Section titled “1. Two PresentCtx types, incompatible lifetimes”

The Backend::present signatures are textually identical in both crates, which makes this easy to miss. The context types behind them are not the same type:

// chroix-vello/src/backend.rs:21 — borrowed
pub struct PresentCtx<'a> { pub brushes: &'a [Rgba], pub transforms: &'a [Transform] }
// chroix-headless/src/lib.rs:26 — owned
pub struct PresentCtx { pub brushes: Vec<Rgba>, pub transforms: Vec<Transform> }

One takes a lifetime parameter and slices; the other owns its tables. Headless does not implement chroix_vello::Backend, and no generic code can be written over “the” trait. For a framework whose stated purpose is “Make Vello, tiny-skia, and headless truly interchangeable” (UI-FRAMEWORK-IDEAS-V2.md:57), the interchangeability is currently two look-alike traits.

2. schema_fingerprint — the compatibility check — returns three different answers

Section titled “2. schema_fingerprint — the compatibility check — returns three different answers”

This is the sharpest part. CHROIX-DISPLAY-LIST.md:294-295 defines schema_fingerprint() as the contract’s own guard: “Schema fingerprint this backend was built against. The host checks it against the buffer’s abi_version before calling present.” It is the only cross-backend compatibility check in the entire design.

Three implementations, two algorithms, and they cannot agree:

Backend file:line value
VelloCpu cpu.rs:209-215 u64::from(ABI_VERSION) → 1
VelloGpu gpu.rs:98-100 u64::from(ABI_VERSION) → 1
Headless chroix-headless/src/lib.rs:369-382 FNV-1a over MAGIC ‖ Version::to_abi()

So a host implementing the spec’s check literally — compare schema_fingerprint() against the buffer’s abi_version — would compute 1 from VelloCpu and a large FNV hash from Headless. The two backends of the same contract would report incompatible schemas to the host on day one, over a buffer both of them decode successfully today.

That is not a theoretical mismatch. It means either:

  • the field’s semantics are undecided (is it a version, or a hash of the schema?), and each backend guessed differently; or
  • the vello impl is a placeholder nobody intends to keep (its own comment, cpu.rs:210-213, says “Once chroix-core owns Backend we can move to a proper hash-of-schema fingerprint”).

Either way, the one guard in the contract is currently wrong in a way that will surface as a spurious “backend mismatch” the moment a host implements it.

The tests only check each backend against itself:

  • chroix-vello/tests/cpu_backend.rs:217-222 — asserts the fingerprint equals ABI_VERSION.
  • chroix-headless/tests/raster.rs:241-246 — asserts stability and != 0.

Nothing compares the two values, because nothing can — they are values behind two unrelated traits in two unrelated crates. This is the direct cost of the duplication, and it is exactly what TODO.md 2g (fixture::reference_scene()) is meant to fix.

4. The promised move never happened, and chroix-core is ready for it

Section titled “4. The promised move never happened, and chroix-core is ready for it”

Both copies document that the trait will move into chroix-core. chroix-core exists (493 LOC, 4 commits) and has no Backend trait. The move was deferred to “once that crate exists”; the crate now exists and the deferral was not revisited.

  • A. Move Backend + PresentCtx into chroix-contract now. This is TODO.md’s own 2f, and it is the correct crate: chroix-project/README.md:13 already assigns the contract crate “Display-list schema, boundary types, encode/decode”. Both backends already depend on it (Cargo.toml:16 / :18), so this is cycle-free. Requires picking one PresentCtx shape — the borrowed &'a [Rgba] is the better fit for a host-leased scratch model (V2:301-302), and it makes “no tables in PresentCtx” (TODO.md 2f’s actual point) reachable. Cost: touches three crates, mechanical.
  • B. Define the fingerprint semantics first, then move. Fix the meaning before the location: decide whether schema_fingerprint is a schema hash or an ABI version, implement it once in the contract crate as a const fn over MAGIC/ABI_VERSION, and have both backends return it. Cost: a design decision, but it is a 5-minute one and B should precede or accompany A.
  • C. Leave it, add a cross-backend test. Impossible as things stand — the values live behind two unrelated traits. Only becomes possible after A/B.

A + B together, B first. The placement fix alone is insufficient — moving a trait whose fingerprint method is incoherent just relocates the incoherence. So:

  1. Define schema_fingerprint’s semantics and implement it once, in chroix-contract, as a const fn derived from MAGIC + ABI_VERSION. Both backends delegate to it. This makes cross-backend agreement a compile-time property rather than a test.
  2. Move Backend + PresentCtx into chroix-contract (this is already TODO.md 2f, and the crate is the designated home for boundary types per README.md:13). Delete both local copies.
  3. Then TODO.md 2g’s fixture::reference_scene() becomes a real conformance test — two backends, one byte buffer, and now a fingerprint they must agree on.

This is the highest-leverage item in the whole contract workstream, because it is the thing that makes “one contract, two backends” a type-level guarantee instead of a convention. Note the nice property: because both backends already depend on the contract crate, moving the trait there adds no new dependency edge — it removes one duplicated abstraction and its drift.