diff --git a/.cargo/config.toml b/.cargo/config.toml index a9f12423..19bc2ee1 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 d8fed1d5..f8616969 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 2965f150..f4d0c829 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 fce628d6..c25eaedc 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 1fd8ba21..26e1aa7c 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 b7064d3c..e1365c52 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 f999b6a9..d69becad 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 2550e249..a2606a77 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 920b4437..95bcfa72 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 c040a218..5858070d 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 8d47476c..5c6544ec 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 00000000..2bf7dede --- /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);