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",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2597,3 +2598,31 @@ name = "zmij"
|
|||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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
|
||||
postcard.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.
|
||||
|
||||
pub mod codec;
|
||||
pub mod encoding;
|
||||
pub mod messages;
|
||||
|
||||
/// 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::{
|
||||
delegate_compositor, delegate_shm,
|
||||
reexports::wayland_server::{
|
||||
Client,
|
||||
protocol::wl_surface::WlSurface,
|
||||
},
|
||||
reexports::wayland_server::{Client, protocol::wl_surface::WlSurface},
|
||||
wayland::{
|
||||
buffer::BufferHandler,
|
||||
compositor::{CompositorClientState, CompositorHandler, CompositorState},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
use smithay::{
|
||||
delegate_output,
|
||||
output::Output,
|
||||
wayland::output::OutputHandler,
|
||||
};
|
||||
use smithay::{delegate_output, output::Output, wayland::output::OutputHandler};
|
||||
|
||||
use crate::state::WayRay;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ use smithay::{
|
|||
delegate_xdg_decoration, delegate_xdg_shell,
|
||||
desktop::Window,
|
||||
reexports::{
|
||||
wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode
|
||||
as DecorationMode,
|
||||
wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode,
|
||||
wayland_server::protocol::wl_seat,
|
||||
},
|
||||
utils::Serial,
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ fn main() -> Result<()> {
|
|||
info!("wrsrvd starting");
|
||||
|
||||
// Create the Wayland display.
|
||||
let mut display = Display::<WayRay>::new()
|
||||
.map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?;
|
||||
let mut display =
|
||||
Display::<WayRay>::new().map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?;
|
||||
|
||||
// Initialize the Winit backend (opens a window, creates a GlesRenderer).
|
||||
let (backend, winit_event_loop) = winit::init::<GlesRenderer>().map_err(|e| {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue