brush-paint: skip-when-not-tracing + precomputed disk offsets
Two perf wins in the walker's per-step inner loop, both bit-exact against the alphabet report. 1. Skip lookahead breakdown for rejected candidates when not tracing. Walker's per-direction loop unconditionally called lookahead_score_breakdown even when rejected_back or rejected_off_ink was true — the score was only used to record would-be values in the debug viewer. When trace is None (every optimizer call), this was wasted work on ~30% of candidates. 2. Precompute the disk's pixel-offset list once per brush radius (set_brush populates Grid::disk_offsets) and reuse for evaluate_disk, paint_disk, and measure_sweep_full. Iteration shrinks from a (2·⌈r+1⌉+1)² bounding box to ~π·(r+0.5)² cells — roughly half for typical radii. The mask is a superset of any actual fractional disk (uses the closest-point-of-pixel-square criterion to ensure no in-disk pixel can ever be missed), so the inner-loop fractional distance check produces exactly the same accepted set as the original bounding-box scan.
This commit is contained in:
@@ -286,13 +286,12 @@ fn sdf_percentile(dist: &std::collections::HashMap<(u32, u32), f32>, q: f32) ->
|
||||
/// baseline single-pass sweep has some repaint from adjacent-sample
|
||||
/// disk overlap (~4× per pixel at step_size_factor=0.5); higher
|
||||
/// values mean the path is doubling back over itself.
|
||||
fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid, brush_radius: f32)
|
||||
fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid)
|
||||
-> (u32, u32, u32)
|
||||
{
|
||||
if strokes.is_empty() { return (0, 0, 0); }
|
||||
let mut count = vec![0u32; grid.was_ink.len()];
|
||||
let r = (brush_radius + 1.0).ceil() as i32;
|
||||
let r2 = brush_radius * brush_radius;
|
||||
let r2 = grid.brush_radius_sq;
|
||||
for stroke in strokes {
|
||||
for win in stroke.windows(2) {
|
||||
let (a, b) = (win[0], win[1]);
|
||||
@@ -305,8 +304,7 @@ fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid, brush_radius: f3
|
||||
let cy = a.1 + dy * t;
|
||||
let cx_i = cx.round() as i32;
|
||||
let cy_i = cy.round() as i32;
|
||||
for ddy in -r..=r {
|
||||
for ddx in -r..=r {
|
||||
for &(ddx, ddy) in &grid.disk_offsets {
|
||||
let dxr = (cx_i + ddx) as f32 - cx;
|
||||
let dyr = (cy_i + ddy) as f32 - cy;
|
||||
if dxr * dxr + dyr * dyr > r2 { continue; }
|
||||
@@ -318,7 +316,6 @@ fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid, brush_radius: f3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut bg = 0u32;
|
||||
let mut total = 0u32;
|
||||
let mut repaint = 0u32;
|
||||
@@ -460,6 +457,18 @@ struct Grid {
|
||||
ink_total: i32,
|
||||
/// Currently unpainted ink pixel count.
|
||||
ink_remaining: i32,
|
||||
/// Brush radius used for disk operations. Set via `set_brush`
|
||||
/// before any disk evaluation/painting; populates `disk_offsets`.
|
||||
brush_radius: f32,
|
||||
/// brush_radius² — precomputed for the inner disk-membership check.
|
||||
brush_radius_sq: f32,
|
||||
/// Pixel offsets (dx, dy) from a rounded disk center such that the
|
||||
/// pixel might land within `brush_radius` of any sub-pixel point
|
||||
/// that rounds to that center. The list is a small superset of any
|
||||
/// actual disk; the per-pixel distance check inside the disk loops
|
||||
/// then prunes to the exact fractional disk for waypoint p. Saves
|
||||
/// ~50% of inner-loop iterations vs the bare bounding-box scan.
|
||||
disk_offsets: Vec<(i32, i32)>,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
@@ -514,7 +523,34 @@ impl Grid {
|
||||
let skeleton_length = skel.len() as u32;
|
||||
|
||||
Self { bx, by, width, height, unpainted, was_ink, sdf, skel_endpoints,
|
||||
skeleton_length, ink_total: count, ink_remaining: count }
|
||||
skeleton_length, ink_total: count, ink_remaining: count,
|
||||
brush_radius: 0.0, brush_radius_sq: 0.0, disk_offsets: Vec::new() }
|
||||
}
|
||||
|
||||
/// Configure the disk shape used for evaluate_disk / paint_disk /
|
||||
/// measure_sweep_full. Must be called before any of those run.
|
||||
/// Computed offsets are a superset of any actual fractional-disk
|
||||
/// at this radius: a pixel (dx, dy) is included iff the closest
|
||||
/// point of the [-0.5, 0.5)² square around it to the origin is
|
||||
/// within `brush_radius`. The inner-loop fractional check then
|
||||
/// prunes to the exact disk for waypoint p, so the result is
|
||||
/// bit-exact w.r.t. iterating the full bounding box.
|
||||
fn set_brush(&mut self, brush_radius: f32) {
|
||||
self.brush_radius = brush_radius;
|
||||
self.brush_radius_sq = brush_radius * brush_radius;
|
||||
let r2 = brush_radius * brush_radius;
|
||||
let mask_r = (brush_radius + 0.5).ceil() as i32;
|
||||
let mut offsets: Vec<(i32, i32)> = Vec::with_capacity(((2 * mask_r + 1) * (2 * mask_r + 1)) as usize);
|
||||
for dy in -mask_r..=mask_r {
|
||||
for dx in -mask_r..=mask_r {
|
||||
let nx = ((dx.abs() as f32) - 0.5).max(0.0);
|
||||
let ny = ((dy.abs() as f32) - 0.5).max(0.0);
|
||||
if nx * nx + ny * ny <= r2 {
|
||||
offsets.push((dx, dy));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.disk_offsets = offsets;
|
||||
}
|
||||
|
||||
/// Look up SDF at an integer pixel.
|
||||
@@ -604,16 +640,14 @@ impl Grid {
|
||||
/// bg: pixels that were never ink (heavy penalty — these
|
||||
/// become visible off-glyph paint on the actual plot)
|
||||
/// Does NOT mutate the grid.
|
||||
fn evaluate_disk(&self, p: (f32, f32), radius: f32) -> (i32, i32, i32) {
|
||||
fn evaluate_disk(&self, p: (f32, f32)) -> (i32, i32, i32) {
|
||||
let cx_i = p.0.round() as i32;
|
||||
let cy_i = p.1.round() as i32;
|
||||
let r = (radius + 1.0).ceil() as i32;
|
||||
let r2 = radius * radius;
|
||||
let r2 = self.brush_radius_sq;
|
||||
let mut new_ink = 0;
|
||||
let mut repaint_ink = 0;
|
||||
let mut bg = 0;
|
||||
for dy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
for &(dx, dy) in &self.disk_offsets {
|
||||
// True distance from float waypoint to integer pixel
|
||||
// center — keeps the brush's footprint shifting smoothly
|
||||
// with sub-pixel waypoint motion (without this, small
|
||||
@@ -633,20 +667,17 @@ impl Grid {
|
||||
bg += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
(new_ink, repaint_ink, bg)
|
||||
}
|
||||
|
||||
/// Paint a disk: marks ink pixels under it as painted. Returns the
|
||||
/// number of ink pixels newly painted.
|
||||
fn paint_disk(&mut self, p: (f32, f32), radius: f32) -> i32 {
|
||||
fn paint_disk(&mut self, p: (f32, f32)) -> i32 {
|
||||
let cx_i = p.0.round() as i32;
|
||||
let cy_i = p.1.round() as i32;
|
||||
let r = (radius + 1.0).ceil() as i32;
|
||||
let r2 = radius * radius;
|
||||
let r2 = self.brush_radius_sq;
|
||||
let mut newly = 0;
|
||||
for dy in -r..=r {
|
||||
for dx in -r..=r {
|
||||
for &(dx, dy) in &self.disk_offsets {
|
||||
let ddx = (cx_i + dx) as f32 - p.0;
|
||||
let ddy = (cy_i + dy) as f32 - p.1;
|
||||
if ddx * ddx + ddy * ddy > r2 { continue; }
|
||||
@@ -659,7 +690,6 @@ impl Grid {
|
||||
newly += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.ink_remaining -= newly;
|
||||
newly
|
||||
}
|
||||
@@ -815,7 +845,7 @@ fn walk_brush(start: (f32, f32), init_dir: Option<(f32, f32)>,
|
||||
|
||||
let mut p = start;
|
||||
let mut path = vec![p];
|
||||
grid.paint_disk(p, brush_radius);
|
||||
grid.paint_disk(p);
|
||||
|
||||
let mut prev_dir: Option<(f32, f32)> = init_dir.map(vec_unit);
|
||||
|
||||
@@ -852,6 +882,12 @@ fn walk_brush(start: (f32, f32), init_dir: Option<(f32, f32)>,
|
||||
|
||||
// Compute breakdown either way (cheap-ish; lets the viz show
|
||||
// even rejected directions' would-be score).
|
||||
// Skip the disk evaluation entirely for rejected candidates
|
||||
// when not tracing. The breakdown is only needed to record
|
||||
// would-be scores in the debug viewer; the walker never
|
||||
// picks a rejected candidate as `best`.
|
||||
if !recording && (rejected_back || rejected_off_ink) { continue; }
|
||||
|
||||
let (new_ink, repaint, bg) = lookahead_score_breakdown(
|
||||
p, dir, grid, params, brush_radius, step_size);
|
||||
let momentum_bonus = if has_momentum {
|
||||
@@ -920,7 +956,7 @@ fn walk_brush(start: (f32, f32), init_dir: Option<(f32, f32)>,
|
||||
p = new_p;
|
||||
path.push(p);
|
||||
prev_dir = Some(dir);
|
||||
grid.paint_disk(p, brush_radius);
|
||||
grid.paint_disk(p);
|
||||
step_idx += 1;
|
||||
}
|
||||
|
||||
@@ -945,7 +981,7 @@ fn lookahead_score_breakdown(start: (f32, f32), dir: (f32, f32),
|
||||
for k in 1..=params.lookahead_steps {
|
||||
let p = (start.0 + dir.0 * step_size * k as f32,
|
||||
start.1 + dir.1 * step_size * k as f32);
|
||||
let (new, repaint, bg) = grid.evaluate_disk(p, brush_radius);
|
||||
let (new, repaint, bg) = grid.evaluate_disk(p);
|
||||
let weight = 1.0 / (k as f32);
|
||||
total_new += new as f32 * weight;
|
||||
total_repaint += repaint as f32 * weight;
|
||||
@@ -1031,6 +1067,7 @@ pub fn paint_fill_with(hull: &Hull, params: &PaintParams) -> FillResult {
|
||||
let brush_radius = params.brush_radius_factor * effective_sdf + params.brush_radius_offset_px;
|
||||
|
||||
let mut grid = Grid::from_hull(hull);
|
||||
grid.set_brush(brush_radius);
|
||||
let mut strokes: Vec<Vec<(f32, f32)>> = Vec::new();
|
||||
|
||||
let brush_area = std::f32::consts::PI * brush_radius * brush_radius;
|
||||
@@ -1048,7 +1085,7 @@ pub fn paint_fill_with(hull: &Hull, params: &PaintParams) -> FillResult {
|
||||
} else { path };
|
||||
strokes.push(simplified);
|
||||
} else {
|
||||
grid.paint_disk(start, brush_radius);
|
||||
grid.paint_disk(start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1383,6 +1420,7 @@ pub fn paint_fill_debug(hull: &Hull, params: &PaintParams) -> PaintDebug {
|
||||
let brush_radius = params.brush_radius_factor * effective_sdf + params.brush_radius_offset_px;
|
||||
|
||||
let mut grid = Grid::from_hull(hull);
|
||||
grid.set_brush(brush_radius);
|
||||
let mut trajectories: Vec<Vec<(f32, f32)>> = Vec::new();
|
||||
let mut starts: Vec<(f32, f32)> = Vec::new();
|
||||
|
||||
@@ -1403,7 +1441,7 @@ pub fn paint_fill_debug(hull: &Hull, params: &PaintParams) -> PaintDebug {
|
||||
starts.push(path[0]);
|
||||
trajectories.push(path);
|
||||
} else {
|
||||
grid.paint_disk(start, brush_radius);
|
||||
grid.paint_disk(start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1416,7 +1454,7 @@ pub fn paint_fill_debug(hull: &Hull, params: &PaintParams) -> PaintDebug {
|
||||
|
||||
let (sdf_b64, _) = encode_sdf_b64(hull);
|
||||
let ink_unpainted = grid.ink_remaining.max(0) as u32;
|
||||
let (bg_painted, total_swept, repaint) = measure_sweep_full(&strokes, &grid, brush_radius);
|
||||
let (bg_painted, total_swept, repaint) = measure_sweep_full(&strokes, &grid);
|
||||
let skeleton_length = grid.skeleton_length;
|
||||
let unpainted_clusters = grid.unpainted_cluster_sizes();
|
||||
PaintDebug {
|
||||
|
||||
Reference in New Issue
Block a user