···1414 - Add a plan/Implementation TODOs section under the relevant spec (e.g., audio FFT/VQT) or create a new spec.
1515 - Reference new/updated plans from AGENTS.md and the docs index.
1616- Keep hygiene visible: mention clippy/test status with each change.
1717+- ALWAYS format code with `cargo fmt` after changes, in addition to fixing all compiler/clippy warnings and errors.
17181819**Context**
1920- **Rewrite code location:** All Rust rewrite code lives under `tic80_rust/` (crate root). Tests live in `tic80_rust/tests/`. The windowed demo binary is `tic80_rust/src/main.rs`.
···5051- Memory ops (`peek/poke` 1/2/4/8‑bit, `memcpy`, `memset`) implemented; VRAM updates reflect on screen.
5152- CLI loads bundled default cart or a provided `.lua` path.
5253- Audio capture foundation: `cpal` input stream with mono downmix into a lock‑free ring buffer (8192 samples); CLI flags `--list-audio`, `--audio-device`, `--audio-vu`, `--audio-disable`; simple VU feedback prints peak dBFS once per second.
5454+ - FFT foundation: 2k R2C (`realfft`) updates on the tick thread; maintains raw/smoothed/normalized buffers with peak tracking; `--debug-fft` prints first bins; Lua APIs pending.
53555456**Near-Term Backlog**
5557- FFT implementation (cpal + realfft) per `docs/specs/audio_fft_vqt.md` (see Implementation TODOs section); add headless tests and Lua `fft/ffts/fftr/fftrs`.
···9597 - Implemented audio capture with `cpal`: device selection/listing, mono downmix, ring buffer.
9698 - Added CLI: `--list-audio`, `--audio-device`, `--audio-vu`, `--audio-disable`.
9799 - Integrated a 1s VU peak readout for manual verification.
100100+ - Implemented 2k FFT analysis (realfft) with normalized/smoothed buffers and debug print flag.
98101 - Kept clippy/tests green; documented the FFT/VQT plan and linked TODOs.
99102100103**Docs Index**
+3
docs/README.md
···3838- Load a `.lua` file:
3939 - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/alt.lua`
4040 - In crate dir: `cargo run -- assets/alt.lua`
4141+- Audio FFT test cart:
4242+ - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/fft_test.lua --audio-device "<name-substr>"`
4343+ - Add `--audio-vu` to print a 1s peak; add `--debug-fft` to print the first few bins.
+1-1
docs/specs/audio_fft_vqt.md
···87878888Status
8989- Phase 1 implemented: `cpal` capture + mono downmix + ring buffer + CLI flags + VU feedback.
9090-- Next: Phase 2 (FFT 2k, realfft R2C, Lua APIs, tests).
9090+- Phase 2 implemented: FFT 2k using `realfft` (tick‑thread), raw/smoothed/normalized buffers and peak tracking; optional `--debug-fft`. Lua wiring and tests next.
91919292Phase 1: Audio (cpal)
9393- Device listing: Add `--list-audio` to print capture devices and default.
+9
docs/specs/implementation_status.md
···2727- `.lua` loader: First CLI arg as a `.lua` path runs external script; fallback to bundled `assets/default.lua`.
2828- Window title: “rustic”.
2929- Audio capture scaffolding: `cpal` input stream (44.1 kHz if supported), stereo→mono downmix, lock‑free ring buffer (8192 samples); CLI flags to list/select devices and optional VU meter output.
3030+ - VU behavior: Peak meter observed ~-180 dBFS at silence (BlackHole 2ch on macOS), responsive under Multi‑Output device routing.
3131+3232+Implemented (Analysis)
3333+- FFT (2k):
3434+ - Real‑to‑complex transform using `realfft` over the latest 2048 samples on the tick thread.
3535+ - Bins 0..1023 maintained (Nyquist dropped), magnitudes scaled by 2.0 to match C behavior.
3636+ - Buffers: raw, raw‑smoothed (0.6), normalized, normalized‑smoothed, with peak tracking (`fPeakMin=0.01`, `fPeakSmooth=0.995`).
3737+ - Optional `--debug-fft` prints the first 16 smoothed normalized bins periodically.
3838+ - Lua APIs pending wiring (`fft/ffts/fftr/fftrs`).
30393140Behavioral Notes
3241- Triangles: top-left inclusion; CCW orientation enforced internally; half-open bounding box prevents shared-edge double draws.
···11+-- Simple FFT visualization test cart
22+-- Usage:
33+-- cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/fft_test.lua --audio-device "..."
44+55+function BOOT()
66+ cls(0)
77+end
88+99+function TIC()
1010+ cls(0)
1111+ local bars = 32
1212+ local sw, sh = 240, 136
1313+ local bw = math.floor(sw / bars)
1414+ for i=0,bars-1 do
1515+ local v = fft(i, -1) -- normalized bin value
1616+ if v < 0 then v = 0 end
1717+ if v > 1 then v = 1 end
1818+ local h = math.floor(v * (sh-10))
1919+ local x = i * bw
2020+ local y = sh - h
2121+ rect(x, y, bw-1, h, 6)
2222+ end
2323+ print("fft test (normalized)", 2, 2, 14, false, 1, false)
2424+end
2525+
···11+use parking_lot::RwLock;
22+use realfft::{num_complex::Complex, RealFftPlanner, RealToComplex};
33+use std::sync::{Arc, OnceLock};
44+55+pub struct FFTState {
66+ // Analysis window
77+ n: usize, // 2048
88+ half: usize, // 1024
99+ // Rolling mono buffer (last samples), sized to at least n (we use 8192 for future VQT)
1010+ buf: Vec<f32>,
1111+ write_idx: usize,
1212+ filled: bool,
1313+1414+ // FFT planning and work buffers
1515+ r2c: std::sync::Arc<dyn RealToComplex<f32>>,
1616+ scratch: Vec<Complex<f32>>,
1717+ input: Vec<f32>,
1818+ spectrum: Vec<Complex<f32>>, // length half+1
1919+2020+ // Output buffers (length half)
2121+ pub fft_raw: Vec<f32>, // magnitudes (2.0 * |X[k]|)
2222+ pub fft_raw_sm: Vec<f32>, // smoothed raw (factor 0.6)
2323+ pub fft_data: Vec<f32>, // normalized magnitudes
2424+ pub fft_sm: Vec<f32>, // smoothed normalized
2525+2626+ // Peak tracking for normalization
2727+ f_peak_min: f32, // 0.01
2828+ f_peak_smoothing: f32, // 0.995
2929+ f_peak_value: f32,
3030+ f_amplification: f32,
3131+3232+ // Smoothing factor for displayed series
3333+ f_smooth_factor: f32, // 0.6
3434+}
3535+3636+impl FFTState {
3737+ pub fn new(rolling_capacity: usize) -> Self {
3838+ let n = 2048usize;
3939+ let half = n / 2;
4040+ let mut planner = RealFftPlanner::<f32>::new();
4141+ let r2c = planner.plan_fft_forward(n);
4242+ let input = r2c.make_input_vec();
4343+ let spectrum = r2c.make_output_vec();
4444+ let scratch = r2c.make_scratch_vec();
4545+ Self {
4646+ n,
4747+ half,
4848+ buf: vec![0.0; rolling_capacity.max(n)],
4949+ write_idx: 0,
5050+ filled: false,
5151+ r2c,
5252+ scratch,
5353+ input,
5454+ spectrum,
5555+ fft_raw: vec![0.0; half],
5656+ fft_raw_sm: vec![0.0; half],
5757+ fft_data: vec![0.0; half],
5858+ fft_sm: vec![0.0; half],
5959+ f_peak_min: 0.01,
6060+ f_peak_smoothing: 0.995,
6161+ f_peak_value: 0.01,
6262+ f_amplification: 1.0,
6363+ f_smooth_factor: 0.6,
6464+ }
6565+ }
6666+6767+ pub fn ingest(&mut self, sample: f32) {
6868+ self.buf[self.write_idx] = sample;
6969+ self.write_idx += 1;
7070+ if self.write_idx >= self.buf.len() {
7171+ self.write_idx = 0;
7272+ self.filled = true;
7373+ }
7474+ }
7575+7676+ fn copy_latest_window(&mut self) {
7777+ let n = self.n;
7878+ let len = self.buf.len();
7979+ // Copy last n samples ending at write_idx (exclusive)
8080+ let end = self.write_idx;
8181+ let start = (len + end).saturating_sub(n) % len;
8282+ if start + n <= len {
8383+ self.input[..n].copy_from_slice(&self.buf[start..start + n]);
8484+ } else {
8585+ let first = len - start;
8686+ self.input[..first].copy_from_slice(&self.buf[start..]);
8787+ self.input[first..n].copy_from_slice(&self.buf[..(n - first)]);
8888+ }
8989+ }
9090+9191+ pub fn update(&mut self) {
9292+ // Ensure we have at least one full window written once
9393+ if !self.filled && self.write_idx < self.n {
9494+ // Not enough data yet; keep buffers near zero
9595+ return;
9696+ }
9797+ self.copy_latest_window();
9898+ // Forward R2C
9999+ self.r2c
100100+ .process_with_scratch(&mut self.input, &mut self.spectrum, &mut self.scratch)
101101+ .ok();
102102+103103+ // Magnitudes for 0..half-1 (drop Nyquist at index half)
104104+ let mut peak_raw = self.f_peak_min;
105105+ for k in 0..self.half {
106106+ let c = self.spectrum[k];
107107+ let mag = (c.re * c.re + c.im * c.im).sqrt() * 2.0;
108108+ self.fft_raw[k] = mag;
109109+ if mag > peak_raw {
110110+ peak_raw = mag;
111111+ }
112112+ }
113113+114114+ // Update peak smoothing and amplification
115115+ if peak_raw > self.f_peak_value {
116116+ self.f_peak_value = peak_raw;
117117+ } else {
118118+ self.f_peak_value = self.f_peak_value * self.f_peak_smoothing
119119+ + peak_raw * (1.0 - self.f_peak_smoothing);
120120+ }
121121+ if self.f_peak_value < self.f_peak_min {
122122+ self.f_peak_value = self.f_peak_min;
123123+ }
124124+ self.f_amplification = 1.0 / self.f_peak_value;
125125+126126+ // Smoothed raw and normalized series
127127+ let a = self.f_smooth_factor; // 0.6
128128+ for k in 0..self.half {
129129+ let raw = self.fft_raw[k];
130130+ let raw_sm = self.fft_raw_sm[k] * a + raw * (1.0 - a);
131131+ self.fft_raw_sm[k] = raw_sm;
132132+133133+ let norm = raw * self.f_amplification;
134134+ let norm_sm = self.fft_sm[k] * a + norm * (1.0 - a);
135135+ self.fft_data[k] = norm;
136136+ self.fft_sm[k] = norm_sm;
137137+ }
138138+ }
139139+140140+ pub fn bins(&self) -> usize {
141141+ self.half
142142+ }
143143+}
144144+145145+// Global shared FFT state for Lua API access.
146146+static FFT_SHARED: OnceLock<Arc<RwLock<FFTState>>> = OnceLock::new();
147147+148148+pub fn set_global_fft(state: Arc<RwLock<FFTState>>) {
149149+ let _ = FFT_SHARED.set(state);
150150+}
151151+152152+pub fn get_global_fft() -> Option<&'static Arc<RwLock<FFTState>>> {
153153+ FFT_SHARED.get()
154154+}
155155+156156+// Helper function to query FFT arrays with C-like clamping semantics.
157157+// smoothing=false => normalized (fft_data) or raw (fft_raw);
158158+// smoothing=true => normalized-smoothed (fft_sm) or raw-smoothed (fft_raw_sm).
159159+pub fn query_fft(state: &FFTState, start: i32, end: i32, smoothing: bool, raw: bool) -> f64 {
160160+ let size = state.half as i32; // 1024
161161+ if end == -1 {
162162+ if start < 0 || start >= size {
163163+ return 0.0;
164164+ }
165165+ let idx = start as usize;
166166+ let v = if raw {
167167+ if smoothing {
168168+ state.fft_raw_sm[idx]
169169+ } else {
170170+ state.fft_raw[idx]
171171+ }
172172+ } else if smoothing {
173173+ state.fft_sm[idx]
174174+ } else {
175175+ state.fft_data[idx]
176176+ };
177177+ return v as f64;
178178+ }
179179+ // both out-of-bounds on same side => 0
180180+ if (start < 0 && end < 0) || (start >= size && end >= size) {
181181+ return 0.0;
182182+ }
183183+ let mut s = start;
184184+ let mut e = end;
185185+ if s < 0 {
186186+ s = 0;
187187+ }
188188+ if s >= size {
189189+ s = 0;
190190+ }
191191+ if e >= size {
192192+ e = size - 1;
193193+ }
194194+ if s > e {
195195+ e = s;
196196+ }
197197+ let mut sum = 0.0f64;
198198+ for i in s..=e {
199199+ let u = i as usize;
200200+ let v = if raw {
201201+ if smoothing {
202202+ state.fft_raw_sm[u]
203203+ } else {
204204+ state.fft_raw[u]
205205+ }
206206+ } else if smoothing {
207207+ state.fft_sm[u]
208208+ } else {
209209+ state.fft_data[u]
210210+ } as f64;
211211+ sum += v;
212212+ }
213213+ sum
214214+}
+20-6
tic80_rust/src/core/memory.rs
···3131 let mut get_px = |pi: usize| -> u8 {
3232 if pi < w * h {
3333 fb.pix((pi % w) as i32, (pi / w) as i32, None).unwrap_or(0) & 0x0F
3434- } else { 0 }
3434+ } else {
3535+ 0
3636+ }
3537 };
3638 let lo = get_px(p);
3739 let hi = get_px(p + 1);
···6971 }
7072 }
71737272- pub fn peek(&self, addr: usize) -> u8 { self.get_byte(addr) }
7373- pub fn poke(&mut self, addr: usize, val: u8) { self.set_byte(addr, val); }
7474+ pub fn peek(&self, addr: usize) -> u8 {
7575+ self.get_byte(addr)
7676+ }
7777+ pub fn poke(&mut self, addr: usize, val: u8) {
7878+ self.set_byte(addr, val);
7979+ }
74807581 // bit-packed peeks/pokes across entire 96KB (VRAM included)
7682 pub fn peek_bits(&self, addr: usize, bits: u8) -> u8 {
···7884 8 => self.peek(addr),
7985 4 => {
8086 let byte = self.peek(addr >> 1);
8181- if (addr & 1) == 0 { byte & 0x0F } else { (byte >> 4) & 0x0F }
8787+ if (addr & 1) == 0 {
8888+ byte & 0x0F
8989+ } else {
9090+ (byte >> 4) & 0x0F
9191+ }
8292 }
8393 2 => {
8494 let byte = self.peek(addr >> 2);
···125135 }
126136127137 pub fn memcpy(&mut self, dst: usize, src: usize, size: usize) {
128128- if size == 0 { return; }
138138+ if size == 0 {
139139+ return;
140140+ }
129141 // Handle overlap with correct direction
130142 if src < dst && src + size > dst {
131143 for i in (0..size).rev() {
···141153 }
142154143155 pub fn memset(&mut self, dst: usize, value: u8, size: usize) {
144144- for i in 0..size { self.set_byte(dst + i, value); }
156156+ for i in 0..size {
157157+ self.set_byte(dst + i, value);
158158+ }
145159 }
146160}
+67-25
tic80_rust/src/gfx/framebuffer.rs
···143143144144 // Blit to RGBA buffer for pixels
145145 pub fn blit_to_rgba(&self, rgba: &mut [u8]) {
146146- for (px, idx) in rgba
147147- .chunks_exact_mut(4)
148148- .zip(self.idx.iter().copied())
149149- {
146146+ for (px, idx) in rgba.chunks_exact_mut(4).zip(self.idx.iter().copied()) {
150147 let pal = &PALETTE[(idx & 0x0F) as usize];
151148 px.copy_from_slice(pal);
152149 }
···225222226223 // Circle border using 8-way symmetry (integer midpoint algorithm)
227224 pub fn circb(&mut self, cx: i32, cy: i32, r: i32, color: u8) {
228228- if r < 0 { return; }
225225+ if r < 0 {
226226+ return;
227227+ }
229228 let c = color & 0x0F;
230229 if r == 0 {
231230 let _ = self.set_pixel(cx, cy, c);
···257256258257 // Filled circle via horizontal spans using symmetry
259258 pub fn circ(&mut self, cx: i32, cy: i32, r: i32, color: u8) {
260260- if r < 0 { return; }
259259+ if r < 0 {
260260+ return;
261261+ }
261262 let c = color & 0x0F;
262263 if r == 0 {
263264 let _ = self.set_pixel(cx, cy, c);
···284285 }
285286286287 fn hspan(&mut self, x0: i32, x1: i32, y: i32, color: u8) {
287287- if y < 0 || y as u32 >= HEIGHT { return; }
288288+ if y < 0 || y as u32 >= HEIGHT {
289289+ return;
290290+ }
288291 let start = x0.min(x1);
289292 let end = x0.max(x1);
290293 for x in start..=end {
···294297295298 // Ellipse border using midpoint algorithm
296299 pub fn ellib(&mut self, cx: i32, cy: i32, a: i32, b: i32, color: u8) {
297297- if a < 0 || b < 0 { return; }
300300+ if a < 0 || b < 0 {
301301+ return;
302302+ }
298303 let c = color & 0x0F;
299299- if a == 0 && b == 0 { let _ = self.set_pixel(cx, cy, c); return; }
304304+ if a == 0 && b == 0 {
305305+ let _ = self.set_pixel(cx, cy, c);
306306+ return;
307307+ }
300308301309 let a2 = (a as i64) * (a as i64);
302310 let b2 = (b as i64) * (b as i64);
···305313 let mut y: i64 = b as i64;
306314 let mut d = b2 - a2 * (b as i64) + a2 / 4;
307315 while b2 * x <= a2 * y {
308308- let xx = x as i32; let yy = y as i32;
316316+ let xx = x as i32;
317317+ let yy = y as i32;
309318 let _ = self.set_pixel(cx + xx, cy + yy, c);
310319 let _ = self.set_pixel(cx - xx, cy + yy, c);
311320 let _ = self.set_pixel(cx + xx, cy - yy, c);
···319328 x += 1;
320329 }
321330322322- x = a as i64; y = 0; d = a2 - b2 * (a as i64) + b2 / 4;
331331+ x = a as i64;
332332+ y = 0;
333333+ d = a2 - b2 * (a as i64) + b2 / 4;
323334 while a2 * y <= b2 * x {
324324- let xx = x as i32; let yy = y as i32;
335335+ let xx = x as i32;
336336+ let yy = y as i32;
325337 let _ = self.set_pixel(cx + xx, cy + yy, c);
326338 let _ = self.set_pixel(cx - xx, cy + yy, c);
327339 let _ = self.set_pixel(cx + xx, cy - yy, c);
···338350339351 // Filled ellipse using horizontal spans
340352 pub fn elli(&mut self, cx: i32, cy: i32, a: i32, b: i32, color: u8) {
341341- if a < 0 || b < 0 { return; }
353353+ if a < 0 || b < 0 {
354354+ return;
355355+ }
342356 let c = color & 0x0F;
343343- if a == 0 && b == 0 { let _ = self.set_pixel(cx, cy, c); return; }
344344- if a == 0 { for yy in (cy - b)..=(cy + b) { let _ = self.set_pixel(cx, yy, c); } return; }
345345- if b == 0 { for xx in (cx - a)..=(cx + a) { let _ = self.set_pixel(xx, cy, c); } return; }
346346- let af = a as f32; let bf = b as f32; let bf2 = bf * bf;
357357+ if a == 0 && b == 0 {
358358+ let _ = self.set_pixel(cx, cy, c);
359359+ return;
360360+ }
361361+ if a == 0 {
362362+ for yy in (cy - b)..=(cy + b) {
363363+ let _ = self.set_pixel(cx, yy, c);
364364+ }
365365+ return;
366366+ }
367367+ if b == 0 {
368368+ for xx in (cx - a)..=(cx + a) {
369369+ let _ = self.set_pixel(xx, cy, c);
370370+ }
371371+ return;
372372+ }
373373+ let af = a as f32;
374374+ let bf = b as f32;
375375+ let bf2 = bf * bf;
347376 for dy in -b..=b {
348377 let yf = dy as f32;
349378 let t = 1.0 - (yf * yf) / bf2;
350350- if t < 0.0 { continue; }
379379+ if t < 0.0 {
380380+ continue;
381381+ }
351382 let xf = af * t.sqrt();
352383 let x = xf.floor() as i32;
353384 self.hspan(cx - x, cx + x, cy + dy, c);
···387418 let (cx2, cy2) = ((v2x as i64) * 2, (v2y as i64) * 2);
388419389420 // Edge deltas
390390- let e0_dx = bx2 - ax2; let e0_dy = by2 - ay2; // v0->v1
391391- let e1_dx = cx2 - bx2; let e1_dy = cy2 - by2; // v1->v2
392392- let e2_dx = ax2 - cx2; let e2_dy = ay2 - cy2; // v2->v0
421421+ let e0_dx = bx2 - ax2;
422422+ let e0_dy = by2 - ay2; // v0->v1
423423+ let e1_dx = cx2 - bx2;
424424+ let e1_dy = cy2 - by2; // v1->v2
425425+ let e2_dx = ax2 - cx2;
426426+ let e2_dy = ay2 - cy2; // v2->v0
393427394428 // Top-left classification
395429 let e0_top_left = e0_dy > 0 || (e0_dy == 0 && e0_dx < 0);
···468502 if mask != 0 {
469503 // find first 1 from the left (LSB)
470504 let mut l = 0;
471471- while l < GLYPH_W && ((mask >> l) & 1) == 0 { l += 1; }
505505+ while l < GLYPH_W && ((mask >> l) & 1) == 0 {
506506+ l += 1;
507507+ }
472508 // find last 1 from the left (rightmost set bit + 1)
473509 let mut r = GLYPH_W;
474474- while r > 0 && (((mask >> (r - 1)) & 1) == 0) { r -= 1; }
475475- if l < left { left = l; }
476476- if r > right { right = r; }
510510+ while r > 0 && (((mask >> (r - 1)) & 1) == 0) {
511511+ r -= 1;
512512+ }
513513+ if l < left {
514514+ left = l;
515515+ }
516516+ if r > right {
517517+ right = r;
518518+ }
477519 }
478520 }
479521 let width = right.saturating_sub(left);
+1
tic80_rust/src/lib.rs
···12121313pub mod audio {
1414 pub mod capture;
1515+ pub mod fft;
1516}
+65-10
tic80_rust/src/main.rs
···1010use winit::event_loop::{ControlFlow, EventLoop};
1111use winit::window::WindowBuilder;
12121313+use parking_lot::RwLock;
1414+use std::sync::Arc;
1515+use tic80_rust::audio::capture as audio_cap;
1616+use tic80_rust::audio::fft::{set_global_fft, FFTState};
1717+use tic80_rust::core::memory::Memory;
1318use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer};
1419use tic80_rust::script::lua_runner::LuaRunner;
1515-use tic80_rust::core::memory::Memory;
1616-use tic80_rust::audio::capture as audio_cap;
17201821// Simple fixed-step ticker at ~60 FPS
1922struct Ticker {
···7376 "--audio-disable" => audio_disable = true,
7477 "--audio-vu" => audio_vu = true,
7578 "--audio-device" => {
7676- if let Some(val) = args_iter.next() { audio_device = Some(val); }
7979+ if let Some(val) = args_iter.next() {
8080+ audio_device = Some(val);
8181+ }
7782 }
7883 other => {
7984 if other.ends_with(".lua") && Path::new(other).is_file() {
···98103 match fs::read_to_string(path) {
99104 Ok(s) => s,
100105 Err(e) => {
101101- eprintln!("Failed to read {}: {}. Falling back to default cart.", path, e);
106106+ eprintln!(
107107+ "Failed to read {}: {}. Falling back to default cart.",
108108+ path, e
109109+ );
102110 DEFAULT_LUA.to_string()
103111 }
104112 }
···114122 vu_enabled: bool,
115123 last_print: Instant,
116124 peak_acc: f32,
125125+ fft: Arc<RwLock<FFTState>>,
126126+ debug_fft: bool,
117127 }
118128 let mut audio_state: Option<AudioState> = None;
129129+ // Optional debug flag for FFT bins
130130+ let debug_fft = std::env::args().any(|a| a == "--debug-fft");
131131+119132 if !audio_disable {
120133 let cap_cfg = audio_cap::AudioCaptureConfig {
121134 device_substr: audio_device.clone(),
···124137 };
125138 match audio_cap::start_capture(cap_cfg) {
126139 Ok((handle, cons)) => {
127127- println!("Audio capture: '{}' @ {} Hz, {} ch", handle.info.device_name, handle.info.sample_rate, handle.info.channels);
128128- if audio_vu { println!("Audio VU: enabled (prints every ~1s)"); }
129129- audio_state = Some(AudioState { _handle: handle, cons, vu_enabled: audio_vu, last_print: Instant::now(), peak_acc: 0.0 });
140140+ println!(
141141+ "Audio capture: '{}' @ {} Hz, {} ch",
142142+ handle.info.device_name, handle.info.sample_rate, handle.info.channels
143143+ );
144144+ if audio_vu {
145145+ println!("Audio VU: enabled (prints every ~1s)");
146146+ }
147147+ let fft_arc: Arc<RwLock<FFTState>> = Arc::new(RwLock::new(FFTState::new(
148148+ audio_cap::default_ring_capacity(),
149149+ )));
150150+ set_global_fft(fft_arc.clone());
151151+ audio_state = Some(AudioState {
152152+ _handle: handle,
153153+ cons,
154154+ vu_enabled: audio_vu,
155155+ last_print: Instant::now(),
156156+ peak_acc: 0.0,
157157+ fft: fft_arc,
158158+ debug_fft,
159159+ });
130160 }
131161 Err(e) => {
132132- eprintln!("Audio capture disabled ({}). Use --audio-disable to silence this.", e);
162162+ eprintln!(
163163+ "Audio capture disabled ({}). Use --audio-disable to silence this.",
164164+ e
165165+ );
133166 }
134167 }
135168 }
···160193 }
161194 // Simple VU meter from audio ring
162195 if let Some(a) = audio_state.as_mut() {
163163- // Drain available samples and track peak
164164- while let Ok(s) = a.cons.pop() { a.peak_acc = a.peak_acc.max(s.abs()); }
196196+ // Drain available samples, feed analyzer, track peak
197197+ while let Ok(s) = a.cons.pop() {
198198+ a.peak_acc = a.peak_acc.max(s.abs());
199199+ if let Some(mut w) = a.fft.try_write() {
200200+ w.ingest(s);
201201+ }
202202+ }
203203+ if let Some(mut w) = a.fft.try_write() {
204204+ w.update();
205205+ }
206206+ if a.debug_fft {
207207+ // Print a small subset of normalized bins
208208+ let bins = {
209209+ let r = a.fft.read();
210210+ r.bins().min(16)
211211+ };
212212+ let mut line = String::from("FFT[0..16]: ");
213213+ if let Some(r) = a.fft.try_read() {
214214+ for i in 0..bins {
215215+ line.push_str(&format!("{:.2} ", r.fft_sm[i]));
216216+ }
217217+ }
218218+ println!("{}", line);
219219+ }
165220 if a.vu_enabled && a.last_print.elapsed() >= Duration::from_millis(1000) {
166221 let peak = a.peak_acc.max(1e-9);
167222 let db = 20.0 * peak.log10();
+199-50
tic80_rust/src/script/lua_runner.rs
···3344use mlua::{Function, Lua, MultiValue, RegistryKey, Result as LuaResult, Value};
5566+use crate::audio::fft::{get_global_fft, query_fft};
77+use crate::core::memory::Memory;
68use crate::gfx::framebuffer::Framebuffer;
77-use crate::core::memory::Memory;
89910pub struct LuaRunner {
1011 lua: Lua,
···1213}
13141415impl LuaRunner {
1515- pub fn new(fb: Rc<RefCell<Framebuffer>>, mem: Rc<RefCell<Memory>>, script_src: &str) -> LuaResult<Self> {
1616+ pub fn new(
1717+ fb: Rc<RefCell<Framebuffer>>,
1818+ mem: Rc<RefCell<Memory>>,
1919+ script_src: &str,
2020+ ) -> LuaResult<Self> {
1621 let lua = Lua::new();
1722 let tic_key = {
1823 let globals = lua.globals();
···56615762 // rectb(x,y,w,h,color)
5863 let fb_rectb = fb.clone();
5959- let rectb_fn = lua.create_function(
6060- move |_, (x, y, w, h, color): (i32, i32, i32, i32, u8)| {
6464+ let rectb_fn =
6565+ lua.create_function(move |_, (x, y, w, h, color): (i32, i32, i32, i32, u8)| {
6166 fb_rectb.borrow_mut().rectb(x, y, w, h, color);
6267 Ok(())
6363- },
6464- )?;
6868+ })?;
6569 globals.set("rectb", rectb_fn)?;
66706771 // circ(x, y, r, color)
6872 let fb_circ = fb.clone();
6969- let circ_fn = lua.create_function(move |_, (x, y, r, color): (i32, i32, i32, u8)| {
7070- fb_circ.borrow_mut().circ(x, y, r, color);
7171- Ok(())
7272- })?;
7373+ let circ_fn =
7474+ lua.create_function(move |_, (x, y, r, color): (i32, i32, i32, u8)| {
7575+ fb_circ.borrow_mut().circ(x, y, r, color);
7676+ Ok(())
7777+ })?;
7378 globals.set("circ", circ_fn)?;
74797580 // circb(x, y, r, color)
7681 let fb_circb = fb.clone();
7777- let circb_fn = lua.create_function(move |_, (x, y, r, color): (i32, i32, i32, u8)| {
7878- fb_circb.borrow_mut().circb(x, y, r, color);
7979- Ok(())
8080- })?;
8282+ let circb_fn =
8383+ lua.create_function(move |_, (x, y, r, color): (i32, i32, i32, u8)| {
8484+ fb_circb.borrow_mut().circb(x, y, r, color);
8585+ Ok(())
8686+ })?;
8187 globals.set("circb", circb_fn)?;
82888389 // clip(x,y,w,h) or clip() to reset
···8692 if args.is_empty() {
8793 fb_clip.borrow_mut().clip_reset();
8894 } else {
8989- let x = match args.get(0) { Some(Value::Integer(n)) => *n as i32, _ => 0 };
9090- let y = match args.get(1) { Some(Value::Integer(n)) => *n as i32, _ => 0 };
9191- let w = match args.get(2) { Some(Value::Integer(n)) => *n as i32, _ => 0 };
9292- let h = match args.get(3) { Some(Value::Integer(n)) => *n as i32, _ => 0 };
9595+ let x = match args.get(0) {
9696+ Some(Value::Integer(n)) => *n as i32,
9797+ _ => 0,
9898+ };
9999+ let y = match args.get(1) {
100100+ Some(Value::Integer(n)) => *n as i32,
101101+ _ => 0,
102102+ };
103103+ let w = match args.get(2) {
104104+ Some(Value::Integer(n)) => *n as i32,
105105+ _ => 0,
106106+ };
107107+ let h = match args.get(3) {
108108+ Some(Value::Integer(n)) => *n as i32,
109109+ _ => 0,
110110+ };
93111 fb_clip.borrow_mut().clip(x, y, w, h);
94112 }
95113 Ok(())
···141159 })?;
142160 globals.set("print", print_fn)?;
143161162162+ // FFT APIs: fft/ffts/fftr/fftrs
163163+ fn parse_fft_args(args: &MultiValue) -> (i32, i32) {
164164+ let start = match args.get(0) {
165165+ Some(Value::Integer(n)) => *n as i32,
166166+ _ => -1,
167167+ };
168168+ let end = match args.get(1) {
169169+ Some(Value::Integer(n)) => *n as i32,
170170+ _ => -1,
171171+ };
172172+ (start, end)
173173+ }
174174+175175+ let fft_fn = lua.create_function(move |_, args: MultiValue| {
176176+ let (start, end) = parse_fft_args(&args);
177177+ let val = if let Some(arc) = get_global_fft() {
178178+ let guard = arc.read();
179179+ query_fft(&guard, start, end, false, false)
180180+ } else {
181181+ 0.0
182182+ };
183183+ Ok(val)
184184+ })?;
185185+ globals.set("fft", fft_fn)?;
186186+187187+ let ffts_fn = lua.create_function(move |_, args: MultiValue| {
188188+ let (start, end) = parse_fft_args(&args);
189189+ let val = if let Some(arc) = get_global_fft() {
190190+ let guard = arc.read();
191191+ query_fft(&guard, start, end, true, false)
192192+ } else {
193193+ 0.0
194194+ };
195195+ Ok(val)
196196+ })?;
197197+ globals.set("ffts", ffts_fn)?;
198198+199199+ let fftr_fn = lua.create_function(move |_, args: MultiValue| {
200200+ let (start, end) = parse_fft_args(&args);
201201+ let val = if let Some(arc) = get_global_fft() {
202202+ let guard = arc.read();
203203+ query_fft(&guard, start, end, false, true)
204204+ } else {
205205+ 0.0
206206+ };
207207+ Ok(val)
208208+ })?;
209209+ globals.set("fftr", fftr_fn)?;
210210+211211+ let fftrs_fn = lua.create_function(move |_, args: MultiValue| {
212212+ let (start, end) = parse_fft_args(&args);
213213+ let val = if let Some(arc) = get_global_fft() {
214214+ let guard = arc.read();
215215+ query_fft(&guard, start, end, true, true)
216216+ } else {
217217+ 0.0
218218+ };
219219+ Ok(val)
220220+ })?;
221221+ globals.set("fftrs", fftrs_fn)?;
222222+144223 // memory: peek/poke + bit variants + memcpy/memset
145224 let mem_peek = mem.clone();
146225 let peek_fn = lua.create_function(move |_, (addr, bits): (u32, Option<u8>)| {
147226 let a = addr as usize;
148227 let b = bits.unwrap_or(8);
149149- let v = if b == 8 { mem_peek.borrow().peek(a) } else { mem_peek.borrow().peek_bits(a, b) };
228228+ let v = if b == 8 {
229229+ mem_peek.borrow().peek(a)
230230+ } else {
231231+ mem_peek.borrow().peek_bits(a, b)
232232+ };
150233 Ok(v as u32)
151234 })?;
152235 globals.set("peek", peek_fn)?;
153236154237 let mem_poke = mem.clone();
155155- let poke_fn = lua.create_function(move |_, (addr, val, bits): (u32, u32, Option<u8>)| {
156156- let a = addr as usize;
157157- let v = val as u8;
158158- let b = bits.unwrap_or(8);
159159- if b == 8 { mem_poke.borrow_mut().poke(a, v); } else { mem_poke.borrow_mut().poke_bits(a, b, v); }
160160- Ok(())
161161- })?;
238238+ let poke_fn =
239239+ lua.create_function(move |_, (addr, val, bits): (u32, u32, Option<u8>)| {
240240+ let a = addr as usize;
241241+ let v = val as u8;
242242+ let b = bits.unwrap_or(8);
243243+ if b == 8 {
244244+ mem_poke.borrow_mut().poke(a, v);
245245+ } else {
246246+ mem_poke.borrow_mut().poke_bits(a, b, v);
247247+ }
248248+ Ok(())
249249+ })?;
162250 globals.set("poke", poke_fn)?;
163251164252 let mem_peek1 = mem.clone();
165165- globals.set("peek1", lua.create_function(move |_, addr: u32| Ok(mem_peek1.borrow().peek_bits(addr as usize, 1) as u32))?)?;
253253+ globals.set(
254254+ "peek1",
255255+ lua.create_function(move |_, addr: u32| {
256256+ Ok(mem_peek1.borrow().peek_bits(addr as usize, 1) as u32)
257257+ })?,
258258+ )?;
166259 let mem_peek2 = mem.clone();
167167- globals.set("peek2", lua.create_function(move |_, addr: u32| Ok(mem_peek2.borrow().peek_bits(addr as usize, 2) as u32))?)?;
260260+ globals.set(
261261+ "peek2",
262262+ lua.create_function(move |_, addr: u32| {
263263+ Ok(mem_peek2.borrow().peek_bits(addr as usize, 2) as u32)
264264+ })?,
265265+ )?;
168266 let mem_peek4 = mem.clone();
169169- globals.set("peek4", lua.create_function(move |_, addr: u32| Ok(mem_peek4.borrow().peek_bits(addr as usize, 4) as u32))?)?;
267267+ globals.set(
268268+ "peek4",
269269+ lua.create_function(move |_, addr: u32| {
270270+ Ok(mem_peek4.borrow().peek_bits(addr as usize, 4) as u32)
271271+ })?,
272272+ )?;
170273171274 let mem_poke1 = mem.clone();
172172- globals.set("poke1", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke1.borrow_mut().poke_bits(addr as usize, 1, val as u8); Ok(()) })?)?;
275275+ globals.set(
276276+ "poke1",
277277+ lua.create_function(move |_, (addr, val): (u32, u32)| {
278278+ mem_poke1
279279+ .borrow_mut()
280280+ .poke_bits(addr as usize, 1, val as u8);
281281+ Ok(())
282282+ })?,
283283+ )?;
173284 let mem_poke2 = mem.clone();
174174- globals.set("poke2", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke2.borrow_mut().poke_bits(addr as usize, 2, val as u8); Ok(()) })?)?;
285285+ globals.set(
286286+ "poke2",
287287+ lua.create_function(move |_, (addr, val): (u32, u32)| {
288288+ mem_poke2
289289+ .borrow_mut()
290290+ .poke_bits(addr as usize, 2, val as u8);
291291+ Ok(())
292292+ })?,
293293+ )?;
175294 let mem_poke4 = mem.clone();
176176- globals.set("poke4", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke4.borrow_mut().poke_bits(addr as usize, 4, val as u8); Ok(()) })?)?;
295295+ globals.set(
296296+ "poke4",
297297+ lua.create_function(move |_, (addr, val): (u32, u32)| {
298298+ mem_poke4
299299+ .borrow_mut()
300300+ .poke_bits(addr as usize, 4, val as u8);
301301+ Ok(())
302302+ })?,
303303+ )?;
177304178305 let mem_memcpy = mem.clone();
179179- globals.set("memcpy", lua.create_function(move |_, (dst, src, size): (u32, u32, u32)| { mem_memcpy.borrow_mut().memcpy(dst as usize, src as usize, size as usize); Ok(()) })?)?;
306306+ globals.set(
307307+ "memcpy",
308308+ lua.create_function(move |_, (dst, src, size): (u32, u32, u32)| {
309309+ mem_memcpy
310310+ .borrow_mut()
311311+ .memcpy(dst as usize, src as usize, size as usize);
312312+ Ok(())
313313+ })?,
314314+ )?;
180315 let mem_memset = mem.clone();
181181- globals.set("memset", lua.create_function(move |_, (dst, val, size): (u32, u32, u32)| { mem_memset.borrow_mut().memset(dst as usize, val as u8, size as usize); Ok(()) })?)?;
316316+ globals.set(
317317+ "memset",
318318+ lua.create_function(move |_, (dst, val, size): (u32, u32, u32)| {
319319+ mem_memset
320320+ .borrow_mut()
321321+ .memset(dst as usize, val as u8, size as usize);
322322+ Ok(())
323323+ })?,
324324+ )?;
182325183326 // elli(x, y, a, b, color)
184327 let fb_elli = fb.clone();
185185- let elli_fn = lua.create_function(move |_, (x, y, a, b, color): (i32, i32, i32, i32, u8)| {
186186- fb_elli.borrow_mut().elli(x, y, a, b, color);
187187- Ok(())
188188- })?;
328328+ let elli_fn =
329329+ lua.create_function(move |_, (x, y, a, b, color): (i32, i32, i32, i32, u8)| {
330330+ fb_elli.borrow_mut().elli(x, y, a, b, color);
331331+ Ok(())
332332+ })?;
189333 globals.set("elli", elli_fn)?;
190334191335 // ellib(x, y, a, b, color)
192336 let fb_ellib = fb.clone();
193193- let ellib_fn = lua.create_function(move |_, (x, y, a, b, color): (i32, i32, i32, i32, u8)| {
194194- fb_ellib.borrow_mut().ellib(x, y, a, b, color);
195195- Ok(())
196196- })?;
337337+ let ellib_fn =
338338+ lua.create_function(move |_, (x, y, a, b, color): (i32, i32, i32, i32, u8)| {
339339+ fb_ellib.borrow_mut().ellib(x, y, a, b, color);
340340+ Ok(())
341341+ })?;
197342 globals.set("ellib", ellib_fn)?;
198343199344 // tri(x1,y1,x2,y2,x3,y3,color)
200345 let fb_tri = fb.clone();
201201- let tri_fn = lua.create_function(move |_, (x1,y1,x2,y2,x3,y3,color): (i32,i32,i32,i32,i32,i32,u8)| {
202202- fb_tri.borrow_mut().tri(x1,y1,x2,y2,x3,y3,color);
203203- Ok(())
204204- })?;
346346+ let tri_fn = lua.create_function(
347347+ move |_, (x1, y1, x2, y2, x3, y3, color): (i32, i32, i32, i32, i32, i32, u8)| {
348348+ fb_tri.borrow_mut().tri(x1, y1, x2, y2, x3, y3, color);
349349+ Ok(())
350350+ },
351351+ )?;
205352 globals.set("tri", tri_fn)?;
206353207354 // trib(x1,y1,x2,y2,x3,y3,color)
208355 let fb_trib = fb.clone();
209209- let trib_fn = lua.create_function(move |_, (x1,y1,x2,y2,x3,y3,color): (i32,i32,i32,i32,i32,i32,u8)| {
210210- fb_trib.borrow_mut().trib(x1,y1,x2,y2,x3,y3,color);
211211- Ok(())
212212- })?;
356356+ let trib_fn = lua.create_function(
357357+ move |_, (x1, y1, x2, y2, x3, y3, color): (i32, i32, i32, i32, i32, i32, u8)| {
358358+ fb_trib.borrow_mut().trib(x1, y1, x2, y2, x3, y3, color);
359359+ Ok(())
360360+ },
361361+ )?;
213362 globals.set("trib", trib_fn)?;
214363215364 // Load script
+76
tic80_rust/tests/fft_tests.rs
···11+use std::sync::Arc;
22+33+use parking_lot::RwLock;
44+use std::cell::RefCell;
55+use std::rc::Rc;
66+use tic80_rust::audio::fft::{query_fft, set_global_fft, FFTState};
77+use tic80_rust::core::memory::Memory;
88+use tic80_rust::gfx::framebuffer::Framebuffer;
99+use tic80_rust::script::lua_runner::LuaRunner;
1010+1111+fn gen_bin_exact_samples(k: usize, n_samples: usize) -> Vec<f32> {
1212+ // Generate y[n] = sin(2π k n / N) with N=2048 to align with FFT bins
1313+ let nfft = 2048.0f32;
1414+ (0..n_samples)
1515+ .map(|n| {
1616+ let angle = 2.0 * std::f32::consts::PI * (k as f32) * (n as f32) / nfft;
1717+ angle.sin()
1818+ })
1919+ .collect()
2020+}
2121+2222+#[test]
2323+fn fft_query_peak_at_bin() {
2424+ let cap = 8192;
2525+ let mut fft = FFTState::new(cap);
2626+ let k = 64usize;
2727+ let samples = gen_bin_exact_samples(k, 4096);
2828+ for s in samples {
2929+ fft.ingest(s);
3030+ }
3131+ fft.update();
3232+3333+ // raw magnitude at k should exceed neighbors
3434+ let v_k = query_fft(&fft, k as i32, -1, false, true);
3535+ let v_l = query_fft(&fft, (k as i32) - 1, -1, false, true);
3636+ let v_r = query_fft(&fft, (k as i32) + 1, -1, false, true);
3737+ assert!(
3838+ v_k > v_l * 2.0 && v_k > v_r * 2.0,
3939+ "expected distinct peak at bin k"
4040+ );
4141+}
4242+4343+#[test]
4444+fn lua_fft_returns_normalized_bin() {
4545+ // Prepare shared FFT state and feed a strong tone at bin k
4646+ let cap = 8192;
4747+ let fft = Arc::new(RwLock::new(FFTState::new(cap)));
4848+ let k = 32usize;
4949+ let samples = gen_bin_exact_samples(k, 4096);
5050+ {
5151+ let mut w = fft.write();
5252+ for s in samples {
5353+ w.ingest(s);
5454+ }
5555+ w.update();
5656+ }
5757+ set_global_fft(fft);
5858+5959+ // Lua script: mark pixel if normalized fft(k) > 0.5
6060+ let script = format!(
6161+ "\
6262+ function BOOT() cls(0) end\n\
6363+ function TIC()\n\
6464+ local v = fft({k}, -1)\n\
6565+ if v > 0.5 then pix(0,0,7) end\n\
6666+ end\n",
6767+ k = k
6868+ );
6969+7070+ let fb = Rc::new(RefCell::new(Framebuffer::new()));
7171+ let mem = Rc::new(RefCell::new(Memory::new(fb.clone())));
7272+ let runner = LuaRunner::new(fb.clone(), mem, &script).expect("lua init");
7373+ runner.tick();
7474+7575+ assert_eq!(fb.borrow_mut().pix(0, 0, None), Some(7));
7676+}
+36-13
tic80_rust/tests/gfx_framebuffer_tests.rs
···5050 let (w, h) = dimensions();
5151 let x0 = 0i32;
5252 let y0 = 0i32;
5353- let x1 = ( -5 + 10).min(w as i32);
5454- let y1 = ( -3 + 8).min(h as i32);
5353+ let x1 = (-5 + 10).min(w as i32);
5454+ let y1 = (-3 + 8).min(h as i32);
5555 let expected = (x1 - x0).max(0) as usize * (y1 - y0).max(0) as usize;
5656 let mut count = 0usize;
5757 for y in 0..(h as i32) {
···141141 let mut fb = Framebuffer::new();
142142 fb.cls(0);
143143 // set three sample pixels to known colors
144144- fb.set_pixel(0, 0, 0); // black
145145- fb.set_pixel(1, 0, 9); // orange
146146- fb.set_pixel(2, 0, 15); // peach
144144+ fb.set_pixel(0, 0, 0); // black
145145+ fb.set_pixel(1, 0, 9); // orange
146146+ fb.set_pixel(2, 0, 15); // peach
147147148148 let (w, h) = dimensions();
149149 let mut rgba = vec![0u8; (w * h * 4) as usize];
···208208 // Something on row 0
209209 let mut any_row0 = false;
210210 for x in 0..8 {
211211- if fb2.pix(x, 0, None) == Some(15) { any_row0 = true; break; }
211211+ if fb2.pix(x, 0, None) == Some(15) {
212212+ any_row0 = true;
213213+ break;
214214+ }
212215 }
213216 assert!(any_row0);
214217 // And something on row 6
215218 let mut any_row6 = false;
216219 for x in 0..8 {
217217- if fb2.pix(x, 6, None) == Some(15) { any_row6 = true; break; }
220220+ if fb2.pix(x, 6, None) == Some(15) {
221221+ any_row6 = true;
222222+ break;
223223+ }
218224 }
219225 assert!(any_row6);
220226}
···224230 let mut fb = Framebuffer::new();
225231 fb.cls(2);
226232 fb.clip(1, 1, 1, 1); // only (1,1)
227227- // Write outside clip
233233+ // Write outside clip
228234 let _ = fb.pix(0, 0, Some(7));
229235 // Write inside clip
230236 let _ = fb.pix(1, 1, Some(7));
···352358 fb.tri(10, 10, 20, 10, 15, 15, 6);
353359 // Top scanline: interior x in (10,20) filled; endpoints excluded by top-left rule
354360 assert_eq!(fb.pix(10, 10, None), Some(0));
355355- for x in 11..20 { assert_eq!(fb.pix(x, 10, None), Some(6)); }
361361+ for x in 11..20 {
362362+ assert_eq!(fb.pix(x, 10, None), Some(6));
363363+ }
356364 assert_eq!(fb.pix(20, 10, None), Some(0));
357365 // Bottom row excluded
358358- for x in 10..=20 { assert_eq!(fb.pix(x, 15, None), Some(0)); }
366366+ for x in 10..=20 {
367367+ assert_eq!(fb.pix(x, 15, None), Some(0));
368368+ }
359369}
360370361371#[test]
···364374 fb.cls(0);
365375 // Flat-bottom triangle: bottom edge y=20 excluded
366376 fb.tri(10, 10, 5, 20, 15, 20, 7);
367367- for x in 5..=15 { assert_eq!(fb.pix(x, 20, None), Some(0)); }
377377+ for x in 5..=15 {
378378+ assert_eq!(fb.pix(x, 20, None), Some(0));
379379+ }
368380 // No assumption on apex inclusion; key check is base exclusion
369381}
370382···376388 fb.tri(0, 0, 10, 0, 0, 10, 3);
377389 fb.tri(10, 10, 10, 0, 0, 10, 3);
378390 let mut count = 0usize;
379379- for y in 0..10 { for x in 0..10 { if fb.pix(x, y, None) == Some(3) { count += 1; } } }
391391+ for y in 0..10 {
392392+ for x in 0..10 {
393393+ if fb.pix(x, y, None) == Some(3) {
394394+ count += 1;
395395+ }
396396+ }
397397+ }
380398 assert_eq!(count, 100, "expected full 10x10 coverage without gaps");
381399}
382400···388406 fb.tri(10, 10, 10, 15, 10, 20, 9);
389407 // No pixels should be colored with 9
390408 let mut any = false;
391391- for y in 10..=20 { if fb.pix(10, y, None) == Some(9) { any = true; break; } }
409409+ for y in 10..=20 {
410410+ if fb.pix(10, y, None) == Some(9) {
411411+ any = true;
412412+ break;
413413+ }
414414+ }
392415 assert!(!any, "degenerate triangle should not draw");
393416}
+36-12
tic80_rust/tests/lua_api_tests.rs
···11use std::cell::RefCell;
22-use std::rc::Rc;
32use std::fs;
43use std::path::PathBuf;
44+use std::rc::Rc;
5566+use tic80_rust::core::memory::Memory;
67use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer};
78use tic80_rust::script::lua_runner::LuaRunner;
88-use tic80_rust::core::memory::Memory;
991010fn run_lua(script: &str, ticks: usize) -> Rc<RefCell<Framebuffer>> {
1111 let fb = Rc::new(RefCell::new(Framebuffer::new()));
1212 let mem = Rc::new(RefCell::new(Memory::new(fb.clone())));
1313 let runner = LuaRunner::new(fb.clone(), mem, script).expect("lua init");
1414- for _ in 0..ticks { runner.tick(); }
1414+ for _ in 0..ticks {
1515+ runner.tick();
1616+ }
1517 fb
1618}
1719···7577 break;
7678 }
7779 }
7878- if any { break; }
8080+ if any {
8181+ break;
8282+ }
7983 }
8084 assert!(any, "expected some glyph pixels drawn near (10,10)");
8185···8387 let (w, _) = dimensions();
8488 let mut found_marker = false;
8589 for x in 10..(w as i32) {
8686- if fbm.pix(x, 10, None) == Some(14) { found_marker = true; break; }
9090+ if fbm.pix(x, 10, None) == Some(14) {
9191+ found_marker = true;
9292+ break;
9393+ }
8794 }
8888- assert!(found_marker, "expected marker pixel with color 14 on row 10");
9595+ assert!(
9696+ found_marker,
9797+ "expected marker pixel with color 14 on row 10"
9898+ );
8999}
9010091101#[test]
···114124 let mut any = false;
115125 for yy in 0..8 {
116126 for xx in 0..8 {
117117- if fbm.pix(xx, yy, None) == Some(15) { any = true; break; }
127127+ if fbm.pix(xx, yy, None) == Some(15) {
128128+ any = true;
129129+ break;
130130+ }
118131 }
119119- if any { break; }
132132+ if any {
133133+ break;
134134+ }
120135 }
121136 assert!(any, "expected some glyph pixels drawn near origin");
122137 // verify the script marked (w,0) with 7 (we don't need to know w here)
···209224 let fb = Rc::new(RefCell::new(Framebuffer::new()));
210225 let mem = Rc::new(RefCell::new(Memory::new(fb.clone())));
211226 let runner = LuaRunner::new(fb.clone(), mem, &script).expect("lua init");
212212- for _ in 0..ticks { runner.tick(); }
227227+ for _ in 0..ticks {
228228+ runner.tick();
229229+ }
213230 let mut borrowed = fb.borrow_mut();
214231 fb_hash(&mut borrowed)
215232 };
···223240 assert_eq!(h2, h2_again, "hash should be deterministic for 2 ticks");
224241225242 // Different frame counts should usually yield different hashes for this cart
226226- assert_ne!(h1, h2, "different ticks should yield different frame hashes");
243243+ assert_ne!(
244244+ h1, h2,
245245+ "different ticks should yield different frame hashes"
246246+ );
227247}
228248229249#[test]
···238258 let fb = run_lua(script, 1);
239259 let mut fbm = fb.borrow_mut();
240260 // Filled circle: center row span for r=4
241241- for x in 16..=24 { assert_eq!(fbm.pix(x, 20, None), Some(6)); }
261261+ for x in 16..=24 {
262262+ assert_eq!(fbm.pix(x, 20, None), Some(6));
263263+ }
242264 // Border circle: cardinal points for r=3
243265 assert_eq!(fbm.pix(33, 20, None), Some(9));
244266 assert_eq!(fbm.pix(27, 20, None), Some(9));
···265287 assert_eq!(fbm.pix(60, 23, None), Some(12));
266288 assert_eq!(fbm.pix(60, 17, None), Some(12));
267289 // Filled ellipse center row (interior only; endpoints are border color)
268268- for x in 56..=64 { assert_eq!(fbm.pix(x, 20, None), Some(4)); }
290290+ for x in 56..=64 {
291291+ assert_eq!(fbm.pix(x, 20, None), Some(4));
292292+ }
269293 // Triangle interior
270294 assert_eq!(fbm.pix(80, 18, None), Some(6));
271295 // Border triangle vertices
+10-4
tic80_rust/tests/memory_tests.rs
···3333 let mut mem = Memory::new(fb.clone());
3434 // memset first 10 bytes of VRAM screen -> sets first 20 pixels (pairs) to (0x5,0x5)
3535 mem.memset(0, 0x55, 10);
3636- for x in 0..20 { assert_eq!(fb.borrow_mut().pix(x, 0, None), Some(0x5)); }
3636+ for x in 0..20 {
3737+ assert_eq!(fb.borrow_mut().pix(x, 0, None), Some(0x5));
3838+ }
37393840 // prepare source bytes with pattern 0xAB -> (0xB,0xA) on pixels
3941 let src = 40000usize; // within RAM region beyond VRAM
4040- for i in 0..4 { mem.poke(src + i, 0xAB); }
4242+ for i in 0..4 {
4343+ mem.poke(src + i, 0xAB);
4444+ }
4145 mem.memcpy(0, src, 4); // copy into beginning of VRAM
4242- // First 8 pixels now map from 0xAB pairs
4646+ // First 8 pixels now map from 0xAB pairs
4347 let mut px = vec![];
4444- for x in 0..8 { px.push(fb.borrow_mut().pix(x, 0, None).unwrap()); }
4848+ for x in 0..8 {
4949+ px.push(fb.borrow_mut().pix(x, 0, None).unwrap());
5050+ }
4551 assert_eq!(&px, &[0xB, 0xA, 0xB, 0xA, 0xB, 0xA, 0xB, 0xA]);
4652}
4753