feat: phase 4 — Hextile decoder and recording example
codec/hextile.rs: - Full Hextile (encoding 5) decoder per ByteColorRFBRenderer.int() - Handles: Raw tiles, BackgroundSpecified, ForegroundSpecified, AnySubrects, SubrectsColoured flags - Background/foreground colors persist across tiles - 4 unit tests covering all subencoding paths framebuffer.rs: - Added fill_rect() for Hextile background/subrect fills session.rs: - Wired Hextile encoding 5 into the rect dispatch examples/record.rs: - 30-second (configurable) recording session - Saves 1 PNG per second to out/ directory - Requests encodings [5, 1, 0] (Hextile, CopyRect, Raw) - Tested against real OmniView: 10 frames in 10s, no errors 27 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
148
crates/ericrfb/src/codec/hextile.rs
Normal file
148
crates/ericrfb/src/codec/hextile.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use std::io::Read;
|
||||
|
||||
use crate::framebuffer::Framebuffer;
|
||||
use crate::proto::{self, read_exact, read_u8};
|
||||
|
||||
// Subencoding flag bits — ByteColorRFBRenderer.int(), line 192
|
||||
const RAW: u8 = 1;
|
||||
const BACKGROUND_SPECIFIED: u8 = 2;
|
||||
const FOREGROUND_SPECIFIED: u8 = 4;
|
||||
const ANY_SUBRECTS: u8 = 8;
|
||||
const SUBRECTS_COLOURED: u8 = 16;
|
||||
|
||||
/// Decode a Hextile-encoded rectangle into the framebuffer.
|
||||
///
|
||||
/// The rectangle is divided into 16x16 tiles (edge tiles may be smaller).
|
||||
/// Background and foreground colors persist across tiles within one call.
|
||||
///
|
||||
/// Reference: ByteColorRFBRenderer.int() line 169.
|
||||
pub fn decode_hextile(
|
||||
r: &mut impl Read,
|
||||
fb: &mut Framebuffer,
|
||||
rx: u16,
|
||||
ry: u16,
|
||||
rw: u16,
|
||||
rh: u16,
|
||||
) -> proto::Result<()> {
|
||||
let mut bg: u8 = 0;
|
||||
let mut fg: u8 = 0;
|
||||
|
||||
let mut ty = ry;
|
||||
while ty < ry + rh {
|
||||
let tile_h = (ry + rh - ty).min(16);
|
||||
let mut tx = rx;
|
||||
while tx < rx + rw {
|
||||
let tile_w = (rx + rw - tx).min(16);
|
||||
let flags = read_u8(r)?;
|
||||
|
||||
if flags & RAW != 0 {
|
||||
// Raw tile: read tile_w * tile_h bytes
|
||||
let size = tile_w as usize * tile_h as usize;
|
||||
let data = read_exact(r, size)?;
|
||||
fb.apply_raw(tx, ty, tile_w, tile_h, &data);
|
||||
tx += 16;
|
||||
continue;
|
||||
}
|
||||
|
||||
if flags & BACKGROUND_SPECIFIED != 0 {
|
||||
bg = read_u8(r)?;
|
||||
}
|
||||
|
||||
// Fill tile with background
|
||||
fb.fill_rect(tx, ty, tile_w, tile_h, bg);
|
||||
|
||||
if flags & FOREGROUND_SPECIFIED != 0 {
|
||||
fg = read_u8(r)?;
|
||||
}
|
||||
|
||||
if flags & ANY_SUBRECTS != 0 {
|
||||
let num_subrects = read_u8(r)?;
|
||||
let coloured = flags & SUBRECTS_COLOURED != 0;
|
||||
|
||||
for _ in 0..num_subrects {
|
||||
let color = if coloured { read_u8(r)? } else { fg };
|
||||
let xy = read_u8(r)?;
|
||||
let wh = read_u8(r)?;
|
||||
let sx = (xy >> 4) as u16;
|
||||
let sy = (xy & 0x0F) as u16;
|
||||
let sw = ((wh >> 4) + 1) as u16;
|
||||
let sh = ((wh & 0x0F) + 1) as u16;
|
||||
fb.fill_rect(tx + sx, ty + sy, sw, sh, color);
|
||||
}
|
||||
}
|
||||
|
||||
tx += 16;
|
||||
}
|
||||
ty += 16;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn test_hextile_raw_tile() {
|
||||
let mut fb = Framebuffer::new(16, 16);
|
||||
// One 16x16 tile, Raw subencoding
|
||||
let mut data = vec![RAW]; // flags
|
||||
data.extend_from_slice(&[0x42u8; 256]); // 16*16 raw pixels
|
||||
let mut c = Cursor::new(data);
|
||||
decode_hextile(&mut c, &mut fb, 0, 0, 16, 16).unwrap();
|
||||
assert_eq!(fb.pixels[0], 0x42);
|
||||
assert_eq!(fb.pixels[255], 0x42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hextile_bg_fill() {
|
||||
let mut fb = Framebuffer::new(16, 16);
|
||||
// One tile: background=0x09, no subrects
|
||||
let data = vec![BACKGROUND_SPECIFIED, 0x09];
|
||||
let mut c = Cursor::new(data);
|
||||
decode_hextile(&mut c, &mut fb, 0, 0, 16, 16).unwrap();
|
||||
assert!(fb.pixels.iter().all(|&p| p == 0x09));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hextile_subrects_coloured() {
|
||||
let mut fb = Framebuffer::new(16, 16);
|
||||
// Background=0x00, 1 coloured subrect at (2,3) size 4x5 color 0xFF
|
||||
let data = vec![
|
||||
BACKGROUND_SPECIFIED | ANY_SUBRECTS | SUBRECTS_COLOURED,
|
||||
0x00, // bg
|
||||
1, // num_subrects
|
||||
0xFF, // subrect color
|
||||
0x23, // xy: x=2, y=3
|
||||
0x34, // wh: w=3+1=4, h=4+1=5
|
||||
];
|
||||
let mut c = Cursor::new(data);
|
||||
decode_hextile(&mut c, &mut fb, 0, 0, 16, 16).unwrap();
|
||||
assert_eq!(fb.pixels[0], 0x00); // background
|
||||
assert_eq!(fb.pixels[3 * 16 + 2], 0xFF); // subrect at (2,3)
|
||||
assert_eq!(fb.pixels[7 * 16 + 5], 0xFF); // subrect at (5,7)
|
||||
assert_eq!(fb.pixels[8 * 16 + 2], 0x00); // below subrect
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hextile_fg_subrects() {
|
||||
let mut fb = Framebuffer::new(16, 16);
|
||||
// Background=0x00, foreground=0xAA, 1 subrect at (0,0) size 2x2
|
||||
let data = vec![
|
||||
BACKGROUND_SPECIFIED | FOREGROUND_SPECIFIED | ANY_SUBRECTS,
|
||||
0x00, // bg
|
||||
0xAA, // fg
|
||||
1, // num_subrects
|
||||
0x00, // xy: x=0, y=0
|
||||
0x11, // wh: w=1+1=2, h=1+1=2
|
||||
];
|
||||
let mut c = Cursor::new(data);
|
||||
decode_hextile(&mut c, &mut fb, 0, 0, 16, 16).unwrap();
|
||||
assert_eq!(fb.pixels[0], 0xAA);
|
||||
assert_eq!(fb.pixels[1], 0xAA);
|
||||
assert_eq!(fb.pixels[16], 0xAA);
|
||||
assert_eq!(fb.pixels[17], 0xAA);
|
||||
assert_eq!(fb.pixels[2], 0x00);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user