Skip to main content

jxl_encoder/
debug_rect.rs

1// Copyright (c) Imazen LLC and the JPEG XL Project Authors.
2// Licensed under AGPL-3.0-or-later. Commercial licenses at https://www.imazen.io/pricing
3
4//! Spatial decision logging for encoder debugging.
5//!
6//! When the `debug-rect` feature is enabled, the `debug_rect!` macro logs every
7//! encoder decision alongside the rectangle it affects. Logs are collected in a
8//! global buffer and flushed to a sidecar CSV when the frame is complete.
9//!
10//! When the feature is disabled, the macro compiles to nothing.
11//!
12//! # CSV format
13//!
14//! ```text
15//! stage,x,y,w,h,message
16//! ```
17//!
18//! # Query workflow
19//!
20//! 1. Encode with `--features debug-rect` → produces `output.jxl.debug_rect.csv`
21//! 2. Decode both our JXL and cjxl's JXL, diff the pixels to find divergent regions
22//! 3. Call [`query_overlapping`] (or grep the CSV) for rectangles touching that region
23//! 4. Read the `message` column to understand every decision that affected those pixels
24
25#[cfg(feature = "debug-rect")]
26use std::sync::Mutex;
27
28/// Global log buffer, protected by a mutex.
29/// Each entry is a pre-formatted CSV row (no newline).
30/// Public so the `debug_rect!` macro can access it from other modules.
31#[cfg(feature = "debug-rect")]
32#[doc(hidden)]
33pub static LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
34
35/// Log a spatial decision.
36///
37/// # Parameters
38/// - `stage`: short label for the encoder phase (e.g. `"patches/seed"`, `"patches/cc"`)
39/// - `x, y, w, h`: the affected rectangle in image coordinates
40/// - `msg`: a formatted message describing the decision
41///
42/// When `debug-rect` is disabled this is a no-op.
43#[cfg(feature = "debug-rect")]
44#[macro_export]
45macro_rules! debug_rect {
46    ($stage:expr, $x:expr, $y:expr, $w:expr, $h:expr, $($arg:tt)*) => {{
47        let msg = format!($($arg)*);
48        let row = format!("{},{},{},{},{},{}", $stage, $x, $y, $w, $h, msg.replace(',', ";"));
49        if let Ok(mut buf) = $crate::debug_rect::LOG.lock() {
50            buf.push(row);
51        }
52    }};
53}
54
55/// No-op version when feature is disabled — still evaluates arguments
56/// to suppress unused variable warnings, but the optimizer eliminates everything.
57#[cfg(not(feature = "debug-rect"))]
58#[macro_export]
59macro_rules! debug_rect {
60    ($stage:expr, $x:expr, $y:expr, $w:expr, $h:expr, $($arg:tt)*) => {
61        if false {
62            // Ensure all arguments are type-checked and considered "used"
63            let _ = ($stage, $x, $y, $w, $h);
64            let _ = format_args!($($arg)*);
65        }
66    };
67}
68
69/// Clear the log buffer. Call at the start of each frame encode.
70#[cfg(feature = "debug-rect")]
71pub fn clear() {
72    if let Ok(mut buf) = LOG.lock() {
73        buf.clear();
74    }
75}
76
77#[cfg(not(feature = "debug-rect"))]
78pub fn clear() {}
79
80/// Flush the log buffer to a CSV file. Call when the frame is done.
81///
82/// The file is written to `{base_path}.debug_rect.csv`.
83/// If `base_path` is empty, writes to `debug_rect.csv` in the current directory.
84#[cfg(feature = "debug-rect")]
85pub fn flush(base_path: &str) {
86    let path = if base_path.is_empty() {
87        "debug_rect.csv".to_string()
88    } else {
89        format!("{base_path}.debug_rect.csv")
90    };
91    let rows = {
92        let Ok(buf) = LOG.lock() else { return };
93        buf.clone()
94    };
95    if rows.is_empty() {
96        return;
97    }
98    let mut out = String::with_capacity(rows.len() * 80);
99    out.push_str("stage,x,y,w,h,message\n");
100    for row in &rows {
101        out.push_str(row);
102        out.push('\n');
103    }
104    if let Err(e) = std::fs::write(&path, &out) {
105        eprintln!("debug_rect: failed to write {path}: {e}");
106    } else {
107        eprintln!("debug_rect: wrote {} rows to {path}", rows.len());
108    }
109}
110
111#[cfg(not(feature = "debug-rect"))]
112pub fn flush(_base_path: &str) {}
113
114/// Return all log rows whose rectangle overlaps the query region.
115///
116/// Useful for programmatic queries: given a region of visual difference,
117/// find every decision that touched it.
118#[cfg(feature = "debug-rect")]
119pub fn query_overlapping(qx: i64, qy: i64, qw: i64, qh: i64) -> Vec<String> {
120    let Ok(buf) = LOG.lock() else {
121        return Vec::new();
122    };
123    let mut hits = Vec::new();
124    for row in buf.iter() {
125        // Parse "stage,x,y,w,h,message"
126        let parts: Vec<&str> = row.splitn(6, ',').collect();
127        if parts.len() < 5 {
128            continue;
129        }
130        let Ok(rx) = parts[1].parse::<i64>() else {
131            continue;
132        };
133        let Ok(ry) = parts[2].parse::<i64>() else {
134            continue;
135        };
136        let Ok(rw) = parts[3].parse::<i64>() else {
137            continue;
138        };
139        let Ok(rh) = parts[4].parse::<i64>() else {
140            continue;
141        };
142        // AABB overlap test
143        if rx < qx + qw && rx + rw > qx && ry < qy + qh && ry + rh > qy {
144            hits.push(row.clone());
145        }
146    }
147    hits
148}
149
150#[cfg(not(feature = "debug-rect"))]
151pub fn query_overlapping(_qx: i64, _qy: i64, _qw: i64, _qh: i64) -> Vec<String> {
152    Vec::new()
153}
154
155/// Find the block with the largest per-pixel absolute difference between two images.
156///
157/// Scans `block_size × block_size` blocks (with stride `block_size`) across the image.
158/// Returns `(x, y, block_w, block_h, max_block_sad)` for the worst block.
159///
160/// `img_a` and `img_b` must have the same dimensions: `width * height * channels` bytes,
161/// row-major, interleaved channels.
162#[cfg(feature = "debug-rect")]
163pub fn find_worst_block(
164    img_a: &[u8],
165    img_b: &[u8],
166    width: usize,
167    height: usize,
168    channels: usize,
169    block_size: usize,
170) -> (usize, usize, usize, usize, f64) {
171    assert_eq!(img_a.len(), img_b.len());
172    assert!(img_a.len() >= width * height * channels);
173    assert!(block_size > 0);
174
175    let mut worst_x = 0;
176    let mut worst_y = 0;
177    let mut worst_sad = 0.0_f64;
178
179    let mut by = 0;
180    while by < height {
181        let bh = block_size.min(height - by);
182        let mut bx = 0;
183        while bx < width {
184            let bw = block_size.min(width - bx);
185            let mut sad = 0.0_f64;
186            for dy in 0..bh {
187                let row = (by + dy) * width * channels + bx * channels;
188                for dx_c in 0..(bw * channels) {
189                    let a = img_a[row + dx_c] as f64;
190                    let b = img_b[row + dx_c] as f64;
191                    sad += (a - b).abs();
192                }
193            }
194            if sad > worst_sad {
195                worst_sad = sad;
196                worst_x = bx;
197                worst_y = by;
198            }
199            bx += block_size;
200        }
201        by += block_size;
202    }
203
204    let final_w = block_size.min(width - worst_x);
205    let final_h = block_size.min(height - worst_y);
206    (worst_x, worst_y, final_w, final_h, worst_sad)
207}
208
209#[cfg(not(feature = "debug-rect"))]
210pub fn find_worst_block(
211    _img_a: &[u8],
212    _img_b: &[u8],
213    _width: usize,
214    _height: usize,
215    _channels: usize,
216    _block_size: usize,
217) -> (usize, usize, usize, usize, f64) {
218    (0, 0, 0, 0, 0.0)
219}
220
221/// Diff two decoded images and return the worst block plus all overlapping debug entries.
222///
223/// Convenience wrapper: finds the worst `block_size × block_size` block by SAD,
224/// then queries the debug log for all decisions affecting that block.
225///
226/// Returns `(x, y, w, h, sad, overlapping_rows)`.
227#[cfg(feature = "debug-rect")]
228pub fn diff_and_query(
229    img_a: &[u8],
230    img_b: &[u8],
231    width: usize,
232    height: usize,
233    channels: usize,
234    block_size: usize,
235) -> (usize, usize, usize, usize, f64, Vec<String>) {
236    let (x, y, w, h, sad) = find_worst_block(img_a, img_b, width, height, channels, block_size);
237    let rows = query_overlapping(x as i64, y as i64, w as i64, h as i64);
238    (x, y, w, h, sad, rows)
239}
240
241#[cfg(not(feature = "debug-rect"))]
242pub fn diff_and_query(
243    _img_a: &[u8],
244    _img_b: &[u8],
245    _width: usize,
246    _height: usize,
247    _channels: usize,
248    _block_size: usize,
249) -> (usize, usize, usize, usize, f64, Vec<String>) {
250    (0, 0, 0, 0, 0.0, Vec::new())
251}
252
253/// Find the top N worst blocks by SAD, returning them sorted worst-first.
254///
255/// Each entry is `(x, y, w, h, sad)`.
256#[cfg(feature = "debug-rect")]
257pub fn find_worst_blocks(
258    img_a: &[u8],
259    img_b: &[u8],
260    width: usize,
261    height: usize,
262    channels: usize,
263    block_size: usize,
264    top_n: usize,
265) -> Vec<(usize, usize, usize, usize, f64)> {
266    assert_eq!(img_a.len(), img_b.len());
267    assert!(img_a.len() >= width * height * channels);
268    assert!(block_size > 0);
269
270    let mut blocks: Vec<(usize, usize, usize, usize, f64)> = Vec::new();
271
272    let mut by = 0;
273    while by < height {
274        let bh = block_size.min(height - by);
275        let mut bx = 0;
276        while bx < width {
277            let bw = block_size.min(width - bx);
278            let mut sad = 0.0_f64;
279            for dy in 0..bh {
280                let row = (by + dy) * width * channels + bx * channels;
281                for dx_c in 0..(bw * channels) {
282                    let a = img_a[row + dx_c] as f64;
283                    let b = img_b[row + dx_c] as f64;
284                    sad += (a - b).abs();
285                }
286            }
287            blocks.push((bx, by, bw, bh, sad));
288            bx += block_size;
289        }
290        by += block_size;
291    }
292
293    blocks.sort_by(|a, b| b.4.partial_cmp(&a.4).unwrap_or(core::cmp::Ordering::Equal));
294    blocks.truncate(top_n);
295    blocks
296}
297
298#[cfg(not(feature = "debug-rect"))]
299pub fn find_worst_blocks(
300    _img_a: &[u8],
301    _img_b: &[u8],
302    _width: usize,
303    _height: usize,
304    _channels: usize,
305    _block_size: usize,
306    _top_n: usize,
307) -> Vec<(usize, usize, usize, usize, f64)> {
308    Vec::new()
309}