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..4657b86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: pull_request: push: - branches: [main] + branches: [master] jobs: Check_Formatting: @@ -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/CHANGELOG.md b/CHANGELOG.md index 12191a3..96400b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ * On MacOS, the contents scale is updated when set_buffer() is called, to adapt when the window is on a new screen. +# 0.2.1 + +* Bump `windows-sys` to 0.48 + # 0.2.0 * Add support for Redox/Orbital. 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 f47e639..647e51a 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 8e538f2..700f87b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -43,6 +43,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 a662f74..0c39dd1 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> { @@ -337,6 +346,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. Call [`Buffer::age`] to determine this. diff --git a/src/orbital.rs b/src/orbital.rs index 7ad792b..407f3b7 100644 --- a/src/orbital.rs +++ b/src/orbital.rs @@ -150,6 +150,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 ce20b1e..05b409f 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -126,7 +126,6 @@ impl WaylandImpl { }; let age = self.buffers.as_mut().unwrap().1.age; - Ok(BufferImpl { stack: util::BorrowStack::new(self, |buffer| { Ok(unsafe { buffer.buffers.as_mut().unwrap().1.mapped_mut() }) @@ -135,6 +134,11 @@ impl WaylandImpl { }) } + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } + fn present_with_damage(&mut self, damage: &[Rect]) -> Result<(), SoftBufferError> { let _ = self .display diff --git a/src/web.rs b/src/web.rs index bdb7781..96b9f03 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 }) } @@ -164,11 +164,36 @@ impl WebImpl { Ok(()) } + + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + let (width, height) = self + .size + .expect("Must set size of surface before calling `fetch()`"); + + let image_data = self + .ctx + .get_image_data(0., 0., width.get().into(), height.get().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; } diff --git a/src/win32.rs b/src/win32.rs index 6607c98..3b99bf7 100644 --- a/src/win32.rs +++ b/src/win32.rs @@ -228,6 +228,34 @@ impl Win32Impl { Ok(()) } + + /// 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 3a9b518..f9edd44 100644 --- a/src/x11.rs +++ b/src/x11.rs @@ -109,6 +109,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, @@ -183,11 +186,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 @@ -206,9 +224,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 { @@ -227,6 +259,7 @@ impl X11Impl { window, gc, depth: geometry_reply.depth, + visual_id, buffer, buffer_presented: false, size: None, @@ -278,6 +311,43 @@ 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); + + let (width, height) = self + .size + .expect("Must set size of surface before calling `fetch()`"); + + // 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, + width.get(), + height.get(), + 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);