feat: phase 9 — encoding 9 (IIP) with tile-versioned delta cache
codec/iip.rs:
- TileCache: (fb_width/16 × fb_height/16) tiles, 8 versions × 256 bytes
each, matching t.java's (8, 16*16) allocation
- TileEntry: versioned read/write at byte offsets within tile data
- decode_iip() handles all 4 modes from the control byte:
- Mode 0/12 (cache-read): tile control bytes select which cached
version of each 16x16 tile to display, no new pixel data on wire
- Mode 4 (write-only): Tight-decoded pixel data written to cache
- Mode 8 (update+read): conditionally writes new data to cache
(bit 7 of control byte = 0 means update), then reads from cache
- Tile control bytes compressed via zlib (varint length) when >= 12
- Sub-types 1-4/8 map to bit-depths 1/2/4/4/8 bpp
- Cache resized on framebuffer resize (ModeChange msg 128)
Wired into session dispatch for encoding 9. Not advertised in default
encoding list — only active if explicitly requested.
codec/tight.rs: made get_or_init() pub for IIP's zlib access.
39 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
263
crates/ericrfb/src/codec/iip.rs
Normal file
263
crates/ericrfb/src/codec/iip.rs
Normal file
@@ -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<TileEntry>,
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod hextile;
|
pub mod hextile;
|
||||||
|
pub mod iip;
|
||||||
pub mod raw_tile;
|
pub mod raw_tile;
|
||||||
pub mod tight;
|
pub mod tight;
|
||||||
|
|||||||
@@ -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))
|
self.streams[idx].get_or_insert_with(|| Decompress::new(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::io::{BufReader, BufWriter};
|
use std::io::{BufReader, BufWriter};
|
||||||
use std::net::TcpStream;
|
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::framebuffer::Framebuffer;
|
||||||
use crate::handshake::{self, Config, ServerInit};
|
use crate::handshake::{self, Config, ServerInit};
|
||||||
use crate::msg::{self, ServerMsg};
|
use crate::msg::{self, ServerMsg};
|
||||||
@@ -46,19 +46,23 @@ pub struct ActiveSession {
|
|||||||
pub writer: BufWriter<TcpStream>,
|
pub writer: BufWriter<TcpStream>,
|
||||||
pub server_init: ServerInit,
|
pub server_init: ServerInit,
|
||||||
zlib: tight::ZlibStreams,
|
zlib: tight::ZlibStreams,
|
||||||
|
tile_cache: iip::TileCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveSession {
|
impl ActiveSession {
|
||||||
/// Connect, handshake, send SetEncodings + initial FBUpdateRequest.
|
/// Connect, handshake, send SetEncodings + initial FBUpdateRequest.
|
||||||
pub fn connect(cfg: &Config, encodings: &[i32]) -> Result<Self, SessionError> {
|
pub fn connect(cfg: &Config, encodings: &[i32]) -> Result<Self, SessionError> {
|
||||||
let raw = handshake::connect(cfg)?;
|
let raw = handshake::connect(cfg)?;
|
||||||
|
let w = raw.server_init.width;
|
||||||
|
let h = raw.server_init.height;
|
||||||
let mut session = Self {
|
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,
|
server_name: raw.server_name,
|
||||||
reader: raw.reader,
|
reader: raw.reader,
|
||||||
writer: raw.writer,
|
writer: raw.writer,
|
||||||
server_init: raw.server_init,
|
server_init: raw.server_init,
|
||||||
zlib: tight::ZlibStreams::new(),
|
zlib: tight::ZlibStreams::new(),
|
||||||
|
tile_cache: iip::TileCache::new(w, h),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tell server to send 8bpp RGB332 pixels
|
// 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 {
|
if self.server_init.width != old_w || self.server_init.height != old_h {
|
||||||
self.framebuffer
|
self.framebuffer
|
||||||
.resize(self.server_init.width, self.server_init.height);
|
.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 {
|
return Ok(Some(Event::Resize {
|
||||||
width: self.server_init.width,
|
width: self.server_init.width,
|
||||||
height: self.server_init.height,
|
height: self.server_init.height,
|
||||||
@@ -221,6 +227,18 @@ impl ActiveSession {
|
|||||||
hdr.h,
|
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 => {
|
10 => {
|
||||||
raw_tile::decode_raw_tile(
|
raw_tile::decode_raw_tile(
|
||||||
&mut self.reader,
|
&mut self.reader,
|
||||||
|
|||||||
Reference in New Issue
Block a user