brush-paint: bit-pack the was_ink and unpainted masks
Replace `Vec<bool>` (1 byte/pixel) with `BitMask` (Vec<u64>, 1 bit/ pixel) for the per-pixel state. A typical letter mask drops from ~40 KB to ~5 KB — fits L1 instead of spilling to L2. Read/write ops are inlined shift+and/or, comparable cost to byte loads but better cache density across the disk-iteration hot path. Bit-exact w.r.t. the alphabet report.
This commit is contained in:
@@ -322,7 +322,7 @@ fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid)
|
|||||||
for (i, &c) in count.iter().enumerate() {
|
for (i, &c) in count.iter().enumerate() {
|
||||||
if c == 0 { continue; }
|
if c == 0 { continue; }
|
||||||
total += 1;
|
total += 1;
|
||||||
if !grid.was_ink[i] { bg += 1; }
|
if !grid.was_ink.get(i) { bg += 1; }
|
||||||
else { repaint += c - 1; }
|
else { repaint += c - 1; }
|
||||||
}
|
}
|
||||||
(bg, total, repaint)
|
(bg, total, repaint)
|
||||||
@@ -415,7 +415,7 @@ fn encode_coverage_b64(grid: &Grid) -> String {
|
|||||||
//
|
//
|
||||||
// For the coverage view, paint UNPAINTED ink red (= missed).
|
// For the coverage view, paint UNPAINTED ink red (= missed).
|
||||||
// Painted/background stays transparent.
|
// Painted/background stays transparent.
|
||||||
if grid.unpainted[idx] {
|
if grid.unpainted.get(idx) {
|
||||||
img.put_pixel(lx as u32, ly as u32, image::Rgba([244, 63, 94, 200]));
|
img.put_pixel(lx as u32, ly as u32, image::Rgba([244, 63, 94, 200]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,17 +427,51 @@ fn encode_coverage_b64(grid: &Grid) -> String {
|
|||||||
format!("data:image/png;base64,{}", b64)
|
format!("data:image/png;base64,{}", b64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bit-packed mask: 1 bit per pixel ────────────────────────────────────
|
||||||
|
|
||||||
|
/// Compact boolean mask backed by `Vec<u64>`. Used for `was_ink` and
|
||||||
|
/// `unpainted` so a 200×200 letter mask is ~5 KB instead of ~40 KB —
|
||||||
|
/// fits L1 nicely, and word-at-a-time popcount is available when
|
||||||
|
/// scanning whole grids. All ops are `#[inline]` since they're called
|
||||||
|
/// from the disk-iteration hot path.
|
||||||
|
struct BitMask {
|
||||||
|
bits: Vec<u64>,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitMask {
|
||||||
|
fn new(n_bits: usize) -> Self {
|
||||||
|
let words = (n_bits + 63) / 64;
|
||||||
|
Self { bits: vec![0u64; words], len: n_bits }
|
||||||
|
}
|
||||||
|
#[inline] fn len(&self) -> usize { self.len }
|
||||||
|
#[inline] fn get(&self, i: usize) -> bool {
|
||||||
|
// Safety: caller guarantees i < self.len; we still bounds-check
|
||||||
|
// via the indexed Vec access (Rust will panic on OOB anyway).
|
||||||
|
(self.bits[i >> 6] >> (i & 63)) & 1 == 1
|
||||||
|
}
|
||||||
|
#[inline] fn set(&mut self, i: usize) {
|
||||||
|
self.bits[i >> 6] |= 1u64 << (i & 63);
|
||||||
|
}
|
||||||
|
#[inline] fn clear(&mut self, i: usize) {
|
||||||
|
self.bits[i >> 6] &= !(1u64 << (i & 63));
|
||||||
|
}
|
||||||
|
fn count_ones(&self) -> u32 {
|
||||||
|
self.bits.iter().map(|w| w.count_ones()).sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Coverage grid: bool per pixel, sized to the hull's bbox ─────────────
|
// ── Coverage grid: bool per pixel, sized to the hull's bbox ─────────────
|
||||||
|
|
||||||
struct Grid {
|
struct Grid {
|
||||||
bx: i32, by: i32,
|
bx: i32, by: i32,
|
||||||
width: i32, height: i32,
|
width: i32, height: i32,
|
||||||
/// `true` = ink pixel that hasn't been painted yet.
|
/// `true` = ink pixel that hasn't been painted yet.
|
||||||
unpainted: Vec<bool>,
|
unpainted: BitMask,
|
||||||
/// `true` = pixel was ink in the original glyph (immutable; never
|
/// `true` = pixel was ink in the original glyph (immutable; never
|
||||||
/// changes after construction). Lets relaxation tell "ink" apart from
|
/// changes after construction). Lets relaxation tell "ink" apart from
|
||||||
/// "background" without conflating it with painted state.
|
/// "background" without conflating it with painted state.
|
||||||
was_ink: Vec<bool>,
|
was_ink: BitMask,
|
||||||
/// Chamfer 3-4 distance / 3 (≈ Euclidean px from boundary). Used to
|
/// Chamfer 3-4 distance / 3 (≈ Euclidean px from boundary). Used to
|
||||||
/// snap raw start points up the gradient onto the medial-axis ridge,
|
/// snap raw start points up the gradient onto the medial-axis ridge,
|
||||||
/// so strokes begin at stroke-centerline rather than polygon-edge.
|
/// so strokes begin at stroke-centerline rather than polygon-edge.
|
||||||
@@ -484,16 +518,16 @@ impl Grid {
|
|||||||
let width = (hull.bounds.x_max as i32 - hull.bounds.x_min as i32 + 1 + 2 * PAD).max(1);
|
let width = (hull.bounds.x_max as i32 - hull.bounds.x_min as i32 + 1 + 2 * PAD).max(1);
|
||||||
let height = (hull.bounds.y_max as i32 - hull.bounds.y_min as i32 + 1 + 2 * PAD).max(1);
|
let height = (hull.bounds.y_max as i32 - hull.bounds.y_min as i32 + 1 + 2 * PAD).max(1);
|
||||||
let cells = (width * height) as usize;
|
let cells = (width * height) as usize;
|
||||||
let mut unpainted = vec![false; cells];
|
let mut unpainted = BitMask::new(cells);
|
||||||
let mut was_ink = vec![false; cells];
|
let mut was_ink = BitMask::new(cells);
|
||||||
let mut sdf = vec![0.0_f32; cells];
|
let mut sdf = vec![0.0_f32; cells];
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for &(x, y) in &hull.pixels {
|
for &(x, y) in &hull.pixels {
|
||||||
let lx = x as i32 - bx; let ly = y as i32 - by;
|
let lx = x as i32 - bx; let ly = y as i32 - by;
|
||||||
if lx < 0 || ly < 0 || lx >= width || ly >= height { continue; }
|
if lx < 0 || ly < 0 || lx >= width || ly >= height { continue; }
|
||||||
let idx = (ly * width + lx) as usize;
|
let idx = (ly * width + lx) as usize;
|
||||||
unpainted[idx] = true;
|
unpainted.set(idx);
|
||||||
was_ink[idx] = true;
|
was_ink.set(idx);
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
// Chamfer distance (per-pixel, in approximate Euclidean units)
|
// Chamfer distance (per-pixel, in approximate Euclidean units)
|
||||||
@@ -603,21 +637,21 @@ impl Grid {
|
|||||||
for sy in 0..self.height {
|
for sy in 0..self.height {
|
||||||
for sx in 0..self.width {
|
for sx in 0..self.width {
|
||||||
let s_idx = (sy * self.width + sx) as usize;
|
let s_idx = (sy * self.width + sx) as usize;
|
||||||
if !self.unpainted[s_idx] || comp_id[s_idx] >= 0 { continue; }
|
if !self.unpainted.get(s_idx) || comp_id[s_idx] >= 0 { continue; }
|
||||||
let id = sizes.len() as i32;
|
let id = sizes.len() as i32;
|
||||||
let mut size = 0u32;
|
let mut size = 0u32;
|
||||||
let mut stack: Vec<(i32, i32)> = vec![(sx, sy)];
|
let mut stack: Vec<(i32, i32)> = vec![(sx, sy)];
|
||||||
while let Some((cx, cy)) = stack.pop() {
|
while let Some((cx, cy)) = stack.pop() {
|
||||||
let cidx = (cy * self.width + cx) as usize;
|
let cidx = (cy * self.width + cx) as usize;
|
||||||
if comp_id[cidx] >= 0 { continue; }
|
if comp_id[cidx] >= 0 { continue; }
|
||||||
if !self.unpainted[cidx] { continue; }
|
if !self.unpainted.get(cidx) { continue; }
|
||||||
comp_id[cidx] = id;
|
comp_id[cidx] = id;
|
||||||
size += 1;
|
size += 1;
|
||||||
for (dx, dy) in [(1, 0i32), (-1, 0), (0, 1), (0, -1)] {
|
for (dx, dy) in [(1, 0i32), (-1, 0), (0, 1), (0, -1)] {
|
||||||
let nx = cx + dx; let ny = cy + dy;
|
let nx = cx + dx; let ny = cy + dy;
|
||||||
if nx < 0 || ny < 0 || nx >= self.width || ny >= self.height { continue; }
|
if nx < 0 || ny < 0 || nx >= self.width || ny >= self.height { continue; }
|
||||||
let nidx = (ny * self.width + nx) as usize;
|
let nidx = (ny * self.width + nx) as usize;
|
||||||
if self.unpainted[nidx] && comp_id[nidx] < 0 {
|
if self.unpainted.get(nidx) && comp_id[nidx] < 0 {
|
||||||
stack.push((nx, ny));
|
stack.push((nx, ny));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,7 +665,7 @@ impl Grid {
|
|||||||
fn is_ink(&self, x: i32, y: i32) -> bool {
|
fn is_ink(&self, x: i32, y: i32) -> bool {
|
||||||
let lx = x - self.bx; let ly = y - self.by;
|
let lx = x - self.bx; let ly = y - self.by;
|
||||||
if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { return false; }
|
if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { return false; }
|
||||||
self.was_ink[(ly * self.width + lx) as usize]
|
self.was_ink.get((ly * self.width + lx) as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns (new_ink, repaint_ink, bg) — pixel counts under disk(p, r):
|
/// Returns (new_ink, repaint_ink, bg) — pixel counts under disk(p, r):
|
||||||
@@ -659,9 +693,9 @@ impl Grid {
|
|||||||
let ly = cy_i + dy - self.by;
|
let ly = cy_i + dy - self.by;
|
||||||
if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { continue; }
|
if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { continue; }
|
||||||
let idx = (ly * self.width + lx) as usize;
|
let idx = (ly * self.width + lx) as usize;
|
||||||
if self.unpainted[idx] {
|
if self.unpainted.get(idx) {
|
||||||
new_ink += 1;
|
new_ink += 1;
|
||||||
} else if self.was_ink[idx] {
|
} else if self.was_ink.get(idx) {
|
||||||
repaint_ink += 1;
|
repaint_ink += 1;
|
||||||
} else {
|
} else {
|
||||||
bg += 1;
|
bg += 1;
|
||||||
@@ -685,8 +719,8 @@ impl Grid {
|
|||||||
let ly = cy_i + dy - self.by;
|
let ly = cy_i + dy - self.by;
|
||||||
if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { continue; }
|
if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { continue; }
|
||||||
let idx = (ly * self.width + lx) as usize;
|
let idx = (ly * self.width + lx) as usize;
|
||||||
if self.unpainted[idx] {
|
if self.unpainted.get(idx) {
|
||||||
self.unpainted[idx] = false;
|
self.unpainted.clear(idx);
|
||||||
newly += 1;
|
newly += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,7 +732,7 @@ impl Grid {
|
|||||||
fn is_unpainted(&self, x: i32, y: i32) -> bool {
|
fn is_unpainted(&self, x: i32, y: i32) -> bool {
|
||||||
let lx = x - self.bx; let ly = y - self.by;
|
let lx = x - self.bx; let ly = y - self.by;
|
||||||
if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { return false; }
|
if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { return false; }
|
||||||
self.unpainted[(ly * self.width + lx) as usize]
|
self.unpainted.get((ly * self.width + lx) as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pick the next stroke's start by analysing the connected components
|
/// Pick the next stroke's start by analysing the connected components
|
||||||
@@ -724,7 +758,7 @@ impl Grid {
|
|||||||
for sy in 0..self.height {
|
for sy in 0..self.height {
|
||||||
for sx in 0..self.width {
|
for sx in 0..self.width {
|
||||||
let s_idx = (sy * self.width + sx) as usize;
|
let s_idx = (sy * self.width + sx) as usize;
|
||||||
if !self.unpainted[s_idx] || comp_id[s_idx] >= 0 { continue; }
|
if !self.unpainted.get(s_idx) || comp_id[s_idx] >= 0 { continue; }
|
||||||
let id = components.len() as i32;
|
let id = components.len() as i32;
|
||||||
let mut pixels: Vec<usize> = Vec::new();
|
let mut pixels: Vec<usize> = Vec::new();
|
||||||
let (mut top, mut left, mut bot, mut right) = (sy, sx, sy, sx);
|
let (mut top, mut left, mut bot, mut right) = (sy, sx, sy, sx);
|
||||||
@@ -732,7 +766,7 @@ impl Grid {
|
|||||||
while let Some((cx, cy)) = stack.pop() {
|
while let Some((cx, cy)) = stack.pop() {
|
||||||
let cidx = (cy * self.width + cx) as usize;
|
let cidx = (cy * self.width + cx) as usize;
|
||||||
if comp_id[cidx] >= 0 { continue; }
|
if comp_id[cidx] >= 0 { continue; }
|
||||||
if !self.unpainted[cidx] { continue; }
|
if !self.unpainted.get(cidx) { continue; }
|
||||||
comp_id[cidx] = id;
|
comp_id[cidx] = id;
|
||||||
pixels.push(cidx);
|
pixels.push(cidx);
|
||||||
if cy < top { top = cy; }
|
if cy < top { top = cy; }
|
||||||
@@ -743,7 +777,7 @@ impl Grid {
|
|||||||
let nx = cx + dx; let ny = cy + dy;
|
let nx = cx + dx; let ny = cy + dy;
|
||||||
if nx < 0 || ny < 0 || nx >= self.width || ny >= self.height { continue; }
|
if nx < 0 || ny < 0 || nx >= self.width || ny >= self.height { continue; }
|
||||||
let nidx = (ny * self.width + nx) as usize;
|
let nidx = (ny * self.width + nx) as usize;
|
||||||
if self.unpainted[nidx] && comp_id[nidx] < 0 {
|
if self.unpainted.get(nidx) && comp_id[nidx] < 0 {
|
||||||
stack.push((nx, ny));
|
stack.push((nx, ny));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user