Frontend reconnection: - WebSocket auto-reconnects with exponential backoff (1s → 30s) - Re-authenticates with OmniView to get fresh APPLET_ID on reconnect - Credentials stored in sessionStorage for automatic re-login - Status bar shows connection state and reconnect countdown - Disconnect button returns to login screen Encoding 10 (Raw with tile interleave): - codec/raw_tile.rs: decodes encoding 10 per ByteColorRFBRenderer.for() - Flag byte bit 0 selects plain raw vs 16x16 tile-interleaved data - Deinterleave handles edge tiles smaller than 16x16 - Wired into session dispatch - 2 unit tests 39 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
3.3 KiB
Rust
104 lines
3.3 KiB
Rust
use std::io::Read;
|
|
|
|
use crate::framebuffer::Framebuffer;
|
|
use crate::proto::{self, read_exact, read_i8};
|
|
|
|
/// Decode encoding 10 (Raw with tile interleave).
|
|
///
|
|
/// Reads 1 flag byte. If bit 0 is clear, falls back to plain Raw.
|
|
/// If bit 0 is set, reads w*h bytes of 16x16 tile-interleaved data
|
|
/// and deinterleaves to row-major before blitting.
|
|
///
|
|
/// Reference: ByteColorRFBRenderer.for() line 109.
|
|
pub fn decode_raw_tile(
|
|
r: &mut impl Read,
|
|
fb: &mut Framebuffer,
|
|
rx: u16,
|
|
ry: u16,
|
|
rw: u16,
|
|
rh: u16,
|
|
) -> proto::Result<()> {
|
|
let flag = read_i8(r)?;
|
|
|
|
let w = rw as usize;
|
|
let h = rh as usize;
|
|
let size = w * h;
|
|
|
|
if flag & 1 == 0 {
|
|
// Plain raw — no interleave
|
|
let data = read_exact(r, size)?;
|
|
fb.apply_raw(rx, ry, rw, rh, &data);
|
|
return Ok(());
|
|
}
|
|
|
|
// Read tile-interleaved data
|
|
let interleaved = read_exact(r, size)?;
|
|
|
|
// Clamp to framebuffer bounds
|
|
let w = w.min(fb.width as usize - rx as usize);
|
|
let h = h.min(fb.height as usize - ry as usize);
|
|
|
|
// Deinterleave 16x16 tiles to row-major.
|
|
// Input is stored tile-by-tile: all pixels of tile (0,0), then tile (1,0), etc.
|
|
// Each tile is row-major within itself.
|
|
let tile = 16usize;
|
|
let tiles_x = w.div_ceil(tile);
|
|
let mut output = vec![0u8; w * h];
|
|
|
|
for row in 0..h {
|
|
let tile_row = row / tile;
|
|
let row_in_tile = row % tile;
|
|
for col in 0..w {
|
|
let tile_col = col / tile;
|
|
let col_in_tile = col % tile;
|
|
// Tile index in raster order
|
|
let tile_idx = tile_row * tiles_x + tile_col;
|
|
// Tile dimensions (edge tiles may be smaller)
|
|
let tw = tile.min(w - tile_col * tile);
|
|
// Offset within tile data: preceding full tiles + row offset + col offset
|
|
let mut tile_data_start = 0usize;
|
|
// Sum sizes of all preceding tiles
|
|
for t in 0..tile_idx {
|
|
let tr = t / tiles_x;
|
|
let tc = t % tiles_x;
|
|
let this_tw = tile.min(w - tc * tile);
|
|
let this_th = tile.min(h - tr * tile);
|
|
tile_data_start += this_tw * this_th;
|
|
}
|
|
let src = tile_data_start + row_in_tile * tw + col_in_tile;
|
|
output[row * w + col] = interleaved[src];
|
|
}
|
|
}
|
|
|
|
fb.apply_raw(rx, ry, w as u16, h as u16, &output);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::Cursor;
|
|
|
|
#[test]
|
|
fn test_raw_tile_plain_fallback() {
|
|
let mut fb = Framebuffer::new(4, 4);
|
|
// Flag byte with bit 0 clear → plain raw
|
|
let mut data = vec![0x00i8 as u8]; // flag
|
|
data.extend_from_slice(&[0x42; 16]); // 4x4 pixels
|
|
let mut c = Cursor::new(data);
|
|
decode_raw_tile(&mut c, &mut fb, 0, 0, 4, 4).unwrap();
|
|
assert!(fb.pixels.iter().all(|&p| p == 0x42));
|
|
}
|
|
|
|
#[test]
|
|
fn test_raw_tile_small_no_tile_boundary() {
|
|
let mut fb = Framebuffer::new(4, 4);
|
|
// Flag byte with bit 0 set, but 4x4 < 16x16 so no tile wrap occurs
|
|
let mut data = vec![0x01u8]; // flag with interleave
|
|
data.extend_from_slice(&[0xAA; 16]); // 4x4 pixels (all same, no deinterleave effect)
|
|
let mut c = Cursor::new(data);
|
|
decode_raw_tile(&mut c, &mut fb, 0, 0, 4, 4).unwrap();
|
|
assert!(fb.pixels.iter().all(|&p| p == 0xAA));
|
|
}
|
|
}
|