Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Has been cancelled
no-vanity-metrics / WAI public text names no comparison (pull_request) Has been cancelled
no-vanity-metrics / WAI public text names no company or product (pull_request) Has been cancelled
standards workspaces build + test / jouleclaw-rs (pull_request) Has been cancelled
standards workspaces build + test / aac-rs (pull_request) Has been cancelled
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
standards workspaces build + test / eoc-rs (pull_request) Has been cancelled
standards workspaces build + test / jcp-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / joulecontract-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / map-rs (pull_request) Has been cancelled
standards workspaces build + test / mesh-rs (pull_request) Has been cancelled
standards workspaces build + test / openpay (pull_request) Has been cancelled
standards workspaces build + test / proof-rs (pull_request) Has been cancelled
standards workspaces build + test / sandbox-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / smart-byte-rs (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI section references / cited sections exist (pull_request) Has been cancelled
wai-vector vector graphics / linux-aarch64 (pull_request) Has been cancelled
wai-vector vector graphics / linux-x86_64 (pull_request) Has been cancelled
wai-vector vector graphics / macos-arm64 (pull_request) Has been cancelled
wai-vector vector graphics / wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
wai-vector vector graphics / wasm32 under Node (pull_request) Has been cancelled
wai-vector vector graphics / fuzz, peak allocation, comparison count (pull_request) Has been cancelled
workflow hygiene / every workflow supersedes its own queue (pull_request) Has been cancelled
The D7-2 cleanup (no generated byte changes), with the fuzz seed conflict resolved to D7-2's literal; the raster's chunked loops on as_chunks, collapsed conditionals, and no placeholder bindings. Co-Authored-By: Claude Opus 5.5 <[email protected]>
242 lines
11 KiB
Rust
242 lines
11 KiB
Rust
//! The raster stage family RS1–RS9 and the frame procedure (§3.5–§3.11): the values
|
||
//! and properties each test names, on the vectors of `vector-conformance/` and on
|
||
//! shapes built here. Reference values for the negative-coordinate and fold cases come
|
||
//! from review A's independent implementation of §3.3–§3.8.
|
||
|
||
use wai_vector::arith::div255;
|
||
use wai_vector::cover::{self, Cov};
|
||
use wai_vector::frame::{render, render_request};
|
||
use wai_vector::geom::Pt;
|
||
use wai_vector::refuse::Code;
|
||
use wai_vector::vectors::raster::{comb, height_for_alpha, j3_triples};
|
||
use wai_vector::vectors::{corpus_store, find};
|
||
use wai_vector::wvg1::{self, Loaded};
|
||
|
||
fn load(group: &str, name: &str) -> Loaded {
|
||
let v = find(group, name).unwrap_or_else(|| panic!("no vector {group}/{name}"));
|
||
wvg1::load(&v.bytes, &mut corpus_store()).unwrap_or_else(|e| panic!("{group}/{name}: {e:?}"))
|
||
}
|
||
|
||
fn closed(pts: &[(i64, i64)]) -> Vec<Pt> {
|
||
let mut v: Vec<Pt> = pts.iter().map(|&(x, y)| [x, y]).collect();
|
||
v.push(v[0]);
|
||
v
|
||
}
|
||
|
||
fn dense(c: &Cov, w: i64, h: i64) -> Vec<Vec<i32>> {
|
||
(0..h).map(|r| (0..w).map(|x| c.at(x, r)).collect()).collect()
|
||
}
|
||
|
||
/// Rows and cells at negative coordinates: the crossings are floors of negative
|
||
/// rationals. The values are review A's `core.py` on the same polygons.
|
||
#[test]
|
||
fn rows_and_cells_at_negative_coordinates() {
|
||
let a = closed(&[(-700, -300), (900, 200), (300, 1100), (-1000, 800)]);
|
||
let want_a = vec![
|
||
vec![-131072, -111416, -70656, -18200, 0],
|
||
vec![-131072, -131072, -122332, -13254, 0],
|
||
vec![-131072, -131072, -48384, 0, 0],
|
||
vec![-131072, -91706, -117, 0, 0],
|
||
vec![-18176, -10004, 0, 0, 0],
|
||
];
|
||
assert_eq!(dense(&cover::accumulate(&[a], 5, 5, 4), 5, 5), want_a);
|
||
let c = closed(&[(-1500, -1300), (700, -200), (-301, 1333)]);
|
||
let want_c = vec![vec![-131072, -112482, -4959, 0], vec![-130379, -32558, 0, 0], vec![-77312, 0, 0, 0], vec![-6834, 0, 0, 0], vec![0; 4], vec![0; 4]];
|
||
assert_eq!(dense(&cover::accumulate(&[c], 4, 6, 3), 4, 6), want_c);
|
||
}
|
||
|
||
/// The left fold: a fill crossing the left edge has, on every canvas pixel, the
|
||
/// values of the same fill moved 8 pixels right onto a wider canvas, unclipped.
|
||
#[test]
|
||
fn the_left_fold() {
|
||
let d = [(-1800, 100), (600, 350), (200, 900), (-2600, 700)];
|
||
let folded = cover::accumulate(&[closed(&d)], 4, 4, 4);
|
||
let moved: Vec<(i64, i64)> = d.iter().map(|&(x, y)| (x + 8 * 256, y)).collect();
|
||
let wide = cover::accumulate(&[closed(&moved)], 12, 4, 4);
|
||
for r in 0..4 {
|
||
for c in 0..4 {
|
||
assert_eq!(folded.at(c, r), wide.at(c + 8, r), "pixel ({c}, {r})");
|
||
}
|
||
}
|
||
// review A's core.py on the same polygon.
|
||
assert_eq!(dense(&folded, 4, 4), vec![vec![0, 0, 0, 0], vec![-108288, -93460, -11440, 0], vec![-131072, -68096, 0, 0], vec![-60272, -2200, 0, 0]]);
|
||
// A contour wholly left of the canvas folds to zero sums: it draws nothing (P5).
|
||
let left = cover::accumulate(&[closed(&[(-30_000, 1_000), (-25_000, 1_000), (-26_000, 3_000)])], 4, 4, 3);
|
||
assert!(left.v.iter().all(|&x| x == 0));
|
||
}
|
||
|
||
/// A-F5's triangle pins `v_k`: the snapped polygon's area, `|v|` = 1280 and 512 in its
|
||
/// two pixels, not the outline's (1152 and 384).
|
||
#[test]
|
||
fn triangle_pins_v_to_the_snapped_polygon() {
|
||
let t = cover::accumulate(&[closed(&[(0, 0), (512, 3), (0, 3)])], 4, 1, 3);
|
||
let v = [t.at(0, 0).abs(), t.at(1, 0).abs()];
|
||
assert_eq!(v, [1280, 512]);
|
||
assert_ne!(v, [1152, 384], "an exact-area value");
|
||
// The vector renders the same: alpha 2 and 1 in a white fill.
|
||
let l = load("fills", "triangle-af5");
|
||
let px = render(&l, 0, 0, 0).unwrap();
|
||
assert_eq!(&px[0..8], &[2, 2, 2, 2, 1, 1, 1, 1]);
|
||
}
|
||
|
||
/// The glyph-wise union: three glyphs of one run abutting mid-pixel leave no seam; two
|
||
/// runs abutting composite by source-over and do.
|
||
#[test]
|
||
fn abutting_run_seam_pixel_is_255() {
|
||
let l = load("runs", "abutting");
|
||
let px = render(&l, 0, 0, 0).unwrap();
|
||
let w = 16;
|
||
// Glyph 1 spans x [0, 384) and y [512, 1536): rows 2..=5. The glyphs at 0, 384 and
|
||
// 768 meet mid-pixel at x = 384 (column 1); the last ends at 1152, half of column 4.
|
||
for r in 2..6 {
|
||
let at = 4 * (r * w + 1);
|
||
assert_eq!(&px[at..at + 4], &[255, 255, 255, 255], "the seam at (1, {r})");
|
||
let at = 4 * (r * w + 4);
|
||
assert_eq!(&px[at..at + 4], &[128, 128, 128, 128], "half of (4, {r})");
|
||
}
|
||
// The same two glyphs in two runs: source-over at the seam, 128 + div255(128·127).
|
||
let l = load("runs", "two-runs");
|
||
let px = render(&l, 0, 0, 0).unwrap();
|
||
let at = 4 * (2 * w + 1);
|
||
let over = 128 + div255(128 * 127) as u8;
|
||
assert_eq!(&px[at..at + 4], &[over; 4], "across runs the seam returns");
|
||
assert!(over < 255);
|
||
}
|
||
|
||
/// J3's discriminating triples: each pixel has the frozen fill order's bytes,
|
||
/// `div255(div255(p·o)·α)`, which the other order does not give.
|
||
#[test]
|
||
fn j3_triples_and_group_isolation() {
|
||
let l = load("composite", "j3-triples");
|
||
let px = render(&l, 0, 0, 0).unwrap();
|
||
for (i, (p, o, a)) in j3_triples().into_iter().enumerate() {
|
||
assert!(height_for_alpha(a) > 0);
|
||
let frozen = div255(div255(u32::from(p) * u32::from(o)) * u32::from(a)) as u8;
|
||
let other = div255(u32::from(p) * div255(u32::from(a) * u32::from(o))) as u8;
|
||
assert_ne!(frozen, other);
|
||
let at = 4 * (2 * i);
|
||
assert_eq!(&px[at..at + 4], &[frozen; 4], "triple {i}: ({p}, {o}, {a})");
|
||
}
|
||
// Isolation: the overlap of two opaque fills in a group at 128 is the top fill at
|
||
// half, div255(c·128), the group composited as one layer.
|
||
let l = load("composite", "isolation");
|
||
let px = render(&l, 0, 0, 0).unwrap();
|
||
let at = 4 * (16 * 3 + 7);
|
||
let green = [40u32, 180, 80, 255].map(|c| div255(c * 128) as u8);
|
||
assert_eq!(&px[at..at + 4], &green);
|
||
}
|
||
|
||
/// P1: reversing every contour of a fill changes no byte of any frame.
|
||
#[test]
|
||
fn whole_fill_reversal_changes_nothing() {
|
||
let mut n = 0;
|
||
for name in ["rect", "hole", "star-nonzero", "star-evenodd", "random-0", "random-1", "random-2", "random-3", "curves"] {
|
||
let (a, b) = (load("fills", name), load("fills", &format!("{name}-reversed")));
|
||
assert_ne!(a.doc.write(), b.doc.write());
|
||
for z in 0..a.doc.sizes.len() {
|
||
assert_eq!(render(&a, z, 0, 0).unwrap(), render(&b, z, 0, 0).unwrap(), "{name}");
|
||
n += 1;
|
||
}
|
||
}
|
||
assert_eq!(n, 9);
|
||
}
|
||
|
||
/// P2: a roll at an integer speed with keys on frame ticks: frame k is frame 0 moved
|
||
/// up 2k pixels at the design size and 4k at twice it.
|
||
#[test]
|
||
fn integer_speed_roll_shifts_whole_pixels() {
|
||
let l = load("motion", "roll-integer");
|
||
for (z, step) in [(0usize, 2usize), (1, 4)] {
|
||
let (w, h) = (l.doc.sizes[z].w as usize, l.doc.sizes[z].h as usize);
|
||
let f0 = render(&l, z, 0, 0).unwrap();
|
||
for k in 1..10u64 {
|
||
let fk = render(&l, z, 0, k).unwrap();
|
||
let s = step * k as usize;
|
||
for r in 0..h {
|
||
let want: &[u8] = if r + s < h { &f0[4 * w * (r + s)..4 * w * (r + s + 1)] } else { &[0u8; 0] };
|
||
let got = &fk[4 * w * r..4 * w * (r + 1)];
|
||
if want.is_empty() {
|
||
assert!(got.iter().all(|&x| x == 0), "size {z}, frame {k}, row {r}: below the moved content");
|
||
} else {
|
||
assert_eq!(got, want, "size {z}, frame {k}, row {r}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// P7: each piece's frames equal the whole's at the same frame number.
|
||
#[test]
|
||
fn slicing_piece_frames_equal_the_whole() {
|
||
let whole = load("slicing", "whole");
|
||
let mut n = 0;
|
||
for i in 0..3 {
|
||
let p = load("slicing", &format!("piece-{i}"));
|
||
for (r, rate) in p.doc.rates.iter().enumerate() {
|
||
let ticks = rate.ticks_per_frame(p.doc.header.tb);
|
||
for g in wai_vector::clock::frames(p.doc.header.t0, p.doc.header.dur, ticks) {
|
||
assert_eq!(render(&p, 0, r, g).unwrap(), render(&whole, 0, r, g).unwrap(), "piece {i}, rate {r}, frame {g}");
|
||
n += 1;
|
||
}
|
||
}
|
||
}
|
||
assert_eq!(n, 90);
|
||
}
|
||
|
||
/// §3.7: the comb of 16,382 points attains `D_r = 256·p`, and renders with every
|
||
/// value in `i32` (a debug build checks each addition).
|
||
#[test]
|
||
fn the_comb_attains_the_density_bound() {
|
||
let pts: Vec<Pt> = comb(16_382, 0, 1, 512, 768).iter().map(|p| [i64::from(p.x), i64::from(p.y)]).chain(std::iter::once([0, 512])).collect();
|
||
let d = cover::row_density(std::slice::from_ref(&pts), 0, 15);
|
||
assert_eq!(d[2], 256 * 16_382);
|
||
assert_eq!(d.iter().copied().max(), Some(256 * 16_382));
|
||
let c = cover::accumulate(&[pts], 64, 16, 16_382);
|
||
assert!(c.v.iter().all(|&x| x.unsigned_abs() <= 512 * 256 * 16_382));
|
||
let l = load("fills", "comb-16382");
|
||
render(&l, 0, 0, 0).unwrap();
|
||
let l = load("fills", "points-16383");
|
||
render(&l, 0, 0, 0).unwrap();
|
||
}
|
||
|
||
/// The density assertion is live: a comb of 16,384 points, built here past the cap a
|
||
/// payload enforces, attains `D_r = 2^22 > 2^22 − 256`, and a debug build stops it.
|
||
#[cfg(debug_assertions)]
|
||
#[test]
|
||
#[should_panic(expected = "D_r = 4194304")]
|
||
fn the_density_assertion_fires_past_the_cap() {
|
||
let pts: Vec<Pt> = comb(16_384, 0, 1, 512, 768).iter().map(|p| [i64::from(p.x), i64::from(p.y)]).chain(std::iter::once([0, 512])).collect();
|
||
cover::accumulate(&[pts], 64, 16, 16_384);
|
||
}
|
||
|
||
/// The render request errors, and the frame range by division.
|
||
#[test]
|
||
fn render_requests() {
|
||
let l = load("motion", "roll-integer");
|
||
assert!(render_request(&l, 160, 90, 25, 1, 0).is_ok());
|
||
assert_eq!(render_request(&l, 161, 90, 25, 1, 0).unwrap_err().code, Code::SizeUnlisted);
|
||
assert_eq!(render_request(&l, 160, 90, 30, 1, 0).unwrap_err().code, Code::RateUnlisted);
|
||
assert_eq!(render_request(&l, 160, 90, 25, 1, 10).unwrap_err().code, Code::FrameRange);
|
||
assert_eq!(render_request(&l, 160, 90, 25, 1, u64::MAX).unwrap_err().code, Code::FrameRange);
|
||
}
|
||
|
||
/// RS9 and P4: every rendered pixel has `R, G, B ≤ A`, and the straight-alpha view is
|
||
/// `R_rat(255·c, A)`.
|
||
#[test]
|
||
fn premultiplied_closure_and_the_straight_view() {
|
||
for (g, n) in [("composite", "groups-depth-4"), ("composite", "overlap-opacity"), ("composite", "group-pop"), ("motion", "lower-third")] {
|
||
let l = load(g, n);
|
||
let px = render(&l, 0, 0, 0).unwrap();
|
||
assert!(px.as_chunks::<4>().0.iter().all(|p| p[0] <= p[3] && p[1] <= p[3] && p[2] <= p[3]), "{n}");
|
||
let s = wai_vector::paint::unpremul_buffer(&px);
|
||
for (p, q) in px.as_chunks::<4>().0.iter().zip(s.as_chunks::<4>().0) {
|
||
if p[3] == 0 {
|
||
assert_eq!(q, &[0, 0, 0, 0]);
|
||
} else {
|
||
for ch in 0..3 {
|
||
assert_eq!(i64::from(q[ch]), wai_vector::arith::r_rat(255 * i64::from(p[ch]), i64::from(p[3])));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|