mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-08-31 01:15:57 -07:00
Session: prove cached renders equal full rebuilds
A 21-step cumulative parameter walk covering every category — finish-phase dials (true cache hits, since the segment key provably ignores them), clustering dials, frontend switches including leaving watershed and returning to its cached hierarchy, compositing, thresholds, palettes and quantization — asserting after each step that the session render is byte-identical to a from-scratch pipeline. Plus: the progress-reporting render path (a separate segmenting branch with the watershed hierarchy shortcut) equals the plain path, a cache warmed by the progress path serves the plain path identically, and invalidate() rebuilds identically.
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
//! `Session` caches the segmentation and re-segments only when a clustering
|
//! `Session` caches the segmentation and re-segments only when a clustering
|
||||||
//! parameter changes — verified both at the key level and end-to-end.
|
//! parameter changes — verified both at the key level and end-to-end.
|
||||||
|
|
||||||
use vtracer::{ColorImage, Config, Session};
|
use visioncortex::Color;
|
||||||
|
use vtracer::{
|
||||||
|
CancelToken, Clustering, ColorImage, Config, FitMode, Hierarchical, Session,
|
||||||
|
};
|
||||||
|
|
||||||
/// A few colored blocks — several clusters.
|
/// A few colored blocks — several clusters.
|
||||||
fn blocks() -> ColorImage {
|
fn blocks() -> ColorImage {
|
||||||
@@ -130,3 +133,150 @@ fn session_matches_one_shot() {
|
|||||||
"re-segmented render must match the one-shot pipeline"
|
"re-segmented render must match the one-shot pipeline"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Blocks plus a gradient band and a small fleck — structure that makes every
|
||||||
|
/// clustering parameter (speckle, precision, gradient step, watershed detail,
|
||||||
|
/// thresholds) actually change the output.
|
||||||
|
fn textured() -> ColorImage {
|
||||||
|
let (w, h) = (48usize, 48usize);
|
||||||
|
let mut pixels = Vec::with_capacity(w * h * 4);
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let c = if y >= 32 {
|
||||||
|
let g = 60 + (x * 3) as u8; // gradient band
|
||||||
|
(g, g, 200)
|
||||||
|
} else if (4..7).contains(&x) && (4..7).contains(&y) {
|
||||||
|
(10, 200, 10) // 9 px fleck
|
||||||
|
} else {
|
||||||
|
match (x / 16, y / 16) {
|
||||||
|
(0, _) => (220u8, 40, 40),
|
||||||
|
(1, _) => (40, 200, 60),
|
||||||
|
_ => (230, 210, 40),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pixels.extend_from_slice(&[c.0, c.1, c.2, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ColorImage {
|
||||||
|
pixels,
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The exhaustive contract: walk a cumulative sequence of config changes that
|
||||||
|
/// touches every parameter category — finish-phase dials, clustering dials,
|
||||||
|
/// frontend switches (including leaving watershed and coming back to its
|
||||||
|
/// cached hierarchy), compositing, palettes — and after each step the cached
|
||||||
|
/// session render must be byte-identical to a from-scratch one-shot pipeline.
|
||||||
|
#[test]
|
||||||
|
fn session_equals_one_shot_across_param_walk() {
|
||||||
|
let img = textured();
|
||||||
|
let mut session = Session::new(img.clone());
|
||||||
|
let mut cfg = Config::default();
|
||||||
|
|
||||||
|
let steps: Vec<(&str, fn(&mut Config))> = vec![
|
||||||
|
("initial", |_| {}),
|
||||||
|
// Finish-phase changes (cache hits).
|
||||||
|
("corner_threshold", |c| c.corner_threshold = 90),
|
||||||
|
("mode polygon", |c| c.mode = FitMode::Polygon),
|
||||||
|
("optimize 2", |c| c.optimize = 2),
|
||||||
|
("cutout", |c| c.hierarchical = Hierarchical::Cutout),
|
||||||
|
("path_precision", |c| c.path_precision = Some(1)),
|
||||||
|
// Clustering changes (re-segment).
|
||||||
|
("filter_speckle", |c| c.filter_speckle = 6),
|
||||||
|
("layer_difference", |c| c.layer_difference = 32),
|
||||||
|
("color_precision", |c| c.color_precision = 5),
|
||||||
|
// Watershed, incl. cheap re-cuts of the cached hierarchy.
|
||||||
|
("watershed", |c| c.clustering = Clustering::Watershed),
|
||||||
|
("detail 200", |c| c.watershed_detail = 200),
|
||||||
|
("detail 64", |c| c.watershed_detail = 64),
|
||||||
|
("stacked", |c| c.hierarchical = Hierarchical::Stacked),
|
||||||
|
("mode spline", |c| c.mode = FitMode::Spline),
|
||||||
|
// Binary, with both thresholding methods.
|
||||||
|
("binary", |c| c.clustering = Clustering::Binary),
|
||||||
|
("threshold 100", |c| c.binary_threshold = 100),
|
||||||
|
("adaptive", |c| c.binary_adaptive = true),
|
||||||
|
// Back to watershed: the hierarchy cache must still be valid.
|
||||||
|
("watershed again", |c| c.clustering = Clustering::Watershed),
|
||||||
|
("quantize", |c| c.max_colors = Some(4)),
|
||||||
|
// And back to the color path with a palette.
|
||||||
|
("color-cluster", |c| {
|
||||||
|
c.clustering = Clustering::ColorCluster;
|
||||||
|
c.max_colors = None;
|
||||||
|
c.palette = vec![
|
||||||
|
Color::new(0, 0, 0),
|
||||||
|
Color::new(255, 255, 255),
|
||||||
|
Color::new(200, 40, 40),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
("speckle again", |c| c.filter_speckle = 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (name, step) in steps {
|
||||||
|
step(&mut cfg);
|
||||||
|
assert_eq!(
|
||||||
|
session.render_svg(&cfg).unwrap(),
|
||||||
|
cfg.build().unwrap().to_svg(&img).unwrap(),
|
||||||
|
"step `{name}`: cached session render must equal a full rebuild"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The progress-reporting render path (which segments through a different
|
||||||
|
/// branch, including the watershed hierarchy shortcut) produces the same
|
||||||
|
/// document as the plain path and the one-shot pipeline.
|
||||||
|
#[test]
|
||||||
|
fn render_with_progress_matches_plain_render() {
|
||||||
|
let img = textured();
|
||||||
|
for clustering in [
|
||||||
|
Clustering::ColorCluster,
|
||||||
|
Clustering::Watershed,
|
||||||
|
Clustering::Binary,
|
||||||
|
] {
|
||||||
|
let cfg = Config {
|
||||||
|
clustering,
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
let one_shot = cfg.build().unwrap().to_svg(&img).unwrap();
|
||||||
|
|
||||||
|
// Fresh session per variant so the progress path does the segmenting.
|
||||||
|
let mut session = Session::new(img.clone());
|
||||||
|
let doc = session
|
||||||
|
.render_with_progress(&cfg, &CancelToken::new(), &mut |_| {})
|
||||||
|
.unwrap();
|
||||||
|
let progress_svg = cfg.build().unwrap().writer.write(&doc);
|
||||||
|
assert_eq!(
|
||||||
|
progress_svg, one_shot,
|
||||||
|
"{clustering:?}: progress path must equal the one-shot pipeline"
|
||||||
|
);
|
||||||
|
|
||||||
|
// And the now-warm cache serves the plain path identically.
|
||||||
|
assert_eq!(
|
||||||
|
session.render_svg(&cfg).unwrap(),
|
||||||
|
one_shot,
|
||||||
|
"{clustering:?}: cache warmed by the progress path must match too"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `invalidate` drops all cached state; the next render rebuilds from scratch
|
||||||
|
/// and still matches.
|
||||||
|
#[test]
|
||||||
|
fn invalidate_then_render_matches() {
|
||||||
|
let img = textured();
|
||||||
|
let cfg = Config {
|
||||||
|
clustering: Clustering::Watershed,
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
let one_shot = cfg.build().unwrap().to_svg(&img).unwrap();
|
||||||
|
|
||||||
|
let mut session = Session::new(img);
|
||||||
|
assert_eq!(session.render_svg(&cfg).unwrap(), one_shot);
|
||||||
|
session.invalidate();
|
||||||
|
assert_eq!(
|
||||||
|
session.render_svg(&cfg).unwrap(),
|
||||||
|
one_shot,
|
||||||
|
"render after invalidate must rebuild identically"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user