Initial WebGL2 support. (#218)

Co-authored-by: MarkAnthonyM <37249494+MarkAnthonyM@users.noreply.github.com>
This commit is contained in:
Jay Oster 2021-11-16 11:37:56 -08:00 committed by GitHub
parent acd42495f2
commit b185ec32e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 475 additions and 60 deletions

View file

@ -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 }}

View file

@ -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"

View file

@ -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)

View file

@ -0,0 +1,30 @@
[package]
name = "minimal-web"
version = "0.1.0"
authors = ["Jay Oster <jay@kodewerx.org>"]
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"

View file

@ -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.

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style type="text/css">
body {
background-color: #000;
margin: 0;
overflow: hidden;
}
</style>
<title>Hello Pixels + Web</title>
</head>
<body>
<script type="module">
import init from "./minimal-web.js";
window.addEventListener("load", () => {
init();
});
</script>
</body>
</html>

View file

@ -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<dyn FnMut(_)>);
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);
}
}
}

BIN
img/minimal-web.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

11
justfile Normal file
View file

@ -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}}/

View file

@ -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<Pixels, Error> {
async fn build_impl(self) -> Result<Pixels, Error> {
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<Pixels, Error> {
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<Pixels, Error> {
self.build_impl().await
}
}
pub(crate) fn create_backing_texture(

View file

@ -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<W: HasRawWindowHandle>(
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<W: HasRawWindowHandle>(
width: u32,
height: u32,
surface_texture: SurfaceTexture<'_, W>,
) -> Result<Self, Error> {
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));