From 1eca3ee0fed108f127dffcfb5e04654516f4fe55 Mon Sep 17 00:00:00 2001 From: Me Here Date: Mon, 1 Jun 2026 13:52:07 -0500 Subject: [PATCH] pm-kit: small-tile incremental updates (sub-rect blit) to cut tearing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated against GeeekPi's lv_port_disp.c (vendor LVGL driver for this kit): it flushes per-dirty-rectangle windowed writes at 62.5MHz, no TE — exactly this approach. Fix blit_rect (1024-byte buffer; the 512-byte one was the black-screen bug), switch incremental updates from full-width 144-row bands to changed 40x40 tiles. Full repaints still use the proven full blit. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/pm-kit/src/main.rs | 111 ++++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/rust/pm-kit/src/main.rs b/rust/pm-kit/src/main.rs index e94b021..91dbac8 100644 --- a/rust/pm-kit/src/main.rs +++ b/rust/pm-kit/src/main.rs @@ -38,6 +38,12 @@ const XTAL_FREQ_HZ: u32 = 12_000_000; const WIDTH: u16 = 320; const HEIGHT: u16 = 480; +/// Dirty-tile grid for incremental updates. TILE wide is sub-screen-width so each tile blit writes +/// faster than the panel scan crosses it → minimal tearing. 320/40=8 cols, 480/40=12 rows = 96 tiles. +const TILE: u16 = 40; +const NX: usize = WIDTH as usize / TILE as usize; +const NY: usize = HEIGHT as usize / TILE as usize; + /// Direct ST7796 driver — a faithful port of the proven MicroPython driver in `pico/main.py`, /// which runs this exact panel on this exact board. Per-command CS framing (CS low → command → /// data → CS high), MADCTL 0x48, INVON, NO row/column offset, big-endian RGB565. This replaces @@ -138,6 +144,40 @@ where fn blit(&mut self, px: &[Rgb565]) { self.blit_band(px, 0, HEIGHT); } + + /// Blit a sub-rectangle `(x,y,w,h)` gathered from the full framebuffer. Used for small tile + /// updates: sub-width writes finish a region faster than the panel scan crosses it (datasheet + /// §10.8 Example 1), so the visible tear is tiny vs a full-width band. Same streaming loop and + /// 1024-byte buffer as `blit_band` (the 512-byte buffer was what broke the earlier version). + fn blit_rect(&mut self, fb: &[Rgb565], x: u16, y: u16, w: u16, h: u16) { + let x1 = x + w - 1; + let y1 = y + h - 1; + self.cmd(0x2A, &[(x >> 8) as u8, (x & 0xFF) as u8, (x1 >> 8) as u8, (x1 & 0xFF) as u8]); + self.cmd(0x2B, &[(y >> 8) as u8, (y & 0xFF) as u8, (y1 >> 8) as u8, (y1 & 0xFF) as u8]); + let _ = self.cs.set_low(); + let _ = self.dc.set_low(); + let _ = self.spi.write(&[0x2C]); // RAMWR + let _ = self.dc.set_high(); + let mut buf = [0u8; 1024]; + let mut n = 0; + for r in 0..h as usize { + let row = (y as usize + r) * WIDTH as usize + x as usize; + for c in 0..w as usize { + let raw = fb[row + c].into_storage(); + buf[n] = (raw >> 8) as u8; + buf[n + 1] = (raw & 0xFF) as u8; + n += 2; + if n == buf.len() { + let _ = self.spi.write(&buf); + n = 0; + } + } + } + if n > 0 { + let _ = self.spi.write(&buf[..n]); + } + let _ = self.cs.set_high(); + } } @@ -275,7 +315,7 @@ fn main() -> ! { // changed vs the last frame. Full-width windows (CASET 0..319) behave exactly like the proven // full-screen blit — unlike sub-width tiles, which the panel's MX bit mirror-maps wrongly. let mut fb = FrameBuf { px: unsafe { &mut *core::ptr::addr_of_mut!(FRAMEBUF) } }; - let mut row_hash = [0u64; HEIGHT as usize]; // FNV-1a of each row at its last blit (0 → force first paint) + let mut tile_hash = [0u64; NX * NY]; // FNV-1a of each tile at its last blit (0 → force first paint) let mut bar_start = timer.get_counter().ticks(); let mut last_step = usize::MAX; // audio: click on a new step @@ -392,51 +432,46 @@ fn main() -> ! { } else { pm_ui::draw_metronome(&mut fb, &screen).ok(); } - #[inline(always)] - fn row_fnv(row: &[Rgb565]) -> u64 { + // FNV-1a of one TILE×TILE block at grid cell (tx, ty) + fn tile_fnv(px: &[Rgb565], tx: usize, ty: usize) -> u64 { let mut h = 0xcbf29ce484222325u64; // FNV-1a offset basis - for &p in row { - h ^= p.into_storage() as u64; - h = h.wrapping_mul(0x100000001b3); + let x0 = tx * TILE as usize; + let y0 = ty * TILE as usize; + for r in 0..TILE as usize { + let base = (y0 + r) * WIDTH as usize + x0; + for c in 0..TILE as usize { + h ^= px[base + c].into_storage() as u64; + h = h.wrapping_mul(0x100000001b3); + } } h } - let w = WIDTH as usize; if force_full { - // known-good full-screen blit; seed the row hashes so increments diff against it + // known-good full-screen blit; seed the tile hashes so increments diff against it st.blit(&fb.px[..]); - for yy in 0..HEIGHT as usize { - row_hash[yy] = row_fnv(&fb.px[yy * w..yy * w + w]); - } - force_full = false; - info!("FULL blit (seeded {} rows)", HEIGHT as u32); - } else { - // ---- dirty row-band blit: contiguous runs of changed rows, full-width ---- - let mut pushed_rows = 0u32; - let mut y = 0usize; - while y < HEIGHT as usize { - let h = row_fnv(&fb.px[y * w..y * w + w]); - if h != row_hash[y] { - let ylo = y; - row_hash[y] = h; - y += 1; - while y < HEIGHT as usize { - let hh = row_fnv(&fb.px[y * w..y * w + w]); - if hh == row_hash[y] { - break; // an unchanged row ends the dirty run - } - row_hash[y] = hh; - y += 1; - } - let band = (y - ylo) as u16; - st.blit_band(&fb.px[ylo * w..y * w], ylo as u16, band); - pushed_rows += band as u32; - } else { - y += 1; + for ty in 0..NY { + for tx in 0..NX { + tile_hash[ty * NX + tx] = tile_fnv(&fb.px[..], tx, ty); } } - if pushed_rows > 0 { - info!("blit {} / {} rows (bands)", pushed_rows, HEIGHT as u32); + force_full = false; + info!("FULL blit (seeded {} tiles)", (NX * NY) as u32); + } else { + // ---- dirty-tile blit: push only the changed TILE×TILE blocks (sub-width = less tearing) ---- + let mut pushed = 0u32; + for ty in 0..NY { + for tx in 0..NX { + let h = tile_fnv(&fb.px[..], tx, ty); + let idx = ty * NX + tx; + if h != tile_hash[idx] { + tile_hash[idx] = h; + st.blit_rect(&fb.px[..], (tx * TILE as usize) as u16, (ty * TILE as usize) as u16, TILE, TILE); + pushed += 1; + } + } + } + if pushed > 0 { + info!("blit {} / {} tiles", pushed, (NX * NY) as u32); } } drawn_step = cur_step;