From b185ec32e9ff1361712a286abae144f42c392684 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Tue, 16 Nov 2021 11:37:56 -0800 Subject: [PATCH] Initial WebGL2 support. (#218) Co-authored-by: MarkAnthonyM <37249494+MarkAnthonyM@users.noreply.github.com> --- .github/workflows/ci.yml | 81 ++++++++----- Cargo.toml | 4 +- README.md | 1 + examples/minimal-web/Cargo.toml | 30 +++++ examples/minimal-web/README.md | 42 +++++++ examples/minimal-web/index.html | 24 ++++ examples/minimal-web/src/main.rs | 190 +++++++++++++++++++++++++++++++ img/minimal-web.png | Bin 0 -> 10866 bytes justfile | 11 ++ src/builder.rs | 86 +++++++++++--- src/lib.rs | 66 ++++++++--- 11 files changed, 475 insertions(+), 60 deletions(-) create mode 100644 examples/minimal-web/Cargo.toml create mode 100644 examples/minimal-web/README.md create mode 100644 examples/minimal-web/index.html create mode 100644 examples/minimal-web/src/main.rs create mode 100644 img/minimal-web.png create mode 100644 justfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f88b58..797c7b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,33 +32,6 @@ jobs: with: command: check args: --all - tests: - name: Test - runs-on: ubuntu-latest - strategy: - matrix: - rust: - - stable - - beta - - 1.52.0 - steps: - - name: Checkout sources - uses: actions/checkout@v2 - - name: Update apt repos - run: sudo apt-get -y update - - name: Install dependencies - run: sudo apt -y install libsdl2-dev - - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ matrix.rust }} - override: true - - name: Cargo test - uses: actions-rs/cargo@v1 - with: - command: test - args: --all lints: name: Lints runs-on: ubuntu-latest @@ -91,3 +64,57 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} args: --all --tests -- -D warnings + tests: + name: Test + runs-on: ubuntu-latest + needs: [checks, lints] + strategy: + matrix: + rust: + - stable + - beta + - 1.52.0 + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Update apt repos + run: sudo apt-get -y update + - name: Install dependencies + run: sudo apt -y install libsdl2-dev + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - name: Cargo test + uses: actions-rs/cargo@v1 + with: + command: test + args: --all + wasm: + name: WASM + runs-on: ubuntu-latest + needs: [checks, lints] + strategy: + matrix: + example: + - minimal-web + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Update apt repos + run: sudo apt-get -y update + - name: Install dependencies + run: sudo apt -y install libsdl2-dev + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: wasm32-unknown-unknown + override: true + - name: Install tools + run: cargo install --locked wasm-bindgen-cli just + - name: Just build + run: just build ${{ matrix.example }} diff --git a/Cargo.toml b/Cargo.toml index 8da0284..75402d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,14 @@ include = [ [dependencies] bytemuck = "1.7" -pollster = "0.2" raw-window-handle = "0.3" thiserror = "1.0" ultraviolet = "0.8" wgpu = "0.11" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +pollster = "0.2" + [dev-dependencies] pixels-mocks = { path = "internals/pixels-mocks" } winit = "0.25" diff --git a/README.md b/README.md index 8ff94d9..6ba0b80 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The Minimum Supported Rust Version for `pixels` will always be made available in - [Custom Shader](./examples/custom-shader) - [Dear ImGui example with `winit`](./examples/imgui-winit) - [Egui example with `winit`](./examples/minimal-egui) +- [Minimal example for WebGL2](./examples/minimal-web) - [Minimal example with SDL2](./examples/minimal-sdl2) - [Minimal example with `winit`](./examples/minimal-winit) - [Minimal example with `fltk`](./examples/minimal-fltk) diff --git a/examples/minimal-web/Cargo.toml b/examples/minimal-web/Cargo.toml new file mode 100644 index 0000000..1b72dfb --- /dev/null +++ b/examples/minimal-web/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "minimal-web" +version = "0.1.0" +authors = ["Jay Oster "] +edition = "2018" +resolver = "2" +publish = false + +[features] +optimize = ["log/release_max_level_warn"] +web = ["wgpu/webgl", "winit/web-sys"] +default = ["optimize"] + +[dependencies] +log = "0.4" +pixels = { path = "../.." } +wgpu = "0.11" +winit = "0.25" +winit_input_helper = "0.10" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1" +console_log = "0.2" +wasm-bindgen = "0.2.78" +wasm-bindgen-futures = "0.4" +web-sys = "0.3" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +env_logger = "0.9" +pollster = "0.2" diff --git a/examples/minimal-web/README.md b/examples/minimal-web/README.md new file mode 100644 index 0000000..09a1be6 --- /dev/null +++ b/examples/minimal-web/README.md @@ -0,0 +1,42 @@ +# Hello Pixels + Web + +![Hello Pixels + Web](../../img/minimal-web.png) + +Minimal example for WebGL2. + +## Install build dependencies + +Install the WASM32 target and a few CLI tools: + +```bash +rustup target add wasm32-unknown-unknown +cargo install --locked wasm-bindgen-cli just miniserve +``` + +## Running on the Web + +Build the project and start a local server to host it: + +```bash +just serve minimal-web +``` + +Open http://localhost:8080/ in your browser to run the example. + +To build the project without serving it: + +```bash +just build minimal-web +``` + +The build files are stored in `./target/minimal-web/`. + +## Running on native targets + +```bash +cargo run --release --package minimal-web +``` + +## About + +This example is based on `minimal-winit`, demonstrating how to build your app for WebGL2 targets. diff --git a/examples/minimal-web/index.html b/examples/minimal-web/index.html new file mode 100644 index 0000000..d19bf55 --- /dev/null +++ b/examples/minimal-web/index.html @@ -0,0 +1,24 @@ + + + + + + + + Hello Pixels + Web + + + + + diff --git a/examples/minimal-web/src/main.rs b/examples/minimal-web/src/main.rs new file mode 100644 index 0000000..e5ae495 --- /dev/null +++ b/examples/minimal-web/src/main.rs @@ -0,0 +1,190 @@ +#![deny(clippy::all)] +#![forbid(unsafe_code)] + +use log::error; +use pixels::{Pixels, SurfaceTexture}; +use std::rc::Rc; +use winit::dpi::LogicalSize; +use winit::event::{Event, VirtualKeyCode}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; +use winit_input_helper::WinitInputHelper; + +const WIDTH: u32 = 320; +const HEIGHT: u32 = 240; +const BOX_SIZE: i16 = 64; + +/// Representation of the application state. In this example, a box will bounce around the screen. +struct World { + box_x: i16, + box_y: i16, + velocity_x: i16, + velocity_y: i16, +} + +fn main() { + #[cfg(target_arch = "wasm32")] + { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init_with_level(log::Level::Trace).expect("error initializing logger"); + + wasm_bindgen_futures::spawn_local(run()); + } + + #[cfg(not(target_arch = "wasm32"))] + { + env_logger::init(); + + pollster::block_on(run()); + } +} + +async fn run() { + let event_loop = EventLoop::new(); + let window = { + let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64); + WindowBuilder::new() + .with_title("Hello Pixels + Web") + .with_inner_size(size) + .with_min_inner_size(size) + .build(&event_loop) + .expect("WindowBuilder error") + }; + + let window = Rc::new(window); + + #[cfg(target_arch = "wasm32")] + { + use wasm_bindgen::JsCast; + use winit::platform::web::WindowExtWebSys; + + // Retrieve current width and height dimensions of browser client window + let get_window_size = || { + let client_window = web_sys::window().unwrap(); + LogicalSize::new( + client_window.inner_width().unwrap().as_f64().unwrap(), + client_window.inner_height().unwrap().as_f64().unwrap(), + ) + }; + + let window = Rc::clone(&window); + + // Initialize winit window with current dimensions of browser client + window.set_inner_size(get_window_size()); + + let client_window = web_sys::window().unwrap(); + + // Attach winit canvas to body element + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| { + body.append_child(&web_sys::Element::from(window.canvas())) + .ok() + }) + .expect("couldn't append canvas to document body"); + + // Listen for resize event on browser client. Adjust winit window dimensions + // on event trigger + let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_e: web_sys::Event| { + let size = get_window_size(); + window.set_inner_size(size) + }) as Box); + client_window + .add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); + } + + let mut input = WinitInputHelper::new(); + let mut pixels = { + let window_size = window.inner_size(); + let surface_texture = + SurfaceTexture::new(window_size.width, window_size.height, window.as_ref()); + Pixels::new_async(WIDTH, HEIGHT, surface_texture) + .await + .expect("Pixels error") + }; + let mut world = World::new(); + + event_loop.run(move |event, _, control_flow| { + // Draw the current frame + if let Event::RedrawRequested(_) = event { + world.draw(pixels.get_frame()); + if pixels + .render() + .map_err(|e| error!("pixels.render() failed: {}", e)) + .is_err() + { + *control_flow = ControlFlow::Exit; + return; + } + } + + // Handle input events + if input.update(&event) { + // Close events + if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { + *control_flow = ControlFlow::Exit; + return; + } + + // Resize the window + if let Some(size) = input.window_resized() { + pixels.resize_surface(size.width, size.height); + } + + // Update internal state and request a redraw + world.update(); + window.request_redraw(); + } + }); +} + +impl World { + /// Create a new `World` instance that can draw a moving box. + fn new() -> Self { + Self { + box_x: 24, + box_y: 16, + velocity_x: 1, + velocity_y: 1, + } + } + + /// Update the `World` internal state; bounce the box around the screen. + fn update(&mut self) { + if self.box_x <= 0 || self.box_x + BOX_SIZE > WIDTH as i16 { + self.velocity_x *= -1; + } + if self.box_y <= 0 || self.box_y + BOX_SIZE > HEIGHT as i16 { + self.velocity_y *= -1; + } + + self.box_x += self.velocity_x; + self.box_y += self.velocity_y; + } + + /// Draw the `World` state to the frame buffer. + /// + /// Assumes the default texture format: `wgpu::TextureFormat::Rgba8UnormSrgb` + fn draw(&self, frame: &mut [u8]) { + for (i, pixel) in frame.chunks_exact_mut(4).enumerate() { + let x = (i % WIDTH as usize) as i16; + let y = (i / WIDTH as usize) as i16; + + let inside_the_box = x >= self.box_x + && x < self.box_x + BOX_SIZE + && y >= self.box_y + && y < self.box_y + BOX_SIZE; + + let rgba = if inside_the_box { + [0x5e, 0x48, 0xe8, 0xff] + } else { + [0x48, 0xb2, 0xe8, 0xff] + }; + + pixel.copy_from_slice(&rgba); + } + } +} diff --git a/img/minimal-web.png b/img/minimal-web.png new file mode 100644 index 0000000000000000000000000000000000000000..27656e8944b9d755513aca77ae52915ae5e659f4 GIT binary patch literal 10866 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 literal 0 HcmV?d00001 diff --git a/justfile b/justfile new file mode 100644 index 0000000..c65d0fa --- /dev/null +++ b/justfile @@ -0,0 +1,11 @@ +serve package: (build package) + miniserve --index index.html ./target/{{package}}/ + +build package: + mkdir -p ./target/{{package}}/ + cp ./examples/{{package}}/index.html ./target/{{package}}/ + cargo build --release --package {{package}} --target wasm32-unknown-unknown --features web + wasm-bindgen --target web --no-typescript --out-dir ./target/{{package}}/ ./target/wasm32-unknown-unknown/release/{{package}}.wasm + +clean package: + rm -rf ./target/{{package}}/ diff --git a/src/builder.rs b/src/builder.rs index aac5a50..5756337 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -23,12 +23,14 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W> /// # Examples /// /// ```no_run + /// use pixels::wgpu::{PowerPreference, RequestAdapterOptions}; + /// /// # use pixels::PixelsBuilder; /// # let window = pixels_mocks::Rwh; - /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &window); + /// # let surface_texture = pixels::SurfaceTexture::new(256, 240, &window); /// let mut pixels = PixelsBuilder::new(256, 240, surface_texture) - /// .request_adapter_options(wgpu::RequestAdapterOptions { - /// power_preference: wgpu::PowerPreference::HighPerformance, + /// .request_adapter_options(RequestAdapterOptions { + /// power_preference: PowerPreference::HighPerformance, /// force_fallback_adapter: false, /// compatible_surface: None, /// }) @@ -47,7 +49,17 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W> Self { request_adapter_options: None, device_descriptor: None, - backend: wgpu::util::backend_bits_from_env().unwrap_or(wgpu::Backends::PRIMARY), + backend: wgpu::util::backend_bits_from_env().unwrap_or({ + #[cfg(not(target_arch = "wasm32"))] + { + wgpu::Backends::PRIMARY + } + + #[cfg(target_arch = "wasm32")] + { + wgpu::Backends::all() + } + }), width, height, _pixel_aspect_ratio: 1.0, @@ -166,20 +178,24 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W> /// Create a pixel buffer from the options builder. /// + /// This is the private implementation shared by [`PixelsBuilder::build`] and + /// [`PixelsBuilder::build_async`]. + /// /// # Errors /// /// Returns an error when a [`wgpu::Adapter`] cannot be found. - pub fn build(self) -> Result { + async fn build_impl(self) -> Result { let instance = wgpu::Instance::new(self.backend); // TODO: Use `options.pixel_aspect_ratio` to stretch the scaled texture let surface = unsafe { instance.create_surface(self.surface_texture.window) }; let compatible_surface = Some(&surface); let request_adapter_options = &self.request_adapter_options; - let adapter = - wgpu::util::initialize_adapter_from_env(&instance, self.backend).or_else(|| { - let future = - instance.request_adapter(&request_adapter_options.as_ref().map_or_else( + let adapter = match wgpu::util::initialize_adapter_from_env(&instance, self.backend) { + Some(adapter) => Some(adapter), + None => { + instance + .request_adapter(&request_adapter_options.as_ref().map_or_else( || wgpu::RequestAdapterOptions { compatible_surface, force_fallback_adapter: false, @@ -188,13 +204,14 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W> }, |rao| wgpu::RequestAdapterOptions { compatible_surface: rao.compatible_surface.or(compatible_surface), - force_fallback_adapter: false, + force_fallback_adapter: rao.force_fallback_adapter, power_preference: rao.power_preference, }, - )); + )) + .await + } + }; - pollster::block_on(future) - }); let adapter = adapter.ok_or(Error::AdapterNotFound)?; let device_descriptor = self @@ -204,7 +221,9 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W> ..wgpu::DeviceDescriptor::default() }); - let (device, queue) = pollster::block_on(adapter.request_device(&device_descriptor, None)) + let (device, queue) = adapter + .request_device(&device_descriptor, None) + .await .map_err(Error::DeviceNotFound)?; let present_mode = self.present_mode; @@ -256,6 +275,45 @@ impl<'req, 'dev, 'win, W: HasRawWindowHandle> PixelsBuilder<'req, 'dev, 'win, W> Ok(pixels) } + + /// Create a pixel buffer from the options builder. + /// + /// This method blocks the current thread, making it unusable on Web targets. Use + /// [`PixelsBuilder::build_async`] for a non-blocking alternative. + /// + /// # Errors + /// + /// Returns an error when a [`wgpu::Adapter`] or [`wgpu::Device`] cannot be found. + #[cfg(not(target_arch = "wasm32"))] + pub fn build(self) -> Result { + pollster::block_on(self.build_impl()) + } + + /// Create a pixel buffer from the options builder without blocking the current thread. + /// + /// # Examples + /// + /// ```no_run + /// use pixels::wgpu::{Backends, DeviceDescriptor, Limits}; + /// + /// # async fn test() -> Result<(), pixels::Error> { + /// # use pixels::PixelsBuilder; + /// # let window = pixels_mocks::Rwh; + /// # let surface_texture = pixels::SurfaceTexture::new(256, 240, &window); + /// let mut pixels = PixelsBuilder::new(256, 240, surface_texture) + /// .enable_vsync(false) + /// .build_async() + /// .await?; + /// # Ok::<(), pixels::Error>(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns an error when a [`wgpu::Adapter`] or [`wgpu::Device`] cannot be found. + pub async fn build_async(self) -> Result { + self.build_impl().await + } } pub(crate) fn create_backing_texture( diff --git a/src/lib.rs b/src/lib.rs index 2bffa12..ebb8c81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -142,10 +142,7 @@ impl<'win, W: HasRawWindowHandle> SurfaceTexture<'win, W> { /// let window = Window::new(&event_loop).unwrap(); /// let size = window.inner_size(); /// - /// let width = size.width; - /// let height = size.height; - /// - /// let surface_texture = SurfaceTexture::new(width, height, &window); + /// let surface_texture = SurfaceTexture::new(size.width, size.height, &window); /// # Ok::<(), pixels::Error>(()) /// ``` /// @@ -173,12 +170,15 @@ impl Pixels { /// `320x240`, `640x480`, `960x720`, etc. without adding a border because these are exactly /// 1x, 2x, and 3x scales, respectively. /// + /// This method blocks the current thread, making it unusable on Web targets. Use + /// [`Pixels::new_async`] for a non-blocking alternative. + /// /// # Examples /// /// ```no_run /// # use pixels::Pixels; /// # let window = pixels_mocks::Rwh; - /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &window); + /// # let surface_texture = pixels::SurfaceTexture::new(320, 240, &window); /// let mut pixels = Pixels::new(320, 240, surface_texture)?; /// # Ok::<(), pixels::Error>(()) /// ``` @@ -190,6 +190,7 @@ impl Pixels { /// # Panics /// /// Panics when `width` or `height` are 0. + #[cfg(not(target_arch = "wasm32"))] pub fn new( width: u32, height: u32, @@ -198,6 +199,39 @@ impl Pixels { PixelsBuilder::new(width, height, surface_texture).build() } + /// Asynchronously create a pixel buffer instance with default options. + /// + /// See [`Pixels::new`] for more information. + /// + /// # Examples + /// + /// ```no_run + /// # async fn test() -> Result<(), pixels::Error> { + /// # use pixels::Pixels; + /// # let window = pixels_mocks::Rwh; + /// # let surface_texture = pixels::SurfaceTexture::new(320, 240, &window); + /// let mut pixels = Pixels::new_async(320, 240, surface_texture).await?; + /// # Ok::<(), pixels::Error>(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns an error when a [`wgpu::Adapter`] cannot be found. + /// + /// # Panics + /// + /// Panics when `width` or `height` are 0. + pub async fn new_async( + width: u32, + height: u32, + surface_texture: SurfaceTexture<'_, W>, + ) -> Result { + PixelsBuilder::new(width, height, surface_texture) + .build_async() + .await + } + /// Resize the pixel buffer and zero its contents. /// /// This does not resize the surface upon which the pixel buffer texture is rendered. Use @@ -291,7 +325,7 @@ impl Pixels { /// ```no_run /// # use pixels::Pixels; /// # let window = pixels_mocks::Rwh; - /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &window); + /// # let surface_texture = pixels::SurfaceTexture::new(320, 240, &window); /// let mut pixels = Pixels::new(320, 240, surface_texture)?; /// /// // Clear the pixel buffer @@ -336,7 +370,7 @@ impl Pixels { /// ```no_run /// # use pixels::Pixels; /// # let window = pixels_mocks::Rwh; - /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &window); + /// # let surface_texture = pixels::SurfaceTexture::new(320, 240, &window); /// let mut pixels = Pixels::new(320, 240, surface_texture)?; /// /// // Clear the pixel buffer @@ -449,16 +483,15 @@ impl Pixels { /// the screen, using isize instead of usize. /// /// ```no_run + /// use winit::dpi::PhysicalPosition; + /// /// # use pixels::Pixels; /// # let window = pixels_mocks::Rwh; - /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &window); - /// const WIDTH: u32 = 320; - /// const HEIGHT: u32 = 240; - /// - /// let mut pixels = Pixels::new(WIDTH, HEIGHT, surface_texture)?; + /// # let surface_texture = pixels::SurfaceTexture::new(320, 240, &window); + /// let mut pixels = Pixels::new(320, 240, surface_texture)?; /// /// // A cursor position in physical units - /// let cursor_position: (f32, f32) = winit::dpi::PhysicalPosition::new(0.0, 0.0).into(); + /// let cursor_position: (f32, f32) = PhysicalPosition::new(0.0, 0.0).into(); /// /// // Convert it to a pixel location /// let pixel_position: (usize, usize) = pixels.window_pos_to_pixel(cursor_position) @@ -509,11 +542,8 @@ impl Pixels { /// ```no_run /// # use pixels::Pixels; /// # let window = pixels_mocks::Rwh; - /// # let surface_texture = pixels::SurfaceTexture::new(1024, 768, &window); - /// const WIDTH: u32 = 320; - /// const HEIGHT: u32 = 240; - /// - /// let mut pixels = Pixels::new(WIDTH, HEIGHT, surface_texture)?; + /// # let surface_texture = pixels::SurfaceTexture::new(320, 240, &window); + /// let mut pixels = Pixels::new(320, 240, surface_texture)?; /// /// let pixel_pos = pixels.clamp_pixel_pos((-19, 20)); /// assert_eq!(pixel_pos, (0, 20));