mirror of
https://github.com/CloudNebulaProject/wayray.git
synced 2026-04-10 13:10:41 +00:00
Add XOR diff + zstd frame encoding in wayray-protocol
Encoder: xor_diff + encode_region (per damage rectangle). Decoder: apply_region (decompress + XOR apply to framebuffer). Both live in wayray-protocol::encoding for shared access. 14 tests including 3 end-to-end round-trip integration tests.
This commit is contained in:
parent
0762cb1fa3
commit
f394d8cd7d
9 changed files with 252 additions and 13 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
|
@ -2192,6 +2192,7 @@ dependencies = [
|
||||||
"postcard",
|
"postcard",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2597,3 +2598,31 @@ name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "7.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-sys"
|
||||||
|
version = "2.0.16+zstd.1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,4 @@ license.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
postcard.workspace = true
|
postcard.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
zstd.workspace = true
|
||||||
|
|
|
||||||
134
crates/wayray-protocol/src/encoding.rs
Normal file
134
crates/wayray-protocol/src/encoding.rs
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
//! Frame encoding and decoding: XOR diff + zstd compression.
|
||||||
|
//!
|
||||||
|
//! The encoder (server-side) produces `DamageRegion` messages from
|
||||||
|
//! framebuffer changes. The decoder (client-side) applies them to
|
||||||
|
//! reconstruct the framebuffer.
|
||||||
|
|
||||||
|
use crate::messages::DamageRegion;
|
||||||
|
|
||||||
|
// ── Encoder (server side) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Compute XOR diff between two ARGB8888 framebuffers.
|
||||||
|
///
|
||||||
|
/// Returns a new buffer where unchanged pixels are zero.
|
||||||
|
/// Both buffers must have the same length.
|
||||||
|
pub fn xor_diff(current: &[u8], previous: &[u8]) -> Vec<u8> {
|
||||||
|
assert_eq!(
|
||||||
|
current.len(),
|
||||||
|
previous.len(),
|
||||||
|
"framebuffer size mismatch: {} vs {}",
|
||||||
|
current.len(),
|
||||||
|
previous.len()
|
||||||
|
);
|
||||||
|
current
|
||||||
|
.iter()
|
||||||
|
.zip(previous.iter())
|
||||||
|
.map(|(a, b)| a ^ b)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a rectangular region from an XOR diff buffer and compress it.
|
||||||
|
///
|
||||||
|
/// `stride` is the number of bytes per row in the full framebuffer.
|
||||||
|
/// The region is compressed with zstd at level 1 (fast).
|
||||||
|
pub fn encode_region(
|
||||||
|
diff: &[u8],
|
||||||
|
stride: usize,
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> DamageRegion {
|
||||||
|
let bpp = 4; // ARGB8888
|
||||||
|
let region_stride = width as usize * bpp;
|
||||||
|
let mut region_data = Vec::with_capacity(region_stride * height as usize);
|
||||||
|
|
||||||
|
for row in 0..height as usize {
|
||||||
|
let src_offset = (y as usize + row) * stride + x as usize * bpp;
|
||||||
|
region_data.extend_from_slice(&diff[src_offset..src_offset + region_stride]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let compressed = zstd::encode_all(region_data.as_slice(), 1).expect("zstd encode failed");
|
||||||
|
|
||||||
|
DamageRegion {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
data: compressed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Decoder (client side) ───────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Apply a compressed XOR diff region onto a framebuffer.
|
||||||
|
///
|
||||||
|
/// Decompresses the region data with zstd, then XOR-applies it at the
|
||||||
|
/// specified position. Modifies `framebuffer` in place.
|
||||||
|
///
|
||||||
|
/// `stride` is the number of bytes per row in the full framebuffer.
|
||||||
|
pub fn apply_region(framebuffer: &mut [u8], stride: usize, region: &DamageRegion) {
|
||||||
|
let bpp = 4;
|
||||||
|
let decompressed = zstd::decode_all(region.data.as_slice()).expect("zstd decode failed");
|
||||||
|
let region_stride = region.width as usize * bpp;
|
||||||
|
|
||||||
|
for row in 0..region.height as usize {
|
||||||
|
let dst_offset = (region.y as usize + row) * stride + region.x as usize * bpp;
|
||||||
|
let src_offset = row * region_stride;
|
||||||
|
let dst_row = &mut framebuffer[dst_offset..dst_offset + region_stride];
|
||||||
|
let src_row = &decompressed[src_offset..src_offset + region_stride];
|
||||||
|
for (d, s) in dst_row.iter_mut().zip(src_row.iter()) {
|
||||||
|
*d ^= s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xor_diff_identical_frames() {
|
||||||
|
let frame = vec![100u8; 400];
|
||||||
|
let diff = xor_diff(&frame, &frame);
|
||||||
|
assert!(diff.iter().all(|&b| b == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xor_diff_changed_pixel() {
|
||||||
|
let prev = vec![0u8; 16];
|
||||||
|
let mut curr = vec![0u8; 16];
|
||||||
|
curr[0..4].copy_from_slice(&[255, 128, 64, 255]);
|
||||||
|
let diff = xor_diff(&curr, &prev);
|
||||||
|
assert_eq!(&diff[0..4], &[255, 128, 64, 255]);
|
||||||
|
assert!(diff[4..].iter().all(|&b| b == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_region_compresses_zeros() {
|
||||||
|
let width = 100usize;
|
||||||
|
let height = 100usize;
|
||||||
|
let stride = width * 4;
|
||||||
|
let diff = vec![0u8; stride * height];
|
||||||
|
let region = encode_region(&diff, stride, 0, 0, width as u32, height as u32);
|
||||||
|
assert!(region.data.len() < 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_region_zeros_is_noop() {
|
||||||
|
let stride = 40; // 10 pixels * 4 bpp
|
||||||
|
let mut framebuffer = vec![42u8; stride * 10];
|
||||||
|
let original = framebuffer.clone();
|
||||||
|
let zero_data = vec![0u8; stride * 10];
|
||||||
|
let compressed = zstd::encode_all(zero_data.as_slice(), 1).unwrap();
|
||||||
|
let region = DamageRegion {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
data: compressed,
|
||||||
|
};
|
||||||
|
apply_region(&mut framebuffer, stride, ®ion);
|
||||||
|
assert_eq!(framebuffer, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
//! length prefix for transmission over QUIC streams.
|
//! length prefix for transmission over QUIC streams.
|
||||||
|
|
||||||
pub mod codec;
|
pub mod codec;
|
||||||
|
pub mod encoding;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
|
||||||
/// Current protocol version. Incremented on breaking changes.
|
/// Current protocol version. Incremented on breaking changes.
|
||||||
|
|
|
||||||
82
crates/wayray-protocol/tests/encoding_roundtrip.rs
Normal file
82
crates/wayray-protocol/tests/encoding_roundtrip.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
//! Integration test: encode → compress → decompress → apply.
|
||||||
|
//!
|
||||||
|
//! Verifies the full XOR diff → zstd → XOR apply pipeline
|
||||||
|
//! reconstructs the original framebuffer correctly.
|
||||||
|
|
||||||
|
use wayray_protocol::encoding::{apply_region, encode_region, xor_diff};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_with_subregion() {
|
||||||
|
let width = 100u32;
|
||||||
|
let height = 100u32;
|
||||||
|
let stride = width as usize * 4;
|
||||||
|
let prev = vec![0u8; stride * height as usize];
|
||||||
|
let mut curr = prev.clone();
|
||||||
|
|
||||||
|
// Paint a 10x10 red rectangle at (5, 5)
|
||||||
|
for y in 5..15u32 {
|
||||||
|
for x in 5..15u32 {
|
||||||
|
let offset = y as usize * stride + x as usize * 4;
|
||||||
|
curr[offset..offset + 4].copy_from_slice(&[255, 0, 0, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff = xor_diff(&curr, &prev);
|
||||||
|
let region = encode_region(&diff, stride, 5, 5, 10, 10);
|
||||||
|
|
||||||
|
let mut reconstructed = prev;
|
||||||
|
apply_region(&mut reconstructed, stride, ®ion);
|
||||||
|
assert_eq!(reconstructed, curr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_unchanged_frame() {
|
||||||
|
let width = 50u32;
|
||||||
|
let height = 50u32;
|
||||||
|
let stride = width as usize * 4;
|
||||||
|
let frame = vec![128u8; stride * height as usize];
|
||||||
|
|
||||||
|
let diff = xor_diff(&frame, &frame);
|
||||||
|
let region = encode_region(&diff, stride, 0, 0, width, height);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
region.data.len() < 200,
|
||||||
|
"all-zero diff should compress well"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reconstructed = frame.clone();
|
||||||
|
apply_region(&mut reconstructed, stride, ®ion);
|
||||||
|
assert_eq!(reconstructed, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_multiple_regions() {
|
||||||
|
let width = 200u32;
|
||||||
|
let height = 200u32;
|
||||||
|
let stride = width as usize * 4;
|
||||||
|
let prev = vec![0u8; stride * height as usize];
|
||||||
|
let mut curr = prev.clone();
|
||||||
|
|
||||||
|
for y in 0..10u32 {
|
||||||
|
for x in 0..10u32 {
|
||||||
|
let offset = y as usize * stride + x as usize * 4;
|
||||||
|
curr[offset..offset + 4].copy_from_slice(&[255, 0, 0, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for y in 100..120u32 {
|
||||||
|
for x in 150..180u32 {
|
||||||
|
let offset = y as usize * stride + x as usize * 4;
|
||||||
|
curr[offset..offset + 4].copy_from_slice(&[0, 255, 0, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff = xor_diff(&curr, &prev);
|
||||||
|
let region1 = encode_region(&diff, stride, 0, 0, 10, 10);
|
||||||
|
let region2 = encode_region(&diff, stride, 150, 100, 30, 20);
|
||||||
|
|
||||||
|
let mut reconstructed = prev;
|
||||||
|
apply_region(&mut reconstructed, stride, ®ion1);
|
||||||
|
apply_region(&mut reconstructed, stride, ®ion2);
|
||||||
|
assert_eq!(reconstructed, curr);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
use smithay::{
|
use smithay::{
|
||||||
delegate_compositor, delegate_shm,
|
delegate_compositor, delegate_shm,
|
||||||
reexports::wayland_server::{
|
reexports::wayland_server::{Client, protocol::wl_surface::WlSurface},
|
||||||
Client,
|
|
||||||
protocol::wl_surface::WlSurface,
|
|
||||||
},
|
|
||||||
wayland::{
|
wayland::{
|
||||||
buffer::BufferHandler,
|
buffer::BufferHandler,
|
||||||
compositor::{CompositorClientState, CompositorHandler, CompositorState},
|
compositor::{CompositorClientState, CompositorHandler, CompositorState},
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
use smithay::{
|
use smithay::{delegate_output, output::Output, wayland::output::OutputHandler};
|
||||||
delegate_output,
|
|
||||||
output::Output,
|
|
||||||
wayland::output::OutputHandler,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::state::WayRay;
|
use crate::state::WayRay;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ use smithay::{
|
||||||
delegate_xdg_decoration, delegate_xdg_shell,
|
delegate_xdg_decoration, delegate_xdg_shell,
|
||||||
desktop::Window,
|
desktop::Window,
|
||||||
reexports::{
|
reexports::{
|
||||||
wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode
|
wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode,
|
||||||
as DecorationMode,
|
|
||||||
wayland_server::protocol::wl_seat,
|
wayland_server::protocol::wl_seat,
|
||||||
},
|
},
|
||||||
utils::Serial,
|
utils::Serial,
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ fn main() -> Result<()> {
|
||||||
info!("wrsrvd starting");
|
info!("wrsrvd starting");
|
||||||
|
|
||||||
// Create the Wayland display.
|
// Create the Wayland display.
|
||||||
let mut display = Display::<WayRay>::new()
|
let mut display =
|
||||||
.map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?;
|
Display::<WayRay>::new().map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?;
|
||||||
|
|
||||||
// Initialize the Winit backend (opens a window, creates a GlesRenderer).
|
// Initialize the Winit backend (opens a window, creates a GlesRenderer).
|
||||||
let (backend, winit_event_loop) = winit::init::<GlesRenderer>().map_err(|e| {
|
let (backend, winit_event_loop) = winit::init::<GlesRenderer>().map_err(|e| {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue