epiphany/spikes/editor-toolkit/round1-candidates/harness/src/lib.rs

639 lines
26 KiB
Rust

//! Round 1 shared harness: oracle loading + pixel-classification / report
//! plumbing common to both candidate binaries (`c1-egui-lyon`,
//! `c2-vello`). See the crate doc comment in `Cargo.toml` for why this crate
//! carries no GPU/wgpu dependency of its own.
use std::path::PathBuf;
use epiphany_glyphs::BravuraGlyphCatalog;
use epiphany_layout_ir::{GlyphCatalog, PathCommand};
use serde::{Deserialize, Serialize};
// ---------------------------------------------------------------------
// Oracle model (mirrors round1-oracle's `GlyphOracle` JSON shape exactly;
// this crate does NOT depend on the round1-oracle crate itself — it is a
// frozen, committed artifact, and re-deriving its Rust types independently
// here means a shape drift is a deserialization error, not a silent
// coupling).
//
// `deny_unknown_fields` is what actually makes that true: serde IGNORES
// unknown fields by default, so without it a field added to the oracle would
// deserialize silently and the "drift is an error" claim above would be
// false. Structural agreement is checked here; SEMANTIC agreement — that the
// oracle is internally coherent and complete — is checked by
// `OracleFile::validate`, which every candidate must call before rendering.
// ---------------------------------------------------------------------
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OracleFile {
pub contract: String,
pub round: String,
pub render_transform_rule: String,
pub flatten_tolerance_staff_space: f64,
pub clearance_floor_device_px: f64,
pub glyphs: Vec<GlyphOracle>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub enum Requirement {
BoundedHole,
DisjointComponents,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub enum SampleClass {
Ink,
Background,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Transform {
pub scale: f64,
pub tx: f64,
pub ty: f64,
pub target_width: f64,
pub target_height: f64,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HoleEvidence {
pub inside_outer_contour: bool,
pub even_odd_filled: bool,
pub nonzero_filled: bool,
pub outer_contour_ring_index: usize,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SamplePoint {
pub staff: (f64, f64),
pub device: (f64, f64),
pub class: SampleClass,
pub clearance_device_px: f64,
pub hole_evidence: Option<HoleEvidence>,
pub subpath_index: Option<usize>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HoleDiagnostic {
pub raw_hole_grid_hits: u64,
pub best_hole_clearance_device_px: Option<f64>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GlyphOracle {
pub name: String,
pub requirement: Requirement,
pub subpath_count: usize,
pub expected_subpath_count: Option<usize>,
pub transform: Transform,
pub bbox_staff: [f64; 4],
pub outer_contour_ring_index: usize,
pub ring_signed_areas: Vec<f64>,
pub points: Vec<SamplePoint>,
pub ink_candidates_found: usize,
pub background_candidates_found: usize,
pub ink_spacing_relaxed: bool,
pub background_spacing_relaxed: bool,
pub ink_satisfied: bool,
pub background_required: bool,
pub background_satisfied: bool,
pub subpath_coverage_required: bool,
pub subpath_coverage_satisfied: bool,
pub satisfied: bool,
pub hole_diagnostic: HoleDiagnostic,
}
// ---------------------------------------------------------------------
// The Round 1 roster, restated here as literals.
//
// These are NOT read from the oracle — restating them is the entire point.
// A validator that only checks the oracle against itself accepts any
// self-consistent oracle, including one with a glyph deleted, a glyph renamed,
// or every target resized together. The contract (Revision 6) names this exact
// roster, so the harness names it too, and a candidate refuses to render
// against anything else.
// ---------------------------------------------------------------------
/// `(name, requirement, subpath_count, point_count)` — CONTRACT_EDITOR_T4_SPIKE
/// Revision 6, Round 1. Four bounded-hole glyphs at 3 ink + 3 hole background
/// each, plus `fClef` at one tagged ink point per disjoint component.
const ROUND1_ROSTER: [(&str, Requirement, usize, usize); 5] = [
("gClef", Requirement::BoundedHole, 4, 6),
("timeSig8", Requirement::BoundedHole, 3, 6),
("accidentalFlat", Requirement::BoundedHole, 2, 6),
("noteheadHalf", Requirement::BoundedHole, 2, 6),
("fClef", Requirement::DisjointComponents, 3, 3),
];
/// Total sample points across the roster: 4 x 6 + 3. Checked as a census so a
/// point silently dropped from one glyph cannot be absorbed by the per-glyph
/// minimums.
const ROUND1_POINT_CENSUS: usize = 27;
/// Pin 4's offscreen target. Exact, not "whatever the oracle agrees on with
/// itself": scaling every glyph's target together stays self-consistent while
/// changing the rasterization the round is meant to compare.
const TARGET_WIDTH: f64 = 1920.0;
const TARGET_HEIGHT: f64 = 1080.0;
/// Minimum distance from any sample point to the nearest outline edge. This is
/// what makes the >= 8 px claim load-bearing: below it, a sample can land in
/// the antialiased band, where a correct render legitimately produces a
/// mid-grey and the luminance threshold decides the verdict instead of the
/// geometry.
const CLEARANCE_FLOOR_DEVICE_PX: f64 = 8.0;
/// Loads the frozen `round1-oracle/oracle.json` relative to this crate's
/// manifest directory (`round1-candidates/harness/../../round1-oracle`).
/// Never writes to that path; read-only.
pub fn load_oracle() -> OracleFile {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("../../round1-oracle/oracle.json");
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read oracle at {}: {e}", path.display()));
serde_json::from_str(&text)
.unwrap_or_else(|e| panic!("failed to parse oracle at {}: {e}", path.display()))
}
impl OracleFile {
/// Checks the oracle is internally coherent and complete **before** any
/// candidate renders against it.
///
/// `deny_unknown_fields` catches *structural* drift; this catches semantic
/// drift, which is the dangerous kind: an oracle that deserializes cleanly
/// but whose expectations no longer mean what Round 1 requires would be
/// tested against faithfully and pass, and the run would look green.
///
/// Every expectation below is checked against a **literal restated here**
/// (`ROUND1_ROSTER`, `TARGET_WIDTH`/`_HEIGHT`, `CLEARANCE_FLOOR_DEVICE_PX`,
/// `ROUND1_POINT_CENSUS`) rather than against the oracle's own other
/// fields. An earlier version compared each glyph's target to the *first
/// glyph's* target and each subpath count to the oracle's own
/// `expected_subpath_count`, which accepts any self-consistent file:
/// deleting `noteheadHalf`, renaming a glyph, or resizing every target
/// together all validated cleanly.
///
/// Enforced:
/// - the glyph set is exactly the five roster names, no duplicates, each
/// with its roster requirement, subpath count, and point count, and 27
/// points in total;
/// - every target is exactly 1920x1080;
/// - the declared clearance floor is 8 px and every sample meets it;
/// - no glyph's point search fell back to relaxed spacing;
/// - every glyph is `satisfied`, with `ink_satisfied` and the
/// requirement-specific status flags coherent for its class;
/// - hole glyphs carry >= 3 ink and >= 3 bounded-hole background points,
/// with hole evidence on each;
/// - `fClef`-class glyphs carry no background requirement and >= 1 tagged
/// ink point in **every** subpath.
pub fn validate(&self) -> Result<(), String> {
if self.round != "round1" {
return Err(format!(
"oracle declares round {:?}, not \"round1\"",
self.round
));
}
if self.clearance_floor_device_px != CLEARANCE_FLOOR_DEVICE_PX {
return Err(format!(
"oracle declares a clearance floor of {} device px, not {CLEARANCE_FLOOR_DEVICE_PX} \
— lowering it lets samples sit in the antialiased band, where the threshold, not \
the geometry, decides the verdict",
self.clearance_floor_device_px
));
}
// Exact set equality, in both directions: same length, every roster
// name present, and no name appearing twice (so a duplicate cannot
// stand in for a deleted glyph and keep the count right).
if self.glyphs.len() != ROUND1_ROSTER.len() {
let names: Vec<&str> = self.glyphs.iter().map(|g| g.name.as_str()).collect();
return Err(format!(
"oracle carries {} glyphs {names:?}, not the {} Round 1 requires",
self.glyphs.len(),
ROUND1_ROSTER.len()
));
}
for (i, g) in self.glyphs.iter().enumerate() {
if self.glyphs[..i].iter().any(|o| o.name == g.name) {
return Err(format!("glyph {:?} appears more than once", g.name));
}
}
let total_points: usize = self.glyphs.iter().map(|g| g.points.len()).sum();
if total_points != ROUND1_POINT_CENSUS {
return Err(format!(
"oracle carries {total_points} sample points in total, not the \
{ROUND1_POINT_CENSUS} Round 1 requires"
));
}
for (name, requirement, subpaths, point_count) in ROUND1_ROSTER {
let g = self
.glyphs
.iter()
.find(|g| g.name == name)
.ok_or_else(|| format!("oracle is missing required Round 1 glyph {name:?}"))?;
let at = |m: String| format!("{}: {m}", g.name);
if g.requirement != requirement {
return Err(at(format!(
"carries requirement {:?}, but Round 1 tests it for {requirement:?}",
g.requirement
)));
}
if g.subpath_count != subpaths || g.expected_subpath_count != Some(subpaths) {
return Err(at(format!(
"subpath_count {} / expected {:?} disagrees with the {subpaths} subpaths \
Round 1 names for this glyph",
g.subpath_count, g.expected_subpath_count
)));
}
if g.points.len() != point_count {
return Err(at(format!(
"carries {} sample points, not the {point_count} Round 1 names",
g.points.len()
)));
}
if !g.satisfied || !g.ink_satisfied {
return Err(at(format!(
"oracle records satisfied = {} / ink_satisfied = {}",
g.satisfied, g.ink_satisfied
)));
}
if g.ink_spacing_relaxed || g.background_spacing_relaxed {
return Err(at(
"point search fell back to relaxed spacing — the recorded points are closer \
together than the round's design, so they no longer probe independent regions"
.to_string(),
));
}
if g.transform.target_width != TARGET_WIDTH
|| g.transform.target_height != TARGET_HEIGHT
{
return Err(at(format!(
"target is {}x{}, not pin 4's {TARGET_WIDTH}x{TARGET_HEIGHT}",
g.transform.target_width, g.transform.target_height
)));
}
for (i, p) in g.points.iter().enumerate() {
if !(p.clearance_device_px >= CLEARANCE_FLOOR_DEVICE_PX) {
return Err(at(format!(
"point {i} at device ({}, {}) has clearance {} device px, below the \
{CLEARANCE_FLOOR_DEVICE_PX} px floor",
p.device.0, p.device.1, p.clearance_device_px
)));
}
}
let ink: Vec<&SamplePoint> = g
.points
.iter()
.filter(|p| p.class == SampleClass::Ink)
.collect();
let bg: Vec<&SamplePoint> = g
.points
.iter()
.filter(|p| p.class == SampleClass::Background)
.collect();
match g.requirement {
Requirement::BoundedHole => {
if !g.background_required
|| !g.background_satisfied
|| g.subpath_coverage_required
{
return Err(at(format!(
"BoundedHole must require and satisfy background points and must not \
claim subpath coverage; has background_required = {}, \
background_satisfied = {}, subpath_coverage_required = {}",
g.background_required,
g.background_satisfied,
g.subpath_coverage_required
)));
}
if ink.len() < 3 || bg.len() < 3 {
return Err(at(format!(
"BoundedHole needs >= 3 ink and >= 3 background points, has {} and {}",
ink.len(),
bg.len()
)));
}
for p in &bg {
let e = p.hole_evidence.as_ref().ok_or_else(|| {
at("background point carries no hole evidence".to_string())
})?;
// Unfilled under BOTH rules, not just even-odd: that
// agreement is the measured fact criterion 1 rests on
// (Bravura's contours are correctly oppositely wound),
// so a point the two rules disagree about is not a
// bounded hole this round can test with.
if !e.inside_outer_contour || e.even_odd_filled || e.nonzero_filled {
return Err(at(format!(
"background point at device ({}, {}) is not an unambiguously \
bounded hole: inside_outer_contour = {}, even_odd_filled = {}, \
nonzero_filled = {}",
p.device.0,
p.device.1,
e.inside_outer_contour,
e.even_odd_filled,
e.nonzero_filled
)));
}
if e.outer_contour_ring_index != g.outer_contour_ring_index {
return Err(at(format!(
"background point names outer ring {} but the glyph names {}",
e.outer_contour_ring_index, g.outer_contour_ring_index
)));
}
}
}
Requirement::DisjointComponents => {
if g.background_required
|| !g.subpath_coverage_required
|| !g.subpath_coverage_satisfied
{
return Err(at(
"DisjointComponents must require subpath coverage and no background"
.to_string(),
));
}
if !bg.is_empty() {
return Err(at(format!(
"DisjointComponents must carry no background points, has {}",
bg.len()
)));
}
let mut covered: Vec<usize> = ink
.iter()
.filter_map(|p| p.subpath_index)
.collect::<Vec<_>>();
covered.sort_unstable();
covered.dedup();
if covered.len() != g.subpath_count
|| covered.first() != Some(&0)
|| covered.last() != Some(&(g.subpath_count - 1))
{
return Err(at(format!(
"ink points cover subpaths {covered:?}, not every index in \
0..{} — the check that catches a largest-contour-only tessellator \
would not fire",
g.subpath_count
)));
}
}
}
}
Ok(())
}
}
/// Fetches one glyph's typed outline from the real `BravuraGlyphCatalog` —
/// the same catalog `round1-oracle` derived its geometry from.
pub fn outline_for(name: &str) -> Vec<PathCommand> {
let catalog = BravuraGlyphCatalog;
catalog
.render_data(name)
.unwrap_or_else(|| panic!("{name}: BravuraGlyphCatalog has no render data"))
.outline
}
// ---------------------------------------------------------------------
// Pixel classification + report model
// ---------------------------------------------------------------------
/// Ink-vs-background classification of one sampled RGBA pixel.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PixelClass {
Ink,
Background,
}
/// Classifies `rgba` by luminance against `threshold` (0..=255): a pixel
/// whose luminance is strictly below `threshold` is `Ink` (opaque black
/// fill), otherwise `Background` (opaque white). Since every oracle sample
/// point sits >= 8 device px from any outline edge, every real sample should
/// land solidly at (0,0,0,255) or (255,255,255,255) — the exact threshold
/// value barely matters for a passing render, but a stated one is required
/// so a FAIL's root cause is never "which threshold did you mean".
pub fn classify_pixel(rgba: [u8; 4], threshold: u8) -> PixelClass {
let [r, g, b, _a] = rgba;
// Standard luma weights (Rec. 601), integer-approximated.
let luma = (299 * r as u32 + 587 * g as u32 + 114 * b as u32) / 1000;
if luma < threshold as u32 {
PixelClass::Ink
} else {
PixelClass::Background
}
}
pub fn expected_class(oracle_class: SampleClass) -> PixelClass {
match oracle_class {
SampleClass::Ink => PixelClass::Ink,
SampleClass::Background => PixelClass::Background,
}
}
#[derive(Clone, Debug)]
pub struct PointResult {
pub staff: (f64, f64),
pub device: (f64, f64),
pub oracle_class: SampleClass,
pub subpath_index: Option<usize>,
pub sampled_rgba: [u8; 4],
pub actual_class: PixelClass,
pub expected_class: PixelClass,
pub pass: bool,
}
#[derive(Clone, Debug)]
pub struct GlyphResult {
pub name: String,
pub points: Vec<PointResult>,
}
impl GlyphResult {
pub fn all_pass(&self) -> bool {
self.points.iter().all(|p| p.pass)
}
}
#[derive(Clone, Debug)]
pub struct AdapterInfo {
pub name: String,
pub backend: String,
pub device_type: String,
pub vendor_id: u32,
pub device_id: u32,
}
#[derive(Clone, Debug)]
pub struct RunReport {
pub candidate: String,
pub adapter: AdapterInfo,
/// The **nominal** sample count. Both candidates report 8, as pin 4
/// requires, but the number alone overstates the agreement — see
/// `aa_mechanism`, which is printed beside it for exactly that reason.
pub msaa_samples: u32,
/// How that sample count is actually achieved. C1 uses a hardware
/// multisample render-target attachment; C2 uses vello's compute-shader
/// antialiasing into a single-sample storage texture. Recording only the
/// integer would let a later round read "8 == 8" as "same work", which it
/// is not — and at Round 4 that difference is in the deciding numbers.
pub aa_mechanism: String,
pub target_format: String,
pub luminance_threshold: u8,
pub fill_rule: String,
pub glyphs: Vec<GlyphResult>,
pub notes: Vec<String>,
}
impl RunReport {
pub fn all_pass(&self) -> bool {
self.glyphs.iter().all(|g| g.all_pass())
}
/// Renders the full per-glyph x per-point table as plain text.
pub fn table(&self) -> String {
let mut out = String::new();
out.push_str(&format!(
"=== {} on {} ({}) [backend={}, vendor=0x{:04x}, device=0x{:04x}] ===\n",
self.candidate,
self.adapter.name,
self.adapter.device_type,
self.adapter.backend,
self.adapter.vendor_id,
self.adapter.device_id,
));
out.push_str(&format!(
"msaa={} ({}) format={} luminance_threshold<{} fill_rule={}\n",
self.msaa_samples,
self.aa_mechanism,
self.target_format,
self.luminance_threshold,
self.fill_rule
));
for g in &self.glyphs {
out.push_str(&format!(
"--- {} ({}/{} points PASS) ---\n",
g.name,
g.points.iter().filter(|p| p.pass).count(),
g.points.len()
));
out.push_str(
" idx class subpath device(x,y) rgba verdict\n",
);
for (i, p) in g.points.iter().enumerate() {
out.push_str(&format!(
" {:>3} {:<10} {:<7} ({:>8.2},{:>8.2}) ({:>3},{:>3},{:>3},{:>3}) {}\n",
i,
format!("{:?}", p.oracle_class),
p.subpath_index
.map(|s| s.to_string())
.unwrap_or_else(|| "-".to_string()),
p.device.0,
p.device.1,
p.sampled_rgba[0],
p.sampled_rgba[1],
p.sampled_rgba[2],
p.sampled_rgba[3],
if p.pass { "PASS" } else { "FAIL" },
));
}
}
if !self.notes.is_empty() {
out.push_str("notes:\n");
for n in &self.notes {
out.push_str(&format!(" - {n}\n"));
}
}
out
}
}
/// Rounds a device-space coordinate to the nearest pixel index, **erroring
/// rather than clamping** when it falls outside `[0, dim)`.
///
/// Clamping was the earlier behaviour and it is unsafe here: an out-of-range
/// coordinate silently becomes an edge pixel, which for a glyph centred in a
/// 1920x1080 target is always background — so a mis-transformed ink point
/// would read as a candidate FAIL, and a mis-transformed background point as a
/// PASS. Either way the harness would be reporting on a pixel the oracle never
/// named.
pub fn device_index(v: f64, dim: u32) -> Result<u32, String> {
let r = v.round();
if r < 0.0 || r >= dim as f64 {
return Err(format!(
"device coordinate {v} rounds to {r}, outside [0, {dim}) — the render target does not \
cover the oracle's sample point"
));
}
Ok(r as u32)
}
/// Builds a `GlyphResult` from an already-rendered RGBA readback buffer
/// (tightly packed, `width * height * 4` bytes, row-major top-to-bottom —
/// standard wgpu texture-copy layout after unpadding row strides) by
/// sampling each oracle point's `device` pixel.
///
/// **Every failure mode here is an error, never a substituted sample.** A
/// short buffer previously yielded `(0,0,0,0)`, whose luma is 0 — it
/// classifies as *ink*, so a truncated readback would have made every ink
/// point pass. Buffer length, coordinate range, and sample opacity are all
/// checked, because each of them can turn a broken run into a green one.
pub fn evaluate_glyph(
oracle: &GlyphOracle,
width: u32,
height: u32,
rgba: &[u8],
threshold: u8,
) -> Result<GlyphResult, String> {
let expect_len = (width as usize) * (height as usize) * 4;
if rgba.len() != expect_len {
return Err(format!(
"{}: readback buffer is {} bytes, expected exactly {expect_len} ({width}x{height} \
RGBA) — a short or padded buffer cannot be sampled safely",
oracle.name,
rgba.len()
));
}
let mut points = Vec::with_capacity(oracle.points.len());
for p in &oracle.points {
let px = device_index(p.device.0, width).map_err(|e| format!("{}: x: {e}", oracle.name))?;
let py =
device_index(p.device.1, height).map_err(|e| format!("{}: y: {e}", oracle.name))?;
let idx = ((py as usize) * (width as usize) + (px as usize)) * 4;
let sampled = [rgba[idx], rgba[idx + 1], rgba[idx + 2], rgba[idx + 3]];
if sampled[3] != 255 {
return Err(format!(
"{}: sample at device ({}, {}) has alpha {} — the target must be fully opaque; a \
transparent sample means the clear or the blend is wrong, not that the candidate \
drew the wrong colour",
oracle.name, p.device.0, p.device.1, sampled[3]
));
}
let actual = classify_pixel(sampled, threshold);
let expected = expected_class(p.class);
points.push(PointResult {
staff: p.staff,
device: p.device,
oracle_class: p.class,
subpath_index: p.subpath_index,
sampled_rgba: sampled,
actual_class: actual,
expected_class: expected,
pass: actual == expected,
});
}
Ok(GlyphResult {
name: oracle.name.clone(),
points,
})
}