diff --git a/Cargo.lock b/Cargo.lock index 017254825..75f447af0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3048,7 +3048,6 @@ dependencies = [ "rgb", "semver", "smallvec", - "softbuffer", "tap", "time", "tracing", diff --git a/crates/ironrdp-core/src/write_buf.rs b/crates/ironrdp-core/src/write_buf.rs index 09023c008..8316439e0 100644 --- a/crates/ironrdp-core/src/write_buf.rs +++ b/crates/ironrdp-core/src/write_buf.rs @@ -62,6 +62,12 @@ impl WriteBuf { &self.inner[..self.filled] } + /// Returns a mutable reference to the filled portion of the buffer. + #[inline] + pub fn filled_mut(&mut self) -> &mut [u8] { + &mut self.inner[..self.filled] + } + /// Ensures initialized and unfilled portion of the buffer is big enough for `additional` more bytes. #[inline] pub fn initialize(&mut self, additional: usize) { diff --git a/crates/ironrdp-web/Cargo.toml b/crates/ironrdp-web/Cargo.toml index e5b75a501..8771c4b29 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -51,13 +51,19 @@ iron-remote-desktop.path = "../iron-remote-desktop" # WASM wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" -web-sys = { version = "0.3", features = ["HtmlCanvasElement", "Navigator", "Performance", "Window"] } +web-sys = { version = "0.3", features = [ + "CanvasRenderingContext2d", + "HtmlCanvasElement", + "ImageData", + "Navigator", + "Performance", + "Window", +] } js-sys = "0.3" gloo-net = { version = "0.7", default-features = false, features = ["websocket", "http", "io-util"] } gloo-timers = { version = "0.4", default-features = false, features = ["futures"] } # Rendering -softbuffer = { version = "0.4", default-features = false } png = "0.18" resize = { version = "0.8", features = ["std"], default-features = false } rgb = "0.8" diff --git a/crates/ironrdp-web/src/canvas.rs b/crates/ironrdp-web/src/canvas.rs index 96b9df50d..30ba5be78 100644 --- a/crates/ironrdp-web/src/canvas.rs +++ b/crates/ironrdp-web/src/canvas.rs @@ -1,93 +1,82 @@ use core::num::NonZeroU32; -use anyhow::Context as _; -use ironrdp::pdu::geometry::{InclusiveRectangle, Rectangle as _}; -use softbuffer::{NoDisplayHandle, NoWindowHandle}; -use web_sys::HtmlCanvasElement; - +#[cfg(target_arch = "wasm32")] +use anyhow::anyhow; +use ironrdp::pdu::geometry::InclusiveRectangle; +#[cfg(target_arch = "wasm32")] +use ironrdp::pdu::geometry::Rectangle as _; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::{Clamped, JsCast as _}; +#[cfg(target_arch = "wasm32")] +use web_sys::ImageData; +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}; + +/// Web render surface: blits each dirty region to the canvas with `put_image_data`. pub(crate) struct Canvas { - width: NonZeroU32, - surface: softbuffer::Surface, + canvas: HtmlCanvasElement, + ctx: CanvasRenderingContext2d, } impl Canvas { pub(crate) fn new(render_canvas: HtmlCanvasElement, width: NonZeroU32, height: NonZeroU32) -> anyhow::Result { render_canvas.set_width(width.get()); render_canvas.set_height(height.get()); + let ctx = context_2d(&render_canvas)?; - #[cfg(target_arch = "wasm32")] - let mut surface = { - use softbuffer::SurfaceExtWeb as _; - softbuffer::Surface::from_canvas(render_canvas).expect("surface") - }; - - #[cfg(not(target_arch = "wasm32"))] - let mut surface = { - fn stub(_: HtmlCanvasElement) -> softbuffer::Surface { - unimplemented!() - } - - stub(render_canvas) - }; - - surface.resize(width, height).expect("surface resize"); - - Ok(Self { width, surface }) + Ok(Self { + canvas: render_canvas, + ctx, + }) } + /// Resizes the backing store. Note: this also clears the canvas and resets 2D context state; + /// the cached `ctx` stays valid. pub(crate) fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) { - self.surface.resize(width, height).expect("surface resize"); - self.width = width; + self.canvas.set_width(width.get()); + self.canvas.set_height(height.get()); } - pub(crate) fn draw(&mut self, buffer: &[u8], region: InclusiveRectangle) -> anyhow::Result<()> { - let region_width = region.width(); - let region_height = region.height(); - - let mut src = buffer.chunks_exact(4).map(|pixel| { - let r = pixel[0]; - let g = pixel[1]; - let b = pixel[2]; - u32::from_be_bytes([0, r, g, b]) - }); - - let mut dst = self.surface.buffer_mut().expect("surface buffer"); + /// Blits a dirty region with `put_image_data`. Forces alpha opaque first: the framebuffer isn't + /// guaranteed opaque (zero-init columns, QOI-RGBA) and `put_image_data` stores alpha verbatim. + pub(crate) fn draw(&self, buffer: &mut [u8], region: InclusiveRectangle) -> anyhow::Result<()> { + for pixel in buffer.chunks_exact_mut(4) { + pixel[3] = 0xFF; + } + #[cfg(target_arch = "wasm32")] { - // Copy src into dst - - let region_top_usize = usize::from(region.top); - let region_height_usize = usize::from(region_height); - let region_left_usize = usize::from(region.left); - let region_width_usize = usize::from(region_width); - - for dst_row in dst - .chunks_exact_mut(usize::try_from(self.width.get()).context("canvas width")?) - .skip(region_top_usize) - .take(region_height_usize) - { - let src_row = src.by_ref().take(region_width_usize); - - dst_row - .iter_mut() - .skip(region_left_usize) - .take(region_width_usize) - .zip(src_row) - .for_each(|(dst, src)| *dst = src); - } + let image = ImageData::new_with_u8_clamped_array_and_sh( + Clamped(&*buffer), + u32::from(region.width()), + u32::from(region.height()), + ) + .map_err(|err| anyhow!("ImageData::new failed: {err:?}"))?; + self.ctx + .put_image_data(&image, f64::from(region.left), f64::from(region.top)) + .map_err(|err| anyhow!("put_image_data failed: {err:?}")) } + #[cfg(not(target_arch = "wasm32"))] + { + let _ = (&self.ctx, buffer, region); + unimplemented!("web canvas is only available on wasm32") + } + } +} - let damage_rect = softbuffer::Rect { - x: u32::from(region.left), - y: u32::from(region.top), - width: NonZeroU32::new(u32::from(region_width)) - .expect("per InclusiveRectangle invariants: 0 < region_width"), - height: NonZeroU32::new(u32::from(region_height)) - .expect("per InclusiveRectangle invariants: 0 < region_height"), - }; - - dst.present_with_damage(&[damage_rect]).expect("buffer present"); - - Ok(()) +/// Acquires the canvas 2D context (wasm only; panics on other targets). +fn context_2d(canvas: &HtmlCanvasElement) -> anyhow::Result { + #[cfg(target_arch = "wasm32")] + { + canvas + .get_context("2d") + .map_err(|err| anyhow!("get_context(\"2d\") failed: {err:?}"))? + .ok_or_else(|| anyhow!("canvas has no 2d context"))? + .dyn_into::() + .map_err(|_| anyhow!("2d context is not a CanvasRenderingContext2d")) + } + #[cfg(not(target_arch = "wasm32"))] + { + let _ = canvas; + unimplemented!("web canvas is only available on wasm32") } } diff --git a/crates/ironrdp-web/src/image.rs b/crates/ironrdp-web/src/image.rs index 13ac3fedb..2df63efcf 100644 --- a/crates/ironrdp-web/src/image.rs +++ b/crates/ironrdp-web/src/image.rs @@ -2,18 +2,29 @@ use ironrdp::pdu::geometry::{InclusiveRectangle, Rectangle as _}; use ironrdp::session::image::DecodedImage; - -pub(crate) fn extract_partial_image(image: &DecodedImage, region: InclusiveRectangle) -> (InclusiveRectangle, Vec) { +use ironrdp_core::WriteBuf; + +/// Copies the dirty `region` into `buffer` from its current cursor (clear it between regions). +/// The returned rect may be wider than `region`: the whole-rows path widens to full image width. +pub(crate) fn extract_partial_image( + image: &DecodedImage, + region: InclusiveRectangle, + buffer: &mut WriteBuf, +) -> InclusiveRectangle { // PERF: needs actual benchmark to find a better heuristic if region.height() > 64 || region.width() > 512 { - extract_whole_rows(image, region) + extract_whole_rows(image, region, buffer) } else { - extract_smallest_rectangle(image, region) + extract_smallest_rectangle(image, region, buffer) } } // Faster for low-height and smaller images -fn extract_smallest_rectangle(image: &DecodedImage, region: InclusiveRectangle) -> (InclusiveRectangle, Vec) { +fn extract_smallest_rectangle( + image: &DecodedImage, + region: InclusiveRectangle, + buffer: &mut WriteBuf, +) -> InclusiveRectangle { let pixel_size = usize::from(image.pixel_format().bytes_per_pixel()); let image_width = usize::from(image.width()); @@ -26,7 +37,7 @@ fn extract_smallest_rectangle(image: &DecodedImage, region: InclusiveRectangle) let region_stride = region_width * pixel_size; let dst_buf_size = region_width * region_height * pixel_size; - let mut dst = vec![0; dst_buf_size]; + let dst = buffer.unfilled_to(dst_buf_size); let src = image.data(); @@ -42,11 +53,13 @@ fn extract_smallest_rectangle(image: &DecodedImage, region: InclusiveRectangle) target_slice.copy_from_slice(src_slice); } - (region, dst) + buffer.advance(dst_buf_size); + + region } // Faster for high-height and bigger images -fn extract_whole_rows(image: &DecodedImage, region: InclusiveRectangle) -> (InclusiveRectangle, Vec) { +fn extract_whole_rows(image: &DecodedImage, region: InclusiveRectangle, buffer: &mut WriteBuf) -> InclusiveRectangle { let pixel_size = usize::from(image.pixel_format().bytes_per_pixel()); let image_width = usize::from(image.width()); @@ -59,15 +72,15 @@ fn extract_whole_rows(image: &DecodedImage, region: InclusiveRectangle) -> (Incl let src_begin = region_top * image_stride; let src_end = (region_bottom + 1) * image_stride; + let len = src_end - src_begin; - let dst = src[src_begin..src_end].to_vec(); + buffer.unfilled_to(len).copy_from_slice(&src[src_begin..src_end]); + buffer.advance(len); - let wider_region = InclusiveRectangle { + InclusiveRectangle { left: 0, top: region.top, right: image.width() - 1, bottom: region.bottom, - }; - - (wider_region, dst) + } } diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 618901370..5aa48d22e 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -649,6 +649,9 @@ impl iron_remote_desktop::Session for Session { let mut requested_resize = None; + // Reused across frames so per-region extraction doesn't allocate on every draw. + let mut draw_buffer = WriteBuf::new(); + let mut active_stage = ActiveStage::new(connection_result); // Timer interval for driving clipboard lock timeouts (5 second interval) @@ -867,9 +870,10 @@ impl iron_remote_desktop::Session for Session { .context("Send frame to writer task")?; } ActiveStageOutput::GraphicsUpdate(region) => { - // PERF: some copies and conversion could be optimized - let (region, buffer) = extract_partial_image(&image, region); - gui.draw(&buffer, region).context("draw updated region")?; + let region = extract_partial_image(&image, region, &mut draw_buffer); + gui.draw(draw_buffer.filled_mut(), region) + .context("draw updated region")?; + draw_buffer.clear(); } ActiveStageOutput::PointerDefault => { self.set_cursor_style(CursorStyle::Default)?; @@ -979,8 +983,6 @@ impl iron_remote_desktop::Session for Session { // We need to perform resize after receiving the Deactivate All PDU, because there may be frames // with the previous dimensions arriving between the resize request and this message. if let Some((width, height)) = requested_resize { - self.render_canvas.set_width(width.get()); - self.render_canvas.set_height(height.get()); gui.resize(width, height); requested_resize = None; }