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, separatepub trait Backendwith 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_fingerprintcomputes FNV-1a overMAGIC ‖ Version::to_abi()~/dev/fera/chroix-vello/src/backend.rs:3-5— “This will eventually move intochroix-core; it lives here for now”~/dev/fera/chroix-headless/src/lib.rs:52— “Defined here for v0; will move tochroix-coreonce 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’spresent/schema_fingerprint
Sum (Суть)
Section titled “Sum (Суть)”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 — borrowedpub struct PresentCtx<'a> { pub brushes: &'a [Rgba], pub transforms: &'a [Transform] }
// chroix-headless/src/lib.rs:26 — ownedpub 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 “Oncechroix-coreownsBackendwe 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.
3. No test can catch it
Section titled “3. No test can catch it”The tests only check each backend against itself:
chroix-vello/tests/cpu_backend.rs:217-222— asserts the fingerprint equalsABI_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.
Options (with tradeoffs)
Section titled “Options (with tradeoffs)”- A. Move
Backend+PresentCtxintochroix-contractnow. This is TODO.md’s own 2f, and it is the correct crate:chroix-project/README.md:13already 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 onePresentCtxshape — the borrowed&'a [Rgba]is the better fit for a host-leased scratch model (V2:301-302), and it makes “no tables inPresentCtx” (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_fingerprintis a schema hash or an ABI version, implement it once in the contract crate as aconst fnoverMAGIC/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.
Proposed solution
Section titled “Proposed solution”A + B together, B first. The placement fix alone is insufficient — moving a trait whose fingerprint method is incoherent just relocates the incoherence. So:
- Define
schema_fingerprint’s semantics and implement it once, inchroix-contract, as aconst fnderived fromMAGIC+ABI_VERSION. Both backends delegate to it. This makes cross-backend agreement a compile-time property rather than a test. - Move
Backend+PresentCtxintochroix-contract(this is already TODO.md 2f, and the crate is the designated home for boundary types perREADME.md:13). Delete both local copies. - 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.