From 44248477be118d3e5fe430a1c2b1589b1fd734b3 Mon Sep 17 00:00:00 2001 From: John Nunley Date: Thu, 1 Jun 2023 20:09:30 -0700 Subject: [PATCH] feat: Add a function for retrieving the window contents This function is useful for testing the window contents in certain cases. In addition, this means that we can now have reliable tests for softbuffer's actual functionality. Signed-off-by: John Nunley Co-authored-by: dAxpeDDa --- .cargo/config.toml | 3 ++ .github/workflows/ci.yml | 31 +++++++++----- Cargo.toml | 9 +++++ src/cg.rs | 5 +++ src/error.rs | 3 ++ src/lib.rs | 21 ++++++++++ src/orbital.rs | 5 +++ src/wayland/mod.rs | 5 +++ src/web.rs | 32 +++++++++++++-- src/win32.rs | 28 +++++++++++++ src/x11.rs | 82 ++++++++++++++++++++++++++++++++++---- tests/present_and_fetch.rs | 56 ++++++++++++++++++++++++++ 12 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 tests/present_and_fetch.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index a9f1242..19bc2ee 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,5 @@ [alias] run-wasm = ["run", "--release", "--package", "run-wasm", "--"] + +[target.wasm32-unknown-unknown] +runner = "wasm-bindgen-test-runner" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8fed1d..f861696 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,12 +47,10 @@ jobs: - { target: x86_64-unknown-freebsd, os: ubuntu-latest, } - { target: x86_64-unknown-netbsd, os: ubuntu-latest, } - { target: x86_64-apple-darwin, os: macos-latest, } - # We're using Windows rather than Ubuntu to run the wasm tests because caching cargo-web - # doesn't currently work on Linux. - - { target: wasm32-unknown-unknown, os: windows-latest, } + - { target: wasm32-unknown-unknown, os: ubuntu-latest, } include: - rust_version: nightly - platform: { target: wasm32-unknown-unknown, os: windows-latest, options: "-Zbuild-std=panic_abort,std", rustflags: "-Ctarget-feature=+atomics,+bulk-memory" } + platform: { target: wasm32-unknown-unknown, os: ubuntu-latest, options: "-Zbuild-std=panic_abort,std", rustflags: "-Ctarget-feature=+atomics,+bulk-memory" } env: RUST_BACKTRACE: 1 @@ -67,12 +65,10 @@ jobs: steps: - uses: actions/checkout@v3 - # Used to cache cargo-web - - name: Cache cargo folder - uses: actions/cache@v3 + - uses: taiki-e/install-action@v2 + if: matrix.platform.target == 'wasm32-unknown-unknown' with: - path: ~/.cargo - key: ${{ matrix.platform.target }}-cargo-${{ matrix.rust_version }} + tool: wasm-bindgen-cli - uses: hecrj/setup-rust-action@v1 with: @@ -102,12 +98,25 @@ jobs: shell: bash if: > !((matrix.platform.os == 'ubuntu-latest') && contains(matrix.platform.target, 'i686')) && - !contains(matrix.platform.target, 'wasm32') && !contains(matrix.platform.target, 'redox') && !contains(matrix.platform.target, 'freebsd') && - !contains(matrix.platform.target, 'netbsd') + !contains(matrix.platform.target, 'netbsd') && + !contains(matrix.platform.target, 'linux') run: cargo $CMD test --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + # TODO: We should also be using Wayland for testing here. + - name: Run tests using Xvfb + shell: bash + if: > + !((matrix.platform.os == 'ubuntu-latest') && contains(matrix.platform.target, 'i686')) && + !contains(matrix.platform.target, 'redox') && + !contains(matrix.platform.target, 'freebsd') && + !contains(matrix.platform.target, 'netbsd') && + contains(matrix.platform.target, 'linux') && + !contains(matrix.platform.options, '--no-default-features') && + !contains(matrix.platform.features, 'wayland') + run: xvfb-run cargo $CMD test --verbose --target ${{ matrix.platform.target }} $OPTIONS --features $FEATURES + - name: Lint with clippy shell: bash if: > diff --git a/Cargo.toml b/Cargo.toml index 2965f15..f4d0c82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ cfg_aliases = "0.1.1" criterion = { version = "0.4.0", default-features = false, features = ["cargo_bench_support"] } instant = "0.1.12" winit = "0.28.1" +winit-test = "0.1.0" [dev-dependencies.image] version = "0.24.6" @@ -81,11 +82,19 @@ features = ["jpeg"] image = "0.24.6" rayon = "1.5.1" +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3" + [workspace] members = [ "run-wasm", ] +[[test]] +name = "present_and_fetch" +path = "tests/present_and_fetch.rs" +harness = false + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/src/cg.rs b/src/cg.rs index fce628d..c25eaed 100644 --- a/src/cg.rs +++ b/src/cg.rs @@ -69,6 +69,11 @@ impl CGImpl { imp: self, }) } + + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } } pub struct BufferImpl<'a> { diff --git a/src/error.rs b/src/error.rs index 1fd8ba2..26e1aa7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -38,6 +38,9 @@ pub enum SoftBufferError { #[error("Platform error")] PlatformError(Option, Option>), + + #[error("This function is unimplemented on this platform")] + Unimplemented, } /// Convenient wrapper to cast errors into SoftBufferError. diff --git a/src/lib.rs b/src/lib.rs index b7064d3..e1365c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,15 @@ macro_rules! make_dispatch { )* } } + + pub fn fetch(&mut self) -> Result, SoftBufferError> { + match self { + $( + $(#[$attr])* + Self::$name(inner) => inner.fetch(), + )* + } + } } enum BufferDispatch<'a> { @@ -306,6 +315,18 @@ impl Surface { self.surface_impl.resize(width, height) } + /// Copies the window contents into a buffer. + /// + /// ## Platform Dependent Behavior + /// + /// - On X11, the window must be visible. + /// - On macOS, Redox and Wayland, this function is unimplemented. + /// - On Web, this will fail if the content was supplied by + /// a different origin depending on the sites CORS rules. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + self.surface_impl.fetch() + } + /// Return a [`Buffer`] that the next frame should be rendered into. The size must /// be set with [`Surface::resize`] first. The initial contents of the buffer may be zeroed, or /// may contain a previous frame. diff --git a/src/orbital.rs b/src/orbital.rs index f999b6a..d69beca 100644 --- a/src/orbital.rs +++ b/src/orbital.rs @@ -143,6 +143,11 @@ impl OrbitalImpl { // Tell orbital to show the latest window data syscall::fsync(self.window_fd()).expect("failed to sync orbital window"); } + + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } } enum Pixels { diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 2550e24..a2606a7 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -129,6 +129,11 @@ impl WaylandImpl { Ok(unsafe { buffer.buffers.as_mut().unwrap().1.mapped_mut() }) })?)) } + + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } } pub struct BufferImpl<'a>(util::BorrowStack<'a, WaylandImpl, [u32]>); diff --git a/src/web.rs b/src/web.rs index 920b443..95bcfa7 100644 --- a/src/web.rs +++ b/src/web.rs @@ -24,9 +24,9 @@ pub struct WebDisplayImpl { impl WebDisplayImpl { pub(super) fn new() -> Result { let document = web_sys::window() - .swbuf_err("`window` is not present in this runtime")? + .swbuf_err("`Window` is not present in this runtime")? .document() - .swbuf_err("`document` is not present in this runtime")?; + .swbuf_err("`Document` is not present in this runtime")?; Ok(Self { document }) } @@ -44,6 +44,9 @@ pub struct WebImpl { /// The current width of the canvas. width: u32, + + /// The current height of the canvas. + height: u32, } impl WebImpl { @@ -76,6 +79,7 @@ impl WebImpl { ctx, buffer: Vec::new(), width: 0, + height: 0, }) } @@ -92,6 +96,7 @@ impl WebImpl { self.canvas.set_width(width); self.canvas.set_height(height); self.width = width; + self.height = height; Ok(()) } @@ -99,11 +104,32 @@ impl WebImpl { pub(crate) fn buffer_mut(&mut self) -> Result { Ok(BufferImpl { imp: self }) } + + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + let image_data = self + .ctx + .get_image_data(0., 0., self.width.into(), self.height.into()) + .ok() + // TODO: Can also error if width or height are 0. + .swbuf_err("`Canvas` contains pixels from a different origin")?; + + Ok(image_data + .data() + .0 + .chunks_exact(4) + .map(|chunk| u32::from_be_bytes([0, chunk[0], chunk[1], chunk[2]])) + .collect()) + } } /// Extension methods for the Wasm target on [`Surface`](crate::Surface). pub trait SurfaceExtWeb: Sized { /// Creates a new instance of this struct, using the provided [`HtmlCanvasElement`]. + /// + /// # Errors + /// - If the canvas was already controlled by an `OffscreenCanvas`. + /// - If a another context then "2d" was already created for this canvas. fn from_canvas(canvas: HtmlCanvasElement) -> Result; } @@ -171,7 +197,7 @@ impl<'a> BufferImpl<'a> { let image_data = result.unwrap(); // This can only throw an error if `data` is detached, which is impossible. - self.imp.ctx.put_image_data(&image_data, 0.0, 0.0).unwrap(); + self.imp.ctx.put_image_data(&image_data, 0., 0.).unwrap(); Ok(()) } diff --git a/src/win32.rs b/src/win32.rs index c040a21..5858070 100644 --- a/src/win32.rs +++ b/src/win32.rs @@ -202,6 +202,34 @@ impl Win32Impl { Ok(BufferImpl(self)) } + + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + let buffer = self.buffer.as_ref().unwrap(); + let temp_buffer = Buffer::new(self.dc, buffer.width, buffer.height); + + // Just go the other way. + unsafe { + Gdi::BitBlt( + temp_buffer.dc, + 0, + 0, + temp_buffer.width.get(), + temp_buffer.height.get(), + self.dc, + 0, + 0, + Gdi::SRCCOPY, + ); + } + + // Flush the operation so that it happens immediately. + unsafe { + Gdi::GdiFlush(); + } + + Ok(temp_buffer.pixels().to_vec()) + } } pub struct BufferImpl<'a>(&'a mut Win32Impl); diff --git a/src/x11.rs b/src/x11.rs index 8d47476..5c6544e 100644 --- a/src/x11.rs +++ b/src/x11.rs @@ -104,6 +104,9 @@ pub struct X11Impl { /// The depth (bits per pixel) of the drawing context. depth: u8, + /// The visual ID of the drawing context. + visual_id: u32, + /// The buffer we draw to. buffer: Buffer, @@ -178,11 +181,26 @@ impl X11Impl { let window = window_handle.window; - // Run in parallel: start getting the window depth. - let geometry_token = display - .connection - .get_geometry(window) - .swbuf_err("Failed to send geometry request")?; + // Run in parallel: start getting the window depth and (if necessary) visual. + let display2 = display.clone(); + let tokens = { + let geometry_token = display2 + .connection + .get_geometry(window) + .swbuf_err("Failed to send geometry request")?; + let window_attrs_token = if window_handle.visual_id == 0 { + Some( + display2 + .connection + .get_window_attributes(window) + .swbuf_err("Failed to send window attributes request")?, + ) + } else { + None + }; + + (geometry_token, window_attrs_token) + }; // Create a new graphics context to draw to. let gc = display @@ -201,9 +219,23 @@ impl X11Impl { .swbuf_err("Failed to create GC")?; // Finish getting the depth of the window. - let geometry_reply = geometry_token - .reply() - .swbuf_err("Failed to get geometry reply")?; + let (geometry_reply, visual_id) = { + let (geometry_token, window_attrs_token) = tokens; + let geometry_reply = geometry_token + .reply() + .swbuf_err("Failed to get geometry reply")?; + let visual_id = match window_attrs_token { + None => window_handle.visual_id, + Some(window_attrs) => { + window_attrs + .reply() + .swbuf_err("Failed to get window attributes reply")? + .visual + } + }; + + (geometry_reply, visual_id) + }; // See if SHM is available. let buffer = if display.is_shm_available { @@ -222,6 +254,7 @@ impl X11Impl { window, gc, depth: geometry_reply.depth, + visual_id, buffer, width: 0, height: 0, @@ -277,6 +310,39 @@ impl X11Impl { // We can now safely call `buffer_mut` on the buffer. Ok(BufferImpl(self)) } + + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + log::trace!("fetch: window={:X}", self.window); + + // TODO: Is it worth it to do SHM here? Probably not. + let reply = self + .display + .connection + .get_image( + xproto::ImageFormat::Z_PIXMAP, + self.window, + 0, + 0, + self.width, + self.height, + u32::MAX, + ) + .swbuf_err("Failed to send image fetching request")? + .reply() + .swbuf_err("Failed to fetch image from window")?; + + if reply.depth == self.depth && reply.visual == self.visual_id { + let mut out = vec![0u32; reply.data.len() / 4]; + bytemuck::cast_slice_mut::(&mut out).copy_from_slice(&reply.data); + Ok(out) + } else { + Err(SoftBufferError::PlatformError( + Some("Mismatch between reply and window data".into()), + None, + )) + } + } } pub struct BufferImpl<'a>(&'a mut X11Impl); diff --git a/tests/present_and_fetch.rs b/tests/present_and_fetch.rs new file mode 100644 index 0000000..2bf7ded --- /dev/null +++ b/tests/present_and_fetch.rs @@ -0,0 +1,56 @@ +use softbuffer::{Context, Surface}; +use std::num::NonZeroU32; +use winit::event_loop::EventLoopWindowTarget; + +fn all_red(elwt: &EventLoopWindowTarget<()>) { + let window = winit::window::WindowBuilder::new() + .with_title("all_red") + .build(elwt) + .unwrap(); + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + web_sys::window() + .unwrap() + .document() + .unwrap() + .body() + .unwrap() + .append_child(&window.canvas()) + .unwrap(); + } + + // winit does not wait for the window to be mapped... sigh + #[cfg(not(target_arch = "wasm32"))] + std::thread::sleep(std::time::Duration::from_millis(1)); + + let context = unsafe { Context::new(elwt) }.unwrap(); + let mut surface = unsafe { Surface::new(&context, &window) }.unwrap(); + let size = window.inner_size(); + + // Set the size of the surface to the size of the window. + surface + .resize( + NonZeroU32::new(size.width).unwrap(), + NonZeroU32::new(size.height).unwrap(), + ) + .unwrap(); + + // Set all pixels to red. + let mut buffer = surface.buffer_mut().unwrap(); + buffer.fill(0x00FF0000); + buffer.present().unwrap(); + + // Check that all pixels are red. + let screen_contents = match surface.fetch() { + Err(softbuffer::SoftBufferError::Unimplemented) => return, + cont => cont.unwrap(), + }; + for pixel in screen_contents.iter() { + assert_eq!(*pixel, 0x00FF0000); + } +} + +winit_test::main!(all_red);