Skip to main content

jxl_encoder/
trace.rs

1// Copyright (c) Imazen LLC and the JPEG XL Project Authors.
2// Algorithms and constants derived from libjxl (BSD-3-Clause).
3// Licensed under AGPL-3.0-or-later. Commercial licenses at https://www.imazen.io/pricing
4
5//! Zero-cost bitstream tracing for debugging encoder output.
6//!
7//! Enable with `--features trace-bitstream` to see exactly what's being
8//! written to the bitstream, at what positions, and what it means.
9//!
10//! # Output Format
11//!
12//! Each write produces a line:
13//! ```text
14//! [bit_pos] SECTION.field: value (n_bits bits) = 0bXXXX
15//! ```
16//!
17//! Example:
18//! ```text
19//! [0] FILE_HEADER.signature: 0xff0a (16 bits) = 0b0000101011111111
20//! [16] FILE_HEADER.all_default: false (1 bit) = 0b0
21//! [17] FRAME_HEADER.all_default: false (1 bit) = 0b0
22//! ```
23//!
24//! # Usage
25//!
26//! Use the `trace_write!` macro instead of direct `writer.write()` calls:
27//!
28//! ```ignore
29//! use crate::trace::trace_write;
30//!
31//! // Instead of: writer.write(16, 0xff0a)?;
32//! trace_write!(writer, 16, 0xff0a, "FILE_HEADER.signature")?;
33//!
34//! // For boolean flags:
35//! trace_write!(writer, 1, 0, "FRAME_HEADER.all_default", "false")?;
36//!
37//! // For enum values:
38//! trace_write!(writer, 2, 0, "FRAME_HEADER.encoding", "VarDCT(0)")?;
39//! ```
40//!
41//! # Sections
42//!
43//! Use `trace_section!` to track hierarchical context:
44//!
45//! ```ignore
46//! trace_section!(begin "FRAME_HEADER");
47//! // ... writes ...
48//! trace_section!(end "FRAME_HEADER" @ writer);
49//! ```
50
51#[cfg(feature = "trace-bitstream")]
52use std::cell::RefCell;
53#[cfg(feature = "trace-bitstream")]
54use std::fs::File;
55#[cfg(feature = "trace-bitstream")]
56use std::io::Write;
57
58// Thread-local trace output file.
59#[cfg(feature = "trace-bitstream")]
60thread_local! {
61    static TRACE_OUTPUT: RefCell<Option<File>> = const { RefCell::new(None) };
62    static SECTION_STACK: RefCell<Vec<(&'static str, usize)>> = const { RefCell::new(Vec::new()) };
63}
64
65/// Initialize tracing to a file.
66///
67/// Call this at the start of encoding to capture trace output.
68#[cfg(feature = "trace-bitstream")]
69pub fn init_trace(path: &str) -> std::io::Result<()> {
70    let file = File::create(path)?;
71    TRACE_OUTPUT.with(|output| {
72        *output.borrow_mut() = Some(file);
73    });
74    Ok(())
75}
76
77/// Initialize tracing to stderr (default).
78#[cfg(feature = "trace-bitstream")]
79pub fn init_trace_stderr() {
80    // Default behavior - trace to stderr
81}
82
83/// Finalize tracing and flush output.
84#[cfg(feature = "trace-bitstream")]
85pub fn finish_trace() {
86    TRACE_OUTPUT.with(|output| {
87        if let Some(mut f) = output.borrow_mut().take() {
88            let _ = f.flush();
89        }
90    });
91}
92
93/// Write a trace line.
94#[cfg(feature = "trace-bitstream")]
95pub fn trace_line(line: &str) {
96    TRACE_OUTPUT.with(|output| {
97        if let Some(ref mut f) = *output.borrow_mut() {
98            let _ = writeln!(f, "{}", line);
99        } else {
100            eprintln!("{}", line);
101        }
102    });
103}
104
105/// Push a section onto the context stack.
106#[cfg(feature = "trace-bitstream")]
107pub fn push_section(name: &'static str, bit_pos: usize) {
108    SECTION_STACK.with(|stack| {
109        stack.borrow_mut().push((name, bit_pos));
110    });
111    trace_line(&format!("[{}] >>> BEGIN {}", bit_pos, name));
112}
113
114/// Pop a section from the context stack.
115#[cfg(feature = "trace-bitstream")]
116pub fn pop_section(name: &'static str, bit_pos: usize) {
117    SECTION_STACK.with(|stack| {
118        if let Some((popped, start_pos)) = stack.borrow_mut().pop() {
119            if popped != name {
120                trace_line(&format!(
121                    "[{}] !!! SECTION MISMATCH: expected {}, got {}",
122                    bit_pos, name, popped
123                ));
124            }
125            trace_line(&format!(
126                "[{}] <<< END {} ({} bits)",
127                bit_pos,
128                name,
129                bit_pos - start_pos
130            ));
131        }
132    });
133}
134
135/// Get current section prefix for field names.
136#[cfg(feature = "trace-bitstream")]
137pub fn section_prefix() -> String {
138    SECTION_STACK.with(|stack| {
139        stack
140            .borrow()
141            .iter()
142            .map(|(name, _)| *name)
143            .collect::<Vec<_>>()
144            .join(".")
145    })
146}
147
148/// Format bits as binary string.
149#[cfg(feature = "trace-bitstream")]
150pub fn format_bits(value: u64, n_bits: usize) -> String {
151    if n_bits == 0 {
152        return "0b(empty)".to_string();
153    }
154    let mut s = String::with_capacity(n_bits + 2);
155    s.push_str("0b");
156    for i in (0..n_bits).rev() {
157        if (value >> i) & 1 == 1 {
158            s.push('1');
159        } else {
160            s.push('0');
161        }
162    }
163    s
164}
165
166/// Core trace write function.
167#[cfg(feature = "trace-bitstream")]
168#[inline]
169pub fn trace_write_impl(
170    bit_pos_before: usize,
171    n_bits: usize,
172    value: u64,
173    field: &str,
174    description: Option<&str>,
175) {
176    let bits_str = format_bits(value, n_bits);
177    let desc = match description {
178        Some(d) => format!(" // {}", d),
179        None => String::new(),
180    };
181
182    let prefix = section_prefix();
183    let full_field = if prefix.is_empty() {
184        field.to_string()
185    } else {
186        format!("{}.{}", prefix, field)
187    };
188
189    trace_line(&format!(
190        "[{:6}] {}: {} ({} bits) = {}{}",
191        bit_pos_before, full_field, value, n_bits, bits_str, desc
192    ));
193}
194
195// ============================================================================
196// MACROS - These are the primary API
197// ============================================================================
198
199/// Trace and write bits to the bitstream.
200///
201/// Zero-cost when `trace-bitstream` feature is disabled.
202///
203/// # Syntax
204///
205/// ```ignore
206/// // Basic write with field name:
207/// trace_write!(writer, n_bits, value, "field_name")?;
208///
209/// // Write with description:
210/// trace_write!(writer, n_bits, value, "field_name", "description")?;
211/// ```
212#[macro_export]
213#[cfg(feature = "trace-bitstream")]
214macro_rules! trace_write {
215    ($writer:expr, $n_bits:expr, $value:expr, $field:expr) => {{
216        let pos = $writer.bits_written();
217        let result = $writer.write($n_bits, $value as u64);
218        $crate::trace::trace_write_impl(pos, $n_bits, $value as u64, $field, None);
219        result
220    }};
221    ($writer:expr, $n_bits:expr, $value:expr, $field:expr, $desc:expr) => {{
222        let pos = $writer.bits_written();
223        let result = $writer.write($n_bits, $value as u64);
224        $crate::trace::trace_write_impl(pos, $n_bits, $value as u64, $field, Some($desc));
225        result
226    }};
227}
228
229#[macro_export]
230#[cfg(not(feature = "trace-bitstream"))]
231macro_rules! trace_write {
232    ($writer:expr, $n_bits:expr, $value:expr, $field:expr) => {
233        $writer.write($n_bits, $value as u64)
234    };
235    ($writer:expr, $n_bits:expr, $value:expr, $field:expr, $desc:expr) => {
236        $writer.write($n_bits, $value as u64)
237    };
238}
239
240/// Trace section boundaries.
241///
242/// # Syntax
243///
244/// ```ignore
245/// trace_section!(begin "SECTION_NAME" @ writer);
246/// // ... writes ...
247/// trace_section!(end "SECTION_NAME" @ writer);
248/// ```
249#[macro_export]
250#[cfg(feature = "trace-bitstream")]
251macro_rules! trace_section {
252    (begin $name:expr, $writer:expr) => {
253        $crate::trace::push_section($name, $writer.bits_written())
254    };
255    (end $name:expr, $writer:expr) => {
256        $crate::trace::pop_section($name, $writer.bits_written())
257    };
258}
259
260#[macro_export]
261#[cfg(not(feature = "trace-bitstream"))]
262macro_rules! trace_section {
263    (begin $name:expr, $writer:expr) => {};
264    (end $name:expr, $writer:expr) => {};
265}
266
267/// Log a trace message without writing bits.
268///
269/// Useful for noting important state or decisions.
270#[macro_export]
271#[cfg(feature = "trace-bitstream")]
272macro_rules! trace_note {
273    ($writer:expr, $($arg:tt)*) => {
274        $crate::trace::trace_line(&format!("[{:6}] NOTE: {}", $writer.bits_written(), format!($($arg)*)))
275    };
276}
277
278#[macro_export]
279#[cfg(not(feature = "trace-bitstream"))]
280macro_rules! trace_note {
281    ($writer:expr, $($arg:tt)*) => {};
282}
283
284/// Trace a byte append operation.
285#[macro_export]
286#[cfg(feature = "trace-bitstream")]
287macro_rules! trace_bytes {
288    ($writer:expr, $bytes:expr, $field:expr) => {{
289        let pos = $writer.bits_written();
290        let data: &[u8] = $bytes;
291        let result = $writer.append_bytes(data);
292        $crate::trace::trace_line(&format!(
293            "[{:6}] {}: [{} bytes] {:02x?}",
294            pos,
295            $field,
296            data.len(),
297            &data[..data.len().min(32)]
298        ));
299        result
300    }};
301}
302
303#[macro_export]
304#[cfg(not(feature = "trace-bitstream"))]
305macro_rules! trace_bytes {
306    ($writer:expr, $bytes:expr, $field:expr) => {
307        $writer.append_bytes($bytes)
308    };
309}
310
311/// Debug print macro - only outputs when trace-bitstream feature is enabled.
312/// Use this instead of eprintln! for debug output in encoder code.
313#[macro_export]
314#[cfg(feature = "trace-bitstream")]
315macro_rules! debug_eprintln {
316    ($($arg:tt)*) => {
317        eprintln!($($arg)*)
318    };
319}
320
321#[macro_export]
322#[cfg(not(feature = "trace-bitstream"))]
323macro_rules! debug_eprintln {
324    ($($arg:tt)*) => {};
325}
326
327// Re-export macros at crate level
328pub use debug_eprintln;
329pub use trace_bytes;
330pub use trace_note;
331pub use trace_section;
332pub use trace_write;
333
334// ============================================================================
335// NO-OP STUBS when feature is disabled
336// ============================================================================
337
338#[cfg(not(feature = "trace-bitstream"))]
339pub fn init_trace(_path: &str) -> std::io::Result<()> {
340    Ok(())
341}
342
343#[cfg(not(feature = "trace-bitstream"))]
344pub fn init_trace_stderr() {}
345
346#[cfg(not(feature = "trace-bitstream"))]
347pub fn finish_trace() {}
348
349#[cfg(test)]
350mod tests {
351    #[cfg(feature = "trace-bitstream")]
352    use super::*;
353
354    #[test]
355    fn test_format_bits() {
356        #[cfg(feature = "trace-bitstream")]
357        {
358            assert_eq!(format_bits(0b1010, 4), "0b1010");
359            assert_eq!(format_bits(0b1, 1), "0b1");
360            assert_eq!(format_bits(0b0, 1), "0b0");
361            assert_eq!(format_bits(0xff0a, 16), "0b1111111100001010");
362        }
363    }
364}