T4 round 1: both candidates render the compound paths, and the oracle now refuses to be edited into agreement

The shared harness plus the two candidate binaries for Round 1 of the
toolkit spike. C1 tessellates each Bravura outline with lyon and draws it
through egui's own paint pipeline; C2 builds one kurbo BezPath and one
Scene::fill. Both classify their readback against the frozen oracle from
0a35697 and report per-point.

Three things this had to get right, none of which is the rendering:

A candidate must not be able to pass by drawing nothing. C1 initially
failed all fifteen ink points while passing all twelve background points,
which is the signature of a blank target, not of a fill bug: the mesh
named TextureId::default() and egui-wgpu silently skips primitives whose
texture id is unregistered. It now registers a 1x1 opaque-white texture.

The harness must not substitute for a broken readback. A short buffer
used to yield (0,0,0,0), whose luma is 0, so every ink point would pass.
Buffer length, coordinate range, and sample opacity are now hard errors
rather than clamps or defaults.

The oracle must not be editable into agreement. deny_unknown_fields
catches structural drift, but semantic drift is the dangerous kind, and
a validator that checks the oracle against its own other fields accepts
any self-consistent file. OracleFile::validate now checks against
literals restated in the harness: the exact five-glyph roster with its
requirement mapping, subpath counts and point counts, the 27-point
census, 1920x1080, the 8 px clearance floor and every sample's clearance,
and the requirement-specific status flags. Thirteen mutations were run
against it -- deleting a glyph, renaming one, duplicating one, dropping a
point, retargeting every glyph together, lowering the floor, relaxing
spacing -- and all thirteen are rejected.

AA is nominally 8x on both, as pin 4 requires, but the mechanisms differ:
C1 uses a hardware multisample attachment (which is why it must request
TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, since 8x on Rgba8Unorm is off
the WebGPU baseline) while C2 uses vello's compute AA into a
sample_count:1 storage texture. The run report prints the mechanism beside
the number so no later round reads "8 == 8" as parity. Round 1 is
indifferent to the difference; Round 4's timings will not be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
This commit is contained in:
Levi Neuwirth 2026-07-29 10:59:49 -04:00
parent 25c473314b
commit c20bc93247
8 changed files with 1588 additions and 0 deletions

View File

@ -638,6 +638,34 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "c1-egui-lyon"
version = "0.1.0"
dependencies = [
"anyhow",
"bytemuck",
"egui",
"egui-wgpu",
"epiphany-layout-ir",
"lyon",
"lyon_path",
"lyon_tessellation",
"pollster",
"round1-harness",
]
[[package]]
name = "c2-vello"
version = "0.1.0"
dependencies = [
"anyhow",
"bytemuck",
"epiphany-layout-ir",
"pollster",
"round1-harness",
"vello",
]
[[package]]
name = "calloop"
version = "0.13.0"
@ -3605,6 +3633,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "round1-harness"
version = "0.1.0"
dependencies = [
"epiphany-glyphs",
"epiphany-layout-ir",
"serde",
"serde_json",
]
[[package]]
name = "round1-oracle"
version = "0.1.0"

View File

@ -5,6 +5,9 @@ members = [
"probe-vello",
"probe-iced",
"round1-oracle",
"round1-candidates/harness",
"round1-candidates/c1-egui-lyon",
"round1-candidates/c2-vello",
]
# a11y-verifier is a standalone Python script (a11y-verifier/verify.py), not

View File

@ -0,0 +1,27 @@
[package]
name = "c1-egui-lyon"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
# Round 1 candidate C1: egui's own paint pipeline (egui_wgpu::Renderer),
# fed an egui::epaint::Mesh tessellated by lyon from the typed PathCommand
# outline. Matches/exceeds probe-egui's Round-0 version pins (egui/eframe
# 0.35); egui_wgpu is added here specifically because Round 1 renders
# through it (Round 0 did not need to render anything).
# wgpu itself is consumed via egui_wgpu's own re-export (`egui_wgpu::wgpu`)
# rather than as a separate top-level dependency, to guarantee the Device
# passed into `egui_wgpu::Renderer::new` is the exact type it expects.
[dependencies]
round1-harness = { path = "../harness" }
epiphany-layout-ir = { path = "../../../../crates/epiphany-layout-ir" }
egui = "0.35"
egui-wgpu = "0.35"
lyon = "1.0"
lyon_path = "1.0"
lyon_tessellation = "1.0"
pollster = "0.4"
bytemuck = "1"
anyhow = "1"

View File

@ -0,0 +1,487 @@
//! Round 1 candidate **C1 — egui + lyon**.
//!
//! Tessellates each Round-1 glyph's typed `PathCommand` outline with `lyon`
//! into an `egui::epaint::Mesh`, then draws it through **egui's own paint
//! pipeline** (`egui_wgpu::Renderer`) to an offscreen texture. The RGBA
//! readback goes to the candidate-neutral harness for classification against
//! the **frozen** `round1-oracle/oracle.json`.
//!
//! **Why it must go through `egui_wgpu::Renderer` and not a hand-rolled wgpu
//! pipeline.** Ruling A names the candidate as "lyon-tessellated meshes inside
//! egui". Rendering the lyon mesh through a bare pipeline would test lyon
//! alone and answer a different question — the same failure shape as Round 0's
//! iced side channel, which read back cleanly while proving nothing about its
//! subject. So this binary builds a real `epaint::ClippedPrimitive` and calls
//! `Renderer::update_buffers` + `Renderer::render`.
//!
//! Windowless: egui's renderer needs a `Device`/`Queue` and a target view, not
//! a surface, so no `eframe`/`winit` appears here.
//!
//! **The whole outline is tessellated as ONE compound path.** `lyon`'s
//! `FillTessellator` applies the fill rule across every contour in the path,
//! so bounded counters survive as holes and disjoint components are all
//! painted. Tessellating each subpath separately would fill counters solid and
//! silently pass the test that exists to catch exactly that.
use anyhow::{anyhow, Result};
use egui::epaint::{ClippedPrimitive, Mesh, Primitive, Vertex};
use egui::{Color32, Pos2, Rect, TextureId};
use egui_wgpu::wgpu;
use egui_wgpu::{Renderer, RendererOptions, ScreenDescriptor};
use epiphany_layout_ir::PathCommand;
use lyon_path::math::point as lyon_point;
use lyon_path::Path as LyonPath;
use lyon_tessellation::{
BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, VertexBuffers,
};
use round1_harness as harness;
const INK: Color32 = Color32::BLACK;
const GROUND: wgpu::Color = wgpu::Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
/// Rec. 601 luma threshold used by the harness — stated, not implied.
const LUMA_THRESHOLD: u8 = 128;
/// **Nominally 8x — the highest sample count BOTH candidates name**, which is
/// what pin 4 asks for ("identical MSAA sample count... 4x, or the highest all
/// survivors support"). An earlier revision ran C1 at 4x against C2's 8x,
/// which is not a common configuration at all: vello's `AaConfig` offers only
/// Area/Msaa8/Msaa16, so 8x is the highest both can name, and both adapters
/// advertise it.
///
/// **This 8x is hardware multisampling and C2's is not.** Here it is a real
/// `sample_count: 8` colour attachment resolved by the GPU — which is exactly
/// why this binary must request `TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES`,
/// since 8x on `Rgba8Unorm` is outside the WebGPU baseline. vello's `Msaa8` is
/// its own compute-shader antialiasing into a single-sample storage texture,
/// with no multisample attachment at all. The declared sample count is
/// identical, as pin 4 requires; the work behind it is not, and Round 4's
/// timings must say so rather than let "8 == 8" stand in for parity.
const MSAA: u32 = 8;
/// Printed beside `msaa_samples` in the run report, so the record itself
/// states the mechanism rather than leaving the bare integer to imply parity.
const AA_MECHANISM: &str = "hardware MSAA render-target attachment, GPU-resolved";
/// `Rgba8Unorm`, not sRGB, so both candidates share one format — vello's
/// `render_to_texture` requires it. A literal deviation from pin 4, immaterial
/// to fill correctness because only pure black and white are drawn.
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
/// Builds one `lyon` path carrying **every** contour of the outline, already
/// in device coordinates.
///
/// Staff-space is y-up, device space y-down, so the flip happens here rather
/// than in a transform — keeping this identical to the oracle's own
/// `device = (staff.x * scale + tx, ty - staff.y * scale)` rule.
fn build_path(outline: &[PathCommand], t: &harness::Transform) -> LyonPath {
let map = |p: epiphany_layout_ir::Point| {
lyon_point(
(p.x.0 as f64 * t.scale + t.tx) as f32,
(t.ty - p.y.0 as f64 * t.scale) as f32,
)
};
let mut builder = LyonPath::builder();
let mut open = false;
for cmd in outline {
match cmd {
PathCommand::MoveTo(p) => {
if open {
builder.end(true);
}
builder.begin(map(*p));
open = true;
}
PathCommand::LineTo(p) => {
builder.line_to(map(*p));
}
PathCommand::CurveTo {
control1,
control2,
to,
} => {
builder.cubic_bezier_to(map(*control1), map(*control2), map(*to));
}
PathCommand::Close => {
if open {
builder.end(true);
open = false;
}
}
}
}
if open {
builder.end(true);
}
builder.build()
}
/// Tessellates the compound path into an `epaint::Mesh` bound to `tex`.
///
/// **`tex` must be a texture actually registered with the renderer.** egui's
/// draw loop does `if let Some(..) = self.textures.get(&mesh.texture_id)`
/// (`egui-wgpu-0.35.0/src/renderer.rs:542`) and **silently skips** the
/// primitive when the id is unknown — no error, no warning, just an unpainted
/// mesh. Using `TextureId::default()` (the font atlas) without uploading one
/// therefore renders a blank target that reads as "every ink sample is
/// background", which is a probe defect wearing a candidate failure's costume.
/// This binary registers its own 1x1 opaque-white texture instead, so the
/// vertex colour passes through unmodulated at any UV.
fn tessellate(path: &LyonPath, tex: TextureId) -> Result<Mesh> {
let mut buffers: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
let mut tess = FillTessellator::new();
tess.tessellate_path(
path,
// NonZero: Bravura's contours are correctly oppositely wound, so
// even-odd and nonzero agree on every bundled counter (the oracle
// records that measurement). The rule is not what is under test —
// preserving contours and counters is.
&FillOptions::default().with_fill_rule(FillRule::NonZero),
&mut BuffersBuilder::new(&mut buffers, |v: FillVertex| {
let p = v.position();
[p.x, p.y]
}),
)
.map_err(|e| anyhow!("lyon tessellation failed: {e:?}"))?;
let mut mesh = Mesh::with_texture(tex);
mesh.vertices = buffers
.vertices
.iter()
.map(|[x, y]| Vertex {
pos: Pos2::new(*x, *y),
uv: Pos2::ZERO,
color: INK,
})
.collect();
mesh.indices = buffers.indices;
Ok(mesh)
}
fn readback(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture: &wgpu::Texture,
width: u32,
height: u32,
) -> Result<Vec<u8>> {
let unpadded = width * 4;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded = unpadded.div_ceil(align) * align;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("c1-readback"),
size: (padded as u64) * (height as u64),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("c1-copy"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
let slice = buffer.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
device.poll(wgpu::PollType::wait_indefinitely())?;
rx.recv()
.map_err(|e| anyhow!("readback channel closed: {e}"))?
.map_err(|e| anyhow!("buffer map failed: {e}"))?;
let mapped = slice.get_mapped_range();
let mut out = Vec::with_capacity((unpadded as usize) * (height as usize));
for row in 0..height as usize {
let start = row * padded as usize;
out.extend_from_slice(&mapped[start..start + unpadded as usize]);
}
drop(mapped);
buffer.unmap();
Ok(out)
}
fn adapter_info(a: &wgpu::Adapter) -> harness::AdapterInfo {
let info = a.get_info();
harness::AdapterInfo {
name: info.name.clone(),
backend: format!("{:?}", info.backend),
device_type: format!("{:?}", info.device_type),
vendor_id: info.vendor,
device_id: info.device,
}
}
fn run_on(adapter: &wgpu::Adapter, oracle: &harness::OracleFile) -> Result<harness::RunReport> {
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("c1-egui-lyon"),
// 8x MSAA on Rgba8Unorm is not a WebGPU baseline guarantee — the spec
// guarantees only [1, 4] for this format, and wgpu rejects the
// pipeline without this feature. Both target adapters report
// [1, 2, 4, 8] with it enabled, so requesting it is what lets C1 meet
// pin 4's common-sample-count requirement rather than silently
// dropping to 4x.
required_features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES,
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
}))?;
let mut renderer = Renderer::new(
&device,
FORMAT,
RendererOptions {
msaa_samples: MSAA,
depth_stencil_format: None,
..Default::default()
},
);
// A 1x1 opaque-white texture, registered so the mesh's texture id resolves
// (see `tessellate`'s doc for why an unregistered id silently paints
// nothing). White modulates the vertex colour by 1.0, so the ink colour is
// unchanged.
let white = device.create_texture(&wgpu::TextureDescriptor {
label: Some("c1-white-1x1"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &white,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&[255u8, 255, 255, 255],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
);
let white_view = white.create_view(&wgpu::TextureViewDescriptor::default());
let white_id =
renderer.register_native_texture(&device, &white_view, wgpu::FilterMode::Nearest);
let mut glyphs = Vec::new();
for g in &oracle.glyphs {
let width = g.transform.target_width as u32;
let height = g.transform.target_height as u32;
let size = wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
};
// The MSAA colour attachment, resolved into `resolve` below.
let msaa_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("c1-msaa"),
size,
mip_level_count: 1,
sample_count: MSAA,
dimension: wgpu::TextureDimension::D2,
format: FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let resolve = device.create_texture(&wgpu::TextureDescriptor {
label: Some("c1-resolve"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
let resolve_view = resolve.create_view(&wgpu::TextureViewDescriptor::default());
let outline = harness::outline_for(&g.name);
let path = build_path(&outline, &g.transform);
let mesh = tessellate(&path, white_id)?;
let jobs = vec![ClippedPrimitive {
clip_rect: Rect::from_min_size(Pos2::ZERO, egui::vec2(width as f32, height as f32)),
primitive: Primitive::Mesh(mesh),
}];
let screen = ScreenDescriptor {
size_in_pixels: [width, height],
pixels_per_point: 1.0,
};
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("c1-encode"),
});
let extra = renderer.update_buffers(&device, &queue, &mut encoder, &jobs, &screen);
{
let pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("c1-pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &msaa_view,
resolve_target: Some(&resolve_view),
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(GROUND),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
// `Renderer::render` requires a 'static pass.
let mut pass = pass.forget_lifetime();
renderer.render(&mut pass, &jobs, &screen);
}
queue.submit(extra.into_iter().chain([encoder.finish()]));
let rgba = readback(&device, &queue, &resolve, width, height)?;
glyphs.push(
harness::evaluate_glyph(g, width, height, &rgba, LUMA_THRESHOLD)
.map_err(|e| anyhow!("{e}"))?,
);
}
Ok(harness::RunReport {
candidate: "C1 egui 0.35 + lyon 1.0 (egui_wgpu::Renderer)".to_string(),
adapter: adapter_info(adapter),
msaa_samples: MSAA,
aa_mechanism: AA_MECHANISM.to_string(),
target_format: format!("{FORMAT:?}"),
luminance_threshold: LUMA_THRESHOLD,
fill_rule: "NonZero".to_string(),
glyphs,
notes: vec![
"Rendered through egui's own paint pipeline (egui_wgpu::Renderer::update_buffers + \
::render) with an epaint::Mesh, NOT a hand-rolled wgpu pipeline the stack under \
test is the real one."
.to_string(),
"Whole outline tessellated as ONE compound lyon path in one tessellate_path call."
.to_string(),
"Format deviation: Rgba8Unorm, not sRGB, to match C2 (vello requires it).".to_string(),
"AA is nominally 8x on both candidates — the highest sample count both name, so pin \
4's identical-configuration requirement is met as stated. The mechanisms are NOT the \
same: this is a hardware multisample render-target attachment (hence the \
TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES request), while C2 uses vello's \
compute-shader AA into a sample_count:1 storage texture. Matching integers do not \
imply matching work or matching cost; this is immaterial to Round 1's capability \
verdict and material to Round 4's timings."
.to_string(),
],
})
}
fn main() -> Result<()> {
let oracle = harness::load_oracle();
// Semantic validation before anything renders: an oracle that deserializes
// but no longer means what Round 1 requires would be tested faithfully and
// pass (harness `OracleFile::validate`).
oracle
.validate()
.map_err(|e| anyhow!("oracle failed validation: {e}"))?;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::VULKAN,
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapters = pollster::block_on(instance.enumerate_adapters(wgpu::Backends::VULKAN));
// Pin 4 requires BOTH adapter classes, and the integrated figure is the one
// that decides. Reporting overall PASS after testing whichever adapter
// happened to enumerate would silently narrow the claim, so a missing class
// is NOT RUN (an environment absence) and never a pass.
let has_discrete = adapters
.iter()
.any(|a| a.get_info().device_type == wgpu::DeviceType::DiscreteGpu);
let has_integrated = adapters
.iter()
.any(|a| a.get_info().device_type == wgpu::DeviceType::IntegratedGpu);
if !has_discrete || !has_integrated {
let found: Vec<String> = adapters
.iter()
.map(|a| {
let i = a.get_info();
format!("{} ({:?})", i.name, i.device_type)
})
.collect();
return Err(anyhow!(
"NOT RUN: pin 4 requires one discrete and one integrated Vulkan adapter; found \
{found:?}. This is an environment absence, not a candidate failure re-run where \
both are present."
));
}
let mut all_pass = true;
for adapter in &adapters {
let report = run_on(adapter, &oracle)?;
print!("{}", report.table());
let total: usize = report.glyphs.iter().map(|g| g.points.len()).sum();
let passed: usize = report
.glyphs
.iter()
.map(|g| g.points.iter().filter(|p| p.pass).count())
.sum();
println!(
"RESULT {} :: {passed}/{total} points PASS\n",
report.adapter.name
);
all_pass &= report.all_pass();
}
if all_pass {
println!("C1 egui+lyon: PASS on all {} adapter(s)", adapters.len());
Ok(())
} else {
Err(anyhow!(
"C1 egui+lyon: FAIL — see the per-point table above"
))
}
}

View File

@ -0,0 +1,23 @@
[package]
name = "c2-vello"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
# Round 1 candidate C2: vello + kurbo, matching probe-vello's Round-0
# version pin (vello 0.9). Windowless: vello's Renderer/RenderContext do not
# require winit, so this binary drives its own headless wgpu Device/Queue.
#
# kurbo and wgpu are consumed via vello's own re-exports (`vello::kurbo`,
# `vello::wgpu`) rather than as separate top-level dependencies, so there is
# no risk of resolving a kurbo/wgpu version that disagrees with the one
# vello 0.9 was actually built against (vello re-exports `peniko::kurbo`,
# whose version peniko itself pins).
[dependencies]
round1-harness = { path = "../harness" }
epiphany-layout-ir = { path = "../../../../crates/epiphany-layout-ir" }
vello = "0.9"
pollster = "0.4"
bytemuck = "1"
anyhow = "1"

View File

@ -0,0 +1,351 @@
//! Round 1 candidate **C2 — vello + kurbo**.
//!
//! Renders each Round-1 glyph's typed `PathCommand` outline through vello's
//! own scene/renderer pipeline to an offscreen texture, then hands the RGBA
//! readback to the candidate-neutral harness for classification against the
//! **frozen** `round1-oracle/oracle.json`. This binary never reads or writes
//! that oracle directly — the harness owns it, read-only.
//!
//! Windowless by construction: vello's `Renderer::render_to_texture` needs no
//! surface, so no `winit` appears here even though C2's Round-0 accessibility
//! route did use it.
//!
//! **The whole outline goes into one `BezPath` and one `Scene::fill` call.**
//! That is the point of criterion 1: a compound path with several contours,
//! filled as one shape, so bounded counters must survive as holes and disjoint
//! components must all be painted. Filling each subpath separately would paint
//! counters solid and quietly pass the test it exists to fail.
use anyhow::{anyhow, Result};
use epiphany_layout_ir::PathCommand;
use round1_harness as harness;
use vello::kurbo::{Affine, BezPath, Point as KPoint};
use vello::peniko::{color::palette, Color, Fill};
use vello::wgpu;
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
/// Opaque black ink on an opaque white ground — the harness classifies by
/// luminance against this contrast, and the oracle's >= 8 device-px clearance
/// guarantees every sample lands fully saturated.
const INK: Color = palette::css::BLACK;
const GROUND: Color = palette::css::WHITE;
/// Rec. 601 luma threshold used by the harness. Stated, not implied, so a
/// FAIL's root cause can never be "which threshold did you mean".
const LUMA_THRESHOLD: u8 = 128;
/// **Nominally 8x — the highest sample count BOTH candidates name**, per pin
/// 4's "identical MSAA sample count... the highest all survivors support".
/// vello's `AaConfig` offers only Area / Msaa8 / Msaa16, so 8x is the ceiling
/// on this side, and C1 now matches that integer rather than the 4x it ran
/// earlier.
///
/// **The integers match; the mechanisms do not, and this is not a footnote.**
/// C1's 8x is hardware multisampling — a `sample_count: 8` colour attachment
/// resolved by the GPU, which is precisely why C1 has to request
/// `TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES` (8x on `Rgba8Unorm` is off the
/// WebGPU baseline). vello's `Msaa8` is its own compute-shader antialiasing,
/// writing into the `sample_count: 1` storage texture created below; there is
/// no multisample attachment anywhere on this side. So pin 4's identical-
/// configuration requirement is met **as stated** — same declared sample count
/// — but equal integers do not imply equal work, equal memory traffic, or
/// equal cost. Round 1 is a capability round and is indifferent to that; Round
/// 4 is not, because AA lands directly in the deciding latency numbers. The
/// report carries the mechanism next to the number so no later round can read
/// "8 == 8" as "same thing measured".
const AA: AaConfig = AaConfig::Msaa8;
/// Printed beside `msaa_samples` in the run report, so the record itself
/// states the mechanism rather than leaving the bare integer to imply parity.
const AA_MECHANISM: &str = "vello compute AA into a sample_count:1 storage texture";
/// vello's `render_to_texture` requires `Rgba8Unorm` + `STORAGE_BINDING`, so
/// both candidates are pinned to this same non-sRGB format for a fair
/// comparison. Immaterial to fill correctness (0 and 255 map to themselves
/// under any transfer function) but a literal deviation from pin 4's "sRGB
/// target format", and reported as one.
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
/// Builds one `kurbo::BezPath` carrying **every** contour of the outline.
///
/// Staff-space is y-up and device space is y-down, so the y axis is flipped
/// here rather than in a transform, keeping the path in final device
/// coordinates and matching the oracle's own
/// `device = (staff.x * scale + tx, ty - staff.y * scale)` rule exactly.
fn build_path(outline: &[PathCommand], t: &harness::Transform) -> BezPath {
let map = |p: epiphany_layout_ir::Point| -> KPoint {
KPoint::new(p.x.0 as f64 * t.scale + t.tx, t.ty - p.y.0 as f64 * t.scale)
};
let mut path = BezPath::new();
let mut open = false;
for cmd in outline {
match cmd {
PathCommand::MoveTo(p) => {
if open {
path.close_path();
}
path.move_to(map(*p));
open = true;
}
PathCommand::LineTo(p) => path.line_to(map(*p)),
PathCommand::CurveTo {
control1,
control2,
to,
} => path.curve_to(map(*control1), map(*control2), map(*to)),
PathCommand::Close => {
path.close_path();
open = false;
}
}
}
if open {
path.close_path();
}
path
}
/// Copies a rendered texture back to host memory as tightly packed RGBA,
/// undoing wgpu's 256-byte row-stride padding.
fn readback(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture: &wgpu::Texture,
width: u32,
height: u32,
) -> Result<Vec<u8>> {
let unpadded = width * 4;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded = unpadded.div_ceil(align) * align;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("c2-readback"),
size: (padded as u64) * (height as u64),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("c2-copy"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
let slice = buffer.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
device.poll(wgpu::PollType::wait_indefinitely())?;
rx.recv()
.map_err(|e| anyhow!("readback channel closed: {e}"))?
.map_err(|e| anyhow!("buffer map failed: {e}"))?;
let mapped = slice.get_mapped_range();
let mut out = Vec::with_capacity((unpadded as usize) * (height as usize));
for row in 0..height as usize {
let start = row * padded as usize;
out.extend_from_slice(&mapped[start..start + unpadded as usize]);
}
drop(mapped);
buffer.unmap();
Ok(out)
}
fn adapter_info(a: &wgpu::Adapter) -> harness::AdapterInfo {
let info = a.get_info();
harness::AdapterInfo {
name: info.name.clone(),
backend: format!("{:?}", info.backend),
device_type: format!("{:?}", info.device_type),
vendor_id: info.vendor,
device_id: info.device,
}
}
fn run_on(adapter: &wgpu::Adapter, oracle: &harness::OracleFile) -> Result<harness::RunReport> {
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("c2-vello"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
}))?;
let mut renderer = Renderer::new(
&device,
RendererOptions {
use_cpu: false,
antialiasing_support: AaSupport::all(),
num_init_threads: None,
pipeline_cache: None,
},
)
.map_err(|e| anyhow!("vello Renderer::new failed: {e}"))?;
let mut glyphs = Vec::new();
for g in &oracle.glyphs {
let width = g.transform.target_width as u32;
let height = g.transform.target_height as u32;
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("c2-target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: FORMAT,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let outline = harness::outline_for(&g.name);
let path = build_path(&outline, &g.transform);
let mut scene = Scene::new();
// NonZero: Bravura's contours are correctly oppositely wound, so
// even-odd and nonzero agree on every bundled counter — the oracle
// records that measurement. The fill *rule* is not what is under test;
// preserving contours and counters is.
scene.fill(Fill::NonZero, Affine::IDENTITY, INK, None, &path);
renderer
.render_to_texture(
&device,
&queue,
&scene,
&view,
&RenderParams {
base_color: GROUND,
width,
height,
antialiasing_method: AA,
},
)
.map_err(|e| anyhow!("{}: vello render_to_texture failed: {e}", g.name))?;
let rgba = readback(&device, &queue, &texture, width, height)?;
glyphs.push(
harness::evaluate_glyph(g, width, height, &rgba, LUMA_THRESHOLD)
.map_err(|e| anyhow!("{e}"))?,
);
}
Ok(harness::RunReport {
candidate: "C2 vello 0.9 + kurbo".to_string(),
adapter: adapter_info(adapter),
msaa_samples: 8,
aa_mechanism: AA_MECHANISM.to_string(),
target_format: format!("{FORMAT:?}"),
luminance_threshold: LUMA_THRESHOLD,
fill_rule: "NonZero".to_string(),
glyphs,
notes: vec![
"AA is nominally 8x on both candidates — the highest sample count both name, so pin \
4's identical-configuration requirement is met as stated. The mechanisms are NOT the \
same: C1 uses a hardware multisample render-target attachment (hence its \
TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES request), C2 uses vello's compute-shader AA \
into a sample_count:1 storage texture. Matching integers do not imply matching work \
or matching cost; this is immaterial to Round 1's capability verdict and material to \
Round 4's timings."
.to_string(),
"Format deviation: Rgba8Unorm, not sRGB — vello's render_to_texture requires \
Rgba8Unorm + STORAGE_BINDING. Both candidates pinned to it for fairness; immaterial \
to fill correctness since only pure black/white are drawn."
.to_string(),
"Whole outline filled as ONE compound BezPath in one Scene::fill call.".to_string(),
],
})
}
fn main() -> Result<()> {
let oracle = harness::load_oracle();
// Semantic validation before anything renders: an oracle that deserializes
// but no longer means what Round 1 requires would be tested faithfully and
// pass (harness `OracleFile::validate`).
oracle
.validate()
.map_err(|e| anyhow!("oracle failed validation: {e}"))?;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::VULKAN,
..wgpu::InstanceDescriptor::new_without_display_handle()
});
let adapters = pollster::block_on(instance.enumerate_adapters(wgpu::Backends::VULKAN));
// Pin 4 requires BOTH adapter classes, and the integrated figure is the one
// that decides. Reporting overall PASS after testing whichever adapter
// happened to enumerate would silently narrow the claim, so a missing class
// is NOT RUN (an environment absence) and never a pass.
let has_discrete = adapters
.iter()
.any(|a| a.get_info().device_type == wgpu::DeviceType::DiscreteGpu);
let has_integrated = adapters
.iter()
.any(|a| a.get_info().device_type == wgpu::DeviceType::IntegratedGpu);
if !has_discrete || !has_integrated {
let found: Vec<String> = adapters
.iter()
.map(|a| {
let i = a.get_info();
format!("{} ({:?})", i.name, i.device_type)
})
.collect();
return Err(anyhow!(
"NOT RUN: pin 4 requires one discrete and one integrated Vulkan adapter; found \
{found:?}. This is an environment absence, not a candidate failure re-run where \
both are present."
));
}
let mut all_pass = true;
for adapter in &adapters {
let report = run_on(adapter, &oracle)?;
print!("{}", report.table());
let total: usize = report.glyphs.iter().map(|g| g.points.len()).sum();
let passed: usize = report
.glyphs
.iter()
.map(|g| g.points.iter().filter(|p| p.pass).count())
.sum();
println!(
"RESULT {} :: {passed}/{total} points PASS\n",
report.adapter.name
);
all_pass &= report.all_pass();
}
if all_pass {
println!("C2 vello: PASS on all {} adapter(s)", adapters.len());
Ok(())
} else {
Err(anyhow!("C2 vello: FAIL — see the per-point table above"))
}
}

View File

@ -0,0 +1,21 @@
[package]
name = "round1-harness"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
# Round 1 shared harness (CONTRACT_EDITOR_T4_SPIKE.md, Round 1: "criterion 1,
# compound-path fill correctness"). Candidate-neutral: loads the frozen
# round1-oracle/oracle.json, fetches typed outlines from BravuraGlyphCatalog,
# and provides the pixel-classification / report-table plumbing shared by
# both candidate binaries (c1-egui-lyon, c2-vello). This crate carries no
# GPU/wgpu/windowing dependency of its own — each candidate binary owns its
# own render path, per the contract's "the stack under test is the real one"
# requirement; this crate only reads oracle data and post-processes a readback
# buffer either candidate hands it.
[dependencies]
epiphany-glyphs = { path = "../../../../crates/epiphany-glyphs" }
epiphany-layout-ir = { path = "../../../../crates/epiphany-layout-ir" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@ -0,0 +1,638 @@
//! 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,
})
}