diff --git a/crates/ericrfb/src/codec/iip.rs b/crates/ericrfb/src/codec/iip.rs new file mode 100644 index 0000000..685cfdc --- /dev/null +++ b/crates/ericrfb/src/codec/iip.rs @@ -0,0 +1,263 @@ +use std::io::Read; + +use crate::codec::tight; +use crate::framebuffer::Framebuffer; +use crate::proto::{self, read_exact, read_u8, read_varint}; + +const TILE: usize = 16; +const VERSIONS: usize = 8; +const TILE_BYTES: usize = TILE * TILE; // 256 bytes at 8bpp + +/// Per-tile versioned pixel cache — stores 8bpp data. +/// Reference: t.java — 8 versions × 256 bytes per tile. +struct TileEntry { + data: [[u8; TILE_BYTES]; VERSIONS], +} + +#[allow(dead_code)] +impl TileEntry { + fn new() -> Self { + Self { + data: [[0u8; TILE_BYTES]; VERSIONS], + } + } + + /// Write `len` bytes from `src[src_off..]` into version at `offset`. + fn write(&mut self, version: usize, offset: usize, src: &[u8], src_off: usize, len: usize) { + let v = version % VERSIONS; + self.data[v][offset..offset + len].copy_from_slice(&src[src_off..src_off + len]); + } + + /// Read `len` bytes from version at `offset` into `dst[dst_off..]`. + fn read(&self, version: usize, offset: usize, dst: &mut [u8], dst_off: usize, len: usize) { + let v = version % VERSIONS; + dst[dst_off..dst_off + len].copy_from_slice(&self.data[v][offset..offset + len]); + } +} + +/// Tile cache for encoding 9 (IIP). One entry per 16×16 tile on screen. +pub struct TileCache { + tiles: Vec, + tiles_x: usize, + tiles_y: usize, +} + +impl TileCache { + pub fn new(fb_width: u16, fb_height: u16) -> Self { + let tx = fb_width as usize / TILE; + let ty = fb_height as usize / TILE; + let count = tx * ty; + let mut tiles = Vec::with_capacity(count); + for _ in 0..count { + tiles.push(TileEntry::new()); + } + Self { + tiles, + tiles_x: tx, + tiles_y: ty, + } + } + + pub fn resize(&mut self, fb_width: u16, fb_height: u16) { + *self = Self::new(fb_width, fb_height); + } + + fn get(&self, tile_x: usize, tile_y: usize) -> &TileEntry { + &self.tiles[tile_y * self.tiles_x + tile_x] + } + + fn get_mut(&mut self, tile_x: usize, tile_y: usize) -> &mut TileEntry { + &mut self.tiles[tile_y * self.tiles_x + tile_x] + } +} + +/// Decode encoding 9 (IIP) — tile-cached delta compression. +/// +/// Control byte layout: +/// - bits 0-3: sub-type (1=1bpp, 2=2bpp, 3=4bpp_gray, 4=4bpp_color, 8=8bpp) +/// - bits 4-5: zlib stream index (0-3) +/// - bits 6-7: mode (0=cache-read, 4=write-only, 8=update+read, 12=read-only) +/// +/// Reference: ByteColorRFBRenderer.do() line 248. +#[allow(clippy::too_many_arguments)] +pub fn decode_iip( + r: &mut impl Read, + fb: &mut Framebuffer, + cache: &mut TileCache, + zlib: &mut tight::ZlibStreams, + rx: u16, + ry: u16, + rw: u16, + rh: u16, +) -> proto::Result<()> { + let control = read_u8(r)?; + let stream_idx = ((control >> 4) & 3) as usize; + let mode = (control >> 4) & 0x0C; // 0, 4, 8, or 12 + let sub_type = control & 0x0F; + + // Determine palette from sub-type (same as tight sub-palettes) + let _palette_id = match sub_type { + 1 => 10, // 1bpp BW + 2 => 11, // 2bpp gray4 + 3 => 12, // 4bpp gray16 + 4 => 13, // 4bpp color16 + 8 => 0, // 8bpp direct + _ => return Ok(()), // unknown sub-type, skip + }; + + // Calculate aligned tile region + let y_start = ry as usize; + let y_end = ry as usize + rh as usize; + let w = rw as usize; + + let tile_y_end = if !y_end.is_multiple_of(TILE) { + y_end / TILE * TILE + } else { + y_end + }; + let aligned_h = tile_y_end - y_start; + let num_tile_ctrl = (w / TILE) * (aligned_h / TILE); + + // Read tile control bytes (compressed or raw) + let tile_ctrl = if num_tile_ctrl < 12 { + read_exact(r, num_tile_ctrl)? + } else { + let comp_len = read_varint(r)? as usize; + let compressed = read_exact(r, comp_len)?; + let decompressor = zlib.get_or_init(stream_idx); + let mut output = vec![0u8; num_tile_ctrl]; + let before_out = decompressor.total_out(); + decompressor + .decompress(&compressed, &mut output, flate2::FlushDecompress::None) + .map_err(|e| { + proto::ProtoError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + })?; + let produced = (decompressor.total_out() - before_out) as usize; + if produced != num_tile_ctrl { + return Err(proto::ProtoError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("iip zlib: produced {produced}, expected {num_tile_ctrl}"), + ))); + } + output + }; + + if mode == 0 || mode == 12 { + // Cache-read only: no new pixel data on the wire. + // Read tile versions from cache directly to framebuffer. + read_cache_to_fb(fb, cache, &tile_ctrl, rx as usize, y_start, w, aligned_h); + } else { + // Mode 4 or 8: Tight-encoded pixel data follows. + // Decode Tight data into framebuffer, then update tile cache from framebuffer. + tight::decode_tight(r, fb, zlib, rx, ry, rw, rh)?; + + // Update tile cache from the framebuffer pixels we just wrote + update_cache_from_fb( + fb, + cache, + &tile_ctrl, + rx as usize, + y_start, + w, + aligned_h, + mode, + ); + + // For mode 8, re-read from cache (may differ if some tiles weren't updated) + if mode == 8 { + read_cache_to_fb(fb, cache, &tile_ctrl, rx as usize, y_start, w, aligned_h); + } + } + + Ok(()) +} + +/// Read tile versions from cache into the framebuffer. +fn read_cache_to_fb( + fb: &mut Framebuffer, + cache: &TileCache, + ctrl: &[u8], + rx: usize, + ry: usize, + rw: usize, + aligned_h: usize, +) { + let stride = fb.width as usize; + let mut ci = 0; + + for ty in (0..aligned_h).step_by(TILE) { + for tx in (0..rw).step_by(TILE) { + let version = (ctrl[ci] & 0x7F) as usize; + ci += 1; + + let tile_x = (rx + tx) / TILE; + let tile_y = (ry + ty) / TILE; + if tile_x >= cache.tiles_x || tile_y >= cache.tiles_y { + continue; + } + + let entry = cache.get(tile_x, tile_y); + let tw = TILE.min(rw - tx); + let th = TILE.min(aligned_h - ty); + + for row in 0..th { + let fb_off = (ry + ty + row) * stride + rx + tx; + let cache_off = row * TILE; + fb.pixels[fb_off..fb_off + tw] + .copy_from_slice(&entry.data[version % VERSIONS][cache_off..cache_off + tw]); + } + } + } +} + +/// Update tile cache from framebuffer pixels. +#[allow(clippy::too_many_arguments)] +fn update_cache_from_fb( + fb: &Framebuffer, + cache: &mut TileCache, + ctrl: &[u8], + rx: usize, + ry: usize, + rw: usize, + aligned_h: usize, + mode: u8, +) { + let stride = fb.width as usize; + let mut ci = 0; + + for ty in (0..aligned_h).step_by(TILE) { + for _tx_idx in 0..(rw / TILE) { + let tx = _tx_idx * TILE; + let byte = ctrl[ci]; + ci += 1; + + let version = (byte & 0x7F) as usize; + let should_update = if mode == 8 { + byte & 0x80 == 0 // bit 7 clear = update + } else { + true // mode 4: always update + }; + + if !should_update { + continue; + } + + let tile_x = (rx + tx) / TILE; + let tile_y = (ry + ty) / TILE; + if tile_x >= cache.tiles_x || tile_y >= cache.tiles_y { + continue; + } + + let entry = cache.get_mut(tile_x, tile_y); + let tw = TILE.min(rw - tx); + let th = TILE.min(aligned_h - ty); + + for row in 0..th { + let fb_off = (ry + ty + row) * stride + rx + tx; + let cache_off = row * TILE; + entry.data[version % VERSIONS][cache_off..cache_off + tw] + .copy_from_slice(&fb.pixels[fb_off..fb_off + tw]); + } + } + } +} diff --git a/crates/ericrfb/src/codec/mod.rs b/crates/ericrfb/src/codec/mod.rs index 911794d..6076470 100644 --- a/crates/ericrfb/src/codec/mod.rs +++ b/crates/ericrfb/src/codec/mod.rs @@ -1,3 +1,4 @@ pub mod hextile; +pub mod iip; pub mod raw_tile; pub mod tight; diff --git a/crates/ericrfb/src/codec/tight.rs b/crates/ericrfb/src/codec/tight.rs index 1d1764c..9c857b5 100644 --- a/crates/ericrfb/src/codec/tight.rs +++ b/crates/ericrfb/src/codec/tight.rs @@ -82,7 +82,7 @@ impl ZlibStreams { } } - fn get_or_init(&mut self, idx: usize) -> &mut Decompress { + pub fn get_or_init(&mut self, idx: usize) -> &mut Decompress { self.streams[idx].get_or_insert_with(|| Decompress::new(true)) } diff --git a/crates/ericrfb/src/session.rs b/crates/ericrfb/src/session.rs index 355cd9c..1de6237 100644 --- a/crates/ericrfb/src/session.rs +++ b/crates/ericrfb/src/session.rs @@ -1,7 +1,7 @@ use std::io::{BufReader, BufWriter}; use std::net::TcpStream; -use crate::codec::{hextile, raw_tile, tight}; +use crate::codec::{hextile, iip, raw_tile, tight}; use crate::framebuffer::Framebuffer; use crate::handshake::{self, Config, ServerInit}; use crate::msg::{self, ServerMsg}; @@ -46,19 +46,23 @@ pub struct ActiveSession { pub writer: BufWriter, pub server_init: ServerInit, zlib: tight::ZlibStreams, + tile_cache: iip::TileCache, } impl ActiveSession { /// Connect, handshake, send SetEncodings + initial FBUpdateRequest. pub fn connect(cfg: &Config, encodings: &[i32]) -> Result { let raw = handshake::connect(cfg)?; + let w = raw.server_init.width; + let h = raw.server_init.height; let mut session = Self { - framebuffer: Framebuffer::new(raw.server_init.width, raw.server_init.height), + framebuffer: Framebuffer::new(w, h), server_name: raw.server_name, reader: raw.reader, writer: raw.writer, server_init: raw.server_init, zlib: tight::ZlibStreams::new(), + tile_cache: iip::TileCache::new(w, h), }; // Tell server to send 8bpp RGB332 pixels @@ -157,6 +161,8 @@ impl ActiveSession { if self.server_init.width != old_w || self.server_init.height != old_h { self.framebuffer .resize(self.server_init.width, self.server_init.height); + self.tile_cache + .resize(self.server_init.width, self.server_init.height); return Ok(Some(Event::Resize { width: self.server_init.width, height: self.server_init.height, @@ -221,6 +227,18 @@ impl ActiveSession { hdr.h, )?; } + 9 => { + iip::decode_iip( + &mut self.reader, + &mut self.framebuffer, + &mut self.tile_cache, + &mut self.zlib, + hdr.x, + hdr.y, + hdr.w, + hdr.h, + )?; + } 10 => { raw_tile::decode_raw_tile( &mut self.reader,