diff --git a/Cargo.lock b/Cargo.lock index dfc2f1a901c..fff14bdbe06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,6 +1840,7 @@ dependencies = [ "gpu-alloc", "gpu-descriptor", "inplace_it", + "js-sys", "khronos-egl", "libloading 0.7.0", "log", @@ -1851,6 +1852,8 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "thiserror", + "wasm-bindgen", + "web-sys", "wgpu-types", "winapi", "winit", diff --git a/run-wasm-example.sh b/run-wasm-example.sh new file mode 100755 index 00000000000..4032616353a --- /dev/null +++ b/run-wasm-example.sh @@ -0,0 +1,34 @@ +#!/bin/env bash + +set -e + +echo "Compiling..." +cargo build --example $1 --target wasm32-unknown-unknown --features webgl + +echo "Generating bindings..." +mkdir -p target/wasm-examples/$1 +wasm-bindgen --target web --out-dir target/wasm-examples/$1 target/wasm32-unknown-unknown/debug/examples/$1.wasm +cp wasm-resources/index.template.html target/wasm-examples/$1/index.html +sed -i "s/{{example}}/$1/g" target/wasm-examples/$1/index.html + +# Find a serving tool to host the example +SERVE_CMD="" +SERVE_ARGS="" +if which basic-http-server; then + SERVE_CMD="basic-http-server" + SERVE_ARGS="target/wasm-examples/$1 -a 127.0.0.1:1234" +elif which miniserve && python3 -m http.server --help > /dev/null; then + SERVE_CMD="miniserve" + SERVE_ARGS="target/wasm-examples/$1 -p 1234 --index index.html" +elif python3 -m http.server --help > /dev/null; then + SERVE_CMD="python3" + SERVE_ARGS="-m http.server --directory target/wasm-examples/$1 1234" +fi + +# Exit if we couldn't find a tool to serve the example with +if [ "$SERVE_CMD" = "" ]; then + echo "Couldn't find a utility to use to serve the example web page. You can serve the `target/wasm-examples/$1` folder yourself using any simple static http file server." +fi + +echo "Serving example with $SERVE_CMD at http://localhost:1234" +$SERVE_CMD $SERVE_ARGS \ No newline at end of file diff --git a/wasm-resources/README.md b/wasm-resources/README.md new file mode 100644 index 00000000000..8b2b328fb74 --- /dev/null +++ b/wasm-resources/README.md @@ -0,0 +1,3 @@ +# WASM Resources + +This directory contains resources used when building the WGPU examples for web. diff --git a/wasm-resources/index.template.html b/wasm-resources/index.template.html new file mode 100644 index 00000000000..155072d10e2 --- /dev/null +++ b/wasm-resources/index.template.html @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/wgpu-core/Cargo.toml b/wgpu-core/Cargo.toml index 6810e90d1e3..05b8ef0b64d 100644 --- a/wgpu-core/Cargo.toml +++ b/wgpu-core/Cargo.toml @@ -19,6 +19,7 @@ trace = ["ron", "serde", "wgt/trace", "arrayvec/serde", "naga/serialize"] replay = ["serde", "wgt/replay", "arrayvec/serde", "naga/deserialize"] # Enable serializable compute/render passes, and bundle encoders. serial-pass = ["serde", "wgt/serde", "arrayvec/serde"] +webgl = ["hal/gles"] [dependencies] arrayvec = "0.7" diff --git a/wgpu-core/build.rs b/wgpu-core/build.rs index 38dad2eac7f..2dabdafc11a 100644 --- a/wgpu-core/build.rs +++ b/wgpu-core/build.rs @@ -11,6 +11,11 @@ fn main() { metal: { all(not(wasm), apple) }, dx12: { all(not(wasm), windows) }, dx11: { all(false, not(wasm), windows) }, - gl: { all(not(wasm), unix_wo_apple) }, + gl: { + any( + all(not(wasm), unix_wo_apple), + all(wasm, feature = "webgl") + ) + }, } } diff --git a/wgpu-core/src/device/mod.rs b/wgpu-core/src/device/mod.rs index 2d3a6f159a9..2123883f3e7 100644 --- a/wgpu-core/src/device/mod.rs +++ b/wgpu-core/src/device/mod.rs @@ -4486,6 +4486,10 @@ impl Global { { self.poll_devices::(force_wait, &mut callbacks)?; } + #[cfg(gl)] + { + self.poll_devices::(force_wait, &mut callbacks)?; + } fire_map_callbacks(callbacks); diff --git a/wgpu-core/src/lib.rs b/wgpu-core/src/lib.rs index 968eff3e667..5c085ec4472 100644 --- a/wgpu-core/src/lib.rs +++ b/wgpu-core/src/lib.rs @@ -205,7 +205,10 @@ macro_rules! gfx_select { wgt::Backend::Dx12 => $global.$method::<$crate::api::Dx12>( $($param),* ), //#[cfg(all(not(target_arch = "wasm32"), windows))] //wgt::Backend::Dx11 => $global.$method::<$crate::api::Dx11>( $($param),* ), - #[cfg(all(not(target_arch = "wasm32"), unix, not(any(target_os = "ios", target_os = "macos"))))] + #[cfg(any( + all(unix, not(target_os = "macos"), not(target_os = "ios")), + all(target_arch = "wasm32", feature = "webgl") + ))] wgt::Backend::Gl => $global.$method::<$crate::api::Gles>( $($param),+ ), other => panic!("Unexpected backend {:?}", other), diff --git a/wgpu-hal/Cargo.toml b/wgpu-hal/Cargo.toml index 876499d0d10..db6156c511b 100644 --- a/wgpu-hal/Cargo.toml +++ b/wgpu-hal/Cargo.toml @@ -63,6 +63,11 @@ mtl = { package = "metal", version = "0.22", git="https://github.com/gfx-rs/meta objc = "0.2.5" core-graphics-types = "0.1" +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { version = "0.2" } +web-sys = { version = "0.3", features = ["Window", "HtmlCanvasElement", "WebGl2RenderingContext"] } +js-sys = { version = "0.3" } + [dependencies.naga] git = "https://github.com/gfx-rs/naga" rev = "a7ac13a" diff --git a/wgpu-hal/src/gles/adapter.rs b/wgpu-hal/src/gles/adapter.rs index 4f731fef8da..2872e262d55 100644 --- a/wgpu-hal/src/gles/adapter.rs +++ b/wgpu-hal/src/gles/adapter.rs @@ -184,7 +184,7 @@ impl super::Adapter { log::info!("Vendor: {}", vendor); log::info!("Renderer: {}", renderer); - log::info!("Version: {}", version); + log::info!("Version: {}", &version); log::debug!("Extensions: {:#?}", extensions); @@ -192,25 +192,42 @@ impl super::Adapter { let shading_language_version = { let sl_version = gl.get_parameter_string(glow::SHADING_LANGUAGE_VERSION); - log::info!("SL version: {}", sl_version); + log::info!("SL version: {}", &sl_version); let (sl_major, sl_minor) = Self::parse_version(&sl_version).ok()?; let value = sl_major as u16 * 100 + sl_minor as u16 * 10; naga::back::glsl::Version::Embedded(value) }; - let vertex_shader_storage_blocks = - gl.get_parameter_i32(glow::MAX_VERTEX_SHADER_STORAGE_BLOCKS) as u32; - let fragment_shader_storage_blocks = - gl.get_parameter_i32(glow::MAX_FRAGMENT_SHADER_STORAGE_BLOCKS) as u32; + let vertex_shader_storage_blocks = if cfg!(target_arch = "wasm32") { + // TODO: find out actual value + 0 + } else { + gl.get_parameter_i32(glow::MAX_VERTEX_SHADER_STORAGE_BLOCKS) as u32 + }; + let fragment_shader_storage_blocks = if cfg!(target_arch = "wasm32") { + // TODO: find out actual value + 0 + } else { + gl.get_parameter_i32(glow::MAX_FRAGMENT_SHADER_STORAGE_BLOCKS) as u32 + }; - let vertex_shader_storage_textures = - gl.get_parameter_i32(glow::MAX_VERTEX_IMAGE_UNIFORMS) as u32; - let fragment_shader_storage_textures = - gl.get_parameter_i32(glow::MAX_FRAGMENT_IMAGE_UNIFORMS) as u32; + let vertex_shader_storage_textures = if cfg!(target_arch = "wasm32") { + // TODO: find out actual value + 8 + } else { + gl.get_parameter_i32(glow::MAX_VERTEX_IMAGE_UNIFORMS) as u32 + }; + let fragment_shader_storage_textures = if cfg!(target_arch = "wasm32") { + 8 + } else { + gl.get_parameter_i32(glow::MAX_FRAGMENT_IMAGE_UNIFORMS) as u32 + }; - // WORKAROUND: - // In order to work around an issue with GL on RPI4 and similar, we ignore a zero vertex ssbo count if there are vertex sstos. (more info: https://github.com/gfx-rs/wgpu/pull/1607#issuecomment-874938961) - // The hardware does not want us to write to these SSBOs, but GLES cannot express that. We detect this case and disable writing to SSBOs. + // WORKAROUND: In order to work around an issue with GL on RPI4 and similar, we ignore a + // zero vertex ssbo count if there are vertex sstos. (more info: + // https://github.com/gfx-rs/wgpu/pull/1607#issuecomment-874938961) The hardware does not + // want us to write to these SSBOs, but GLES cannot express that. We detect this case and + // disable writing to SSBOs. let vertex_ssbo_false_zero = vertex_shader_storage_blocks == 0 && vertex_shader_storage_textures != 0; @@ -228,6 +245,19 @@ impl super::Adapter { vertex_shader_storage_textures.min(fragment_shader_storage_textures) }; + let max_vertex_buffer_array_stride = if cfg!(target_arch = "wasm32") { + 255 // https://www.khronos.org/registry/webgl/specs/latest/1.0/index.html#VERTEX_STRIDE + } else { + gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIB_STRIDE) as u32 + }; + + let max_vertex_buffers = if cfg!(target_arch = "wasm32") { + // TODO: Not sure what the max vertex buffer count is for WebGL + 8 + } else { + gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIB_BINDINGS) as u32 + }; + let mut features = wgt::Features::empty() | wgt::Features::TEXTURE_COMPRESSION_ETC2 | wgt::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES; @@ -261,6 +291,14 @@ impl super::Adapter { ver >= (3, 2) || extensions.contains("GL_EXT_draw_buffers_indexed"), ); + if cfg!(not(target_arch = "wasm32")) + && (ver >= (3, 1) + || extensions.contains("GL_ARB_buffer_storage") + || extensions.contains("GL_EXT_buffer_storage")) + { + downlevel_flags.set(wgt::DownlevelFlags::STORAGE_RESOURCES, true); + } + let max_texture_size = gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) as u32; let max_texture_3d_size = gl.get_parameter_i32(glow::MAX_3D_TEXTURE_SIZE) as u32; @@ -281,8 +319,11 @@ impl super::Adapter { max_texture_dimension_3d: max_texture_3d_size, max_texture_array_layers: gl.get_parameter_i32(glow::MAX_ARRAY_TEXTURE_LAYERS) as u32, max_bind_groups: crate::MAX_BIND_GROUPS as u32, - max_dynamic_uniform_buffers_per_pipeline_layout: max_uniform_buffers_per_shader_stage, - max_dynamic_storage_buffers_per_pipeline_layout: max_storage_buffers_per_shader_stage, + // FIXME: !!! + // max_dynamic_uniform_buffers_per_pipeline_layout: max_uniform_buffers_per_shader_stage, + // max_dynamic_storage_buffers_per_pipeline_layout: max_storage_buffers_per_shader_stage, + max_dynamic_uniform_buffers_per_pipeline_layout: 8, + max_dynamic_storage_buffers_per_pipeline_layout: 4, max_sampled_textures_per_shader_stage: super::MAX_TEXTURE_SLOTS as u32, max_samplers_per_shader_stage: super::MAX_SAMPLERS as u32, max_storage_buffers_per_shader_stage, @@ -295,11 +336,10 @@ impl super::Adapter { } else { 0 } as u32, - max_vertex_buffers: gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIB_BINDINGS) as u32, + max_vertex_buffers, max_vertex_attributes: (gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIBS) as u32) .min(super::MAX_VERTEX_ATTRIBUTES as u32), - max_vertex_buffer_array_stride: gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIB_STRIDE) - as u32, + max_vertex_buffer_array_stride, max_push_constant_size: 0, }; @@ -318,6 +358,12 @@ impl super::Adapter { ver >= (3, 1), ); + #[cfg(not(target_arch = "wasm32"))] + private_caps.set(super::PrivateCapabilities::INDEX_BUFFER_ROLE_CHANGE, true); + + #[cfg(target_arch = "wasm32")] + private_caps.set(super::PrivateCapabilities::EMULATE_BUFFER_MAP, true); + let mut workarounds = super::Workarounds::empty(); let r = renderer.to_lowercase(); // Check for Mesa sRGB clear bug. See @@ -344,6 +390,7 @@ impl super::Adapter { shared: Arc::new(super::AdapterShared { context, private_caps, + downlevel_flags, workarounds, shading_language_version, }), @@ -448,6 +495,7 @@ impl crate::Adapter for super::Adapter { zero_buffer, temp_query_results: Vec::new(), draw_buffer_count: 1, + current_index_buffer: None, }, }) } @@ -547,11 +595,13 @@ impl crate::Adapter for super::Adapter { formats: if surface.enable_srgb { vec![ wgt::TextureFormat::Rgba8UnormSrgb, + #[cfg(not(target_arch = "wasm32"))] wgt::TextureFormat::Bgra8UnormSrgb, ] } else { vec![ wgt::TextureFormat::Rgba8Unorm, + #[cfg(not(target_arch = "wasm32"))] wgt::TextureFormat::Bgra8Unorm, ] }, @@ -576,6 +626,12 @@ impl crate::Adapter for super::Adapter { } } +// SAFE: WASM doesn't have threads +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for super::Adapter {} +#[cfg(target_arch = "wasm32")] +unsafe impl Send for super::Adapter {} + #[cfg(test)] mod tests { use super::super::Adapter; diff --git a/wgpu-hal/src/gles/command.rs b/wgpu-hal/src/gles/command.rs index b5020554b45..85f2480218e 100644 --- a/wgpu-hal/src/gles/command.rs +++ b/wgpu-hal/src/gles/command.rs @@ -251,14 +251,12 @@ impl crate::CommandEncoder for super::CommandEncoder { ) where T: Iterator, { - //TODO: preserve `src.target` and `dst.target` - // at least for the buffers that require it. for copy in regions { self.cmd_buffer.commands.push(C::CopyBufferToBuffer { src: src.raw, - src_target: glow::COPY_READ_BUFFER, + src_target: src.target, dst: dst.raw, - dst_target: glow::COPY_WRITE_BUFFER, + dst_target: dst.target, copy, }) } diff --git a/wgpu-hal/src/gles/device.rs b/wgpu-hal/src/gles/device.rs index bf47814ded9..5b4da35f256 100644 --- a/wgpu-hal/src/gles/device.rs +++ b/wgpu-hal/src/gles/device.rs @@ -76,7 +76,7 @@ impl super::Device { gl: &glow::Context, shader: &str, naga_stage: naga::ShaderStage, - label: Option<&str>, + #[cfg_attr(target_arch = "wasm32", allow(unused))] label: Option<&str>, ) -> Result { let target = match naga_stage { naga::ShaderStage::Vertex => glow::VERTEX_SHADER, @@ -85,6 +85,7 @@ impl super::Device { }; let raw = gl.create_shader(target).unwrap(); + #[cfg(not(target_arch = "wasm32"))] if gl.supports_debug() { gl.object_label(glow::SHADER, raw, label); } @@ -163,9 +164,10 @@ impl super::Device { gl: &glow::Context, shaders: I, layout: &super::PipelineLayout, - label: crate::Label, + #[cfg_attr(target_arch = "wasm32", allow(unused))] label: Option<&str>, ) -> Result { let program = gl.create_program().unwrap(); + #[cfg(not(target_arch = "wasm32"))] if let Some(label) = label { if gl.supports_debug() { gl.object_label(glow::PROGRAM, program, Some(label)); @@ -318,26 +320,46 @@ impl crate::Device for super::Device { .contains(crate::MemoryFlags::PREFER_COHERENT); let mut map_flags = 0; - if is_host_visible { - map_flags |= glow::MAP_PERSISTENT_BIT; - if is_coherent { - map_flags |= glow::MAP_COHERENT_BIT; - } - } - if desc.usage.contains(crate::BufferUses::MAP_READ) { - map_flags |= glow::MAP_READ_BIT; - } - if desc.usage.contains(crate::BufferUses::MAP_WRITE) { - map_flags |= glow::MAP_WRITE_BIT; - } - let raw = gl.create_buffer().unwrap(); gl.bind_buffer(target, Some(raw)); let raw_size = desc .size .try_into() .map_err(|_| crate::DeviceError::OutOfMemory)?; - gl.buffer_storage(target, raw_size, None, map_flags); + + if self + .shared + .downlevel_flags + .contains(wgt::DownlevelFlags::STORAGE_RESOURCES) + { + if is_host_visible { + map_flags |= glow::MAP_PERSISTENT_BIT; + if is_coherent { + map_flags |= glow::MAP_COHERENT_BIT; + } + } + if desc.usage.contains(crate::BufferUses::MAP_READ) { + map_flags |= glow::MAP_READ_BIT; + } + if desc.usage.contains(crate::BufferUses::MAP_WRITE) { + map_flags |= glow::MAP_WRITE_BIT; + } + + gl.buffer_storage(target, raw_size, None, map_flags); + } else { + assert!(!is_coherent); + let usage = if is_host_visible { + if desc.usage.contains(crate::BufferUses::MAP_READ) { + glow::STREAM_READ + } else { + glow::DYNAMIC_DRAW + } + } else { + glow::STATIC_DRAW + }; + gl.buffer_data_size(target, raw_size, usage); + } + gl.bind_buffer(target, None); if !is_coherent && desc.usage.contains(crate::BufferUses::MAP_WRITE) { @@ -345,6 +367,7 @@ impl crate::Device for super::Device { } //TODO: do we need `glow::MAP_UNSYNCHRONIZED_BIT`? + #[cfg(not(target_arch = "wasm32"))] if let Some(label) = desc.label { if gl.supports_debug() { gl.object_label(glow::BUFFER, raw, Some(label)); @@ -356,6 +379,8 @@ impl crate::Device for super::Device { target, size: desc.size, map_flags, + #[cfg(target_arch = "wasm32")] + emulate_map_allocation: Default::default(), }) } unsafe fn destroy_buffer(&self, buffer: super::Buffer) { @@ -372,14 +397,41 @@ impl crate::Device for super::Device { let is_coherent = buffer.map_flags & glow::MAP_COHERENT_BIT != 0; - gl.bind_buffer(buffer.target, Some(buffer.raw)); - let ptr = gl.map_buffer_range( - buffer.target, - range.start as i32, - (range.end - range.start) as i32, - buffer.map_flags, - ); - gl.bind_buffer(buffer.target, None); + let ptr = if self + .shared + .private_caps + .contains(super::PrivateCapabilities::EMULATE_BUFFER_MAP) + { + #[cfg(target_arch = "wasm32")] + { + let ptr: *mut u8 = if let Some(ptr) = buffer.emulate_map_allocation.get().as_ref() { + *ptr + } else { + let ptr = + Box::into_raw(vec![0; buffer.size as usize].into_boxed_slice()) as *mut u8; + buffer.emulate_map_allocation.set(Some(ptr)); + ptr + }; + + ptr.offset(range.start as isize) + } + + #[cfg(not(target_arch = "wasm32"))] + unimplemented!( + "map_buffer() is not implemented for native platforms that do not support it" + ); + } else { + gl.bind_buffer(buffer.target, Some(buffer.raw)); + let ptr = gl.map_buffer_range( + buffer.target, + range.start as i32, + (range.end - range.start) as i32, + buffer.map_flags, + ); + gl.bind_buffer(buffer.target, None); + + ptr + }; Ok(crate::BufferMapping { ptr: ptr::NonNull::new(ptr).ok_or(crate::DeviceError::Lost)?, @@ -389,7 +441,26 @@ impl crate::Device for super::Device { unsafe fn unmap_buffer(&self, buffer: &super::Buffer) -> Result<(), crate::DeviceError> { let gl = &self.shared.context.lock(); gl.bind_buffer(buffer.target, Some(buffer.raw)); - gl.unmap_buffer(buffer.target); + + if self + .shared + .private_caps + .contains(super::PrivateCapabilities::EMULATE_BUFFER_MAP) + { + #[cfg(target_arch = "wasm32")] + { + let ptr = buffer.emulate_map_allocation.take().unwrap(); + let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr, buffer.size as usize)); + } + + #[cfg(not(target_arch = "wasm32"))] + unimplemented!( + "unmap_buffer() is not implemented for native plaforms that do not support it" + ); + } else { + gl.unmap_buffer(buffer.target); + } + gl.bind_buffer(buffer.target, None); Ok(()) } @@ -400,11 +471,35 @@ impl crate::Device for super::Device { let gl = &self.shared.context.lock(); gl.bind_buffer(buffer.target, Some(buffer.raw)); for range in ranges { - gl.flush_mapped_buffer_range( - buffer.target, - range.start as i32, - (range.end - range.start) as i32, - ); + if self + .shared + .private_caps + .contains(super::PrivateCapabilities::EMULATE_BUFFER_MAP) + { + #[cfg(target_arch = "wasm32")] + { + let ptr = buffer + .emulate_map_allocation + .get() + .expect("Buffer not mapped"); + let slice = std::slice::from_raw_parts_mut( + ptr.offset(range.start as isize), + buffer.size as usize, + ); + gl.buffer_sub_data_u8_slice(buffer.target, range.start as i32, slice); + } + + #[cfg(not(target_arch = "wasm32"))] + unimplemented!( + "flush_mapped_ranges() not implemented for native targets that do not support it" + ); + } else { + gl.flush_mapped_buffer_range( + buffer.target, + range.start as i32, + (range.end - range.start) as i32, + ); + } } } unsafe fn invalidate_mapped_ranges(&self, _buffer: &super::Buffer, _ranges: I) { @@ -445,6 +540,7 @@ impl crate::Device for super::Device { ); } + #[cfg(not(target_arch = "wasm32"))] if let Some(label) = desc.label { if gl.supports_debug() { gl.object_label(glow::RENDERBUFFER, raw, Some(label)); @@ -523,6 +619,7 @@ impl crate::Device for super::Device { } }; + #[cfg(not(target_arch = "wasm32"))] if let Some(label) = desc.label { if gl.supports_debug() { gl.object_label(glow::TEXTURE, raw, Some(label)); @@ -657,6 +754,7 @@ impl crate::Device for super::Device { ); } + #[cfg(not(target_arch = "wasm32"))] if let Some(label) = desc.label { if gl.supports_debug() { gl.object_label(glow::SAMPLER, raw, Some(label)); @@ -944,11 +1042,11 @@ impl crate::Device for super::Device { gl.delete_program(pipeline.inner.program); } + #[cfg_attr(target_arch = "wasm32", allow(unused))] unsafe fn create_query_set( &self, desc: &wgt::QuerySetDescriptor, ) -> Result { - use std::fmt::Write; let gl = &self.shared.context.lock(); let mut temp_string = String::new(); @@ -957,7 +1055,10 @@ impl crate::Device for super::Device { let query = gl .create_query() .map_err(|_| crate::DeviceError::OutOfMemory)?; + #[cfg(not(target_arch = "wasm32"))] if gl.supports_debug() { + use std::fmt::Write; + if let Some(label) = desc.label { temp_string.clear(); let _ = write!(temp_string, "{}[{}]", label, i); @@ -1039,3 +1140,9 @@ impl crate::Device for super::Device { .end_frame_capture(ptr::null_mut(), ptr::null_mut()) } } + +// SAFE: WASM doesn't have threads +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for super::Device {} +#[cfg(target_arch = "wasm32")] +unsafe impl Send for super::Device {} diff --git a/wgpu-hal/src/gles/mod.rs b/wgpu-hal/src/gles/mod.rs index 144e9bd725e..35905bbd994 100644 --- a/wgpu-hal/src/gles/mod.rs +++ b/wgpu-hal/src/gles/mod.rs @@ -58,6 +58,8 @@ To address this, we invalidate the vertex buffers based on: #[cfg(not(target_arch = "wasm32"))] mod egl; +#[cfg(target_arch = "wasm32")] +mod web; mod adapter; mod command; @@ -68,6 +70,9 @@ mod queue; #[cfg(not(target_arch = "wasm32"))] use self::egl::{AdapterContext, Instance, Surface}; +#[cfg(target_arch = "wasm32")] +use self::web::{AdapterContext, Instance, Surface}; + use arrayvec::ArrayVec; use glow::HasContext; @@ -122,6 +127,11 @@ bitflags::bitflags! { const MEMORY_BARRIERS = 1 << 2; /// Vertex buffer layouts separate from the data. const VERTEX_BUFFER_LAYOUT = 1 << 3; + /// Buffer map must emulated becuase it is not supported natively. + const EMULATE_BUFFER_MAP = 1 << 4; + /// Indicates that buffers used as ELEMENT_ARRAY_BUFFER may be created / initialized / used + /// as other targets, if not present they must not be mixed with other targets. + const INDEX_BUFFER_ROLE_CHANGE = 1 << 5; } } @@ -163,6 +173,7 @@ struct TextureFormatDesc { struct AdapterShared { context: AdapterContext, private_caps: PrivateCapabilities, + downlevel_flags: wgt::DownlevelFlags, workarounds: Workarounds, shading_language_version: naga::back::glsl::Version, } @@ -193,6 +204,7 @@ pub struct Queue { zero_buffer: glow::Buffer, temp_query_results: Vec, draw_buffer_count: u8, + current_index_buffer: Option, } #[derive(Debug)] @@ -201,8 +213,16 @@ pub struct Buffer { target: BindTarget, size: wgt::BufferAddress, map_flags: u32, + #[cfg(target_arch = "wasm32")] + emulate_map_allocation: std::cell::Cell>, } +// Safe: WASM doesn't have threads +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for Buffer {} +#[cfg(target_arch = "wasm32")] +unsafe impl Send for Buffer {} + #[derive(Clone, Debug)] enum TextureInner { Renderbuffer { @@ -217,7 +237,14 @@ enum TextureInner { impl TextureInner { fn as_native(&self) -> (glow::Texture, BindTarget) { match *self { - Self::Renderbuffer { raw, .. } => panic!("Unexpected renderbuffer {}", raw), + #[cfg(not(target_arch = "wasm32"))] // Wasm renderbuffers don't impl Debug + Self::Renderbuffer { raw, .. } => { + panic!("Unexpected renderbuffer {}", raw); + } + #[cfg(target_arch = "wasm32")] + Self::Renderbuffer { .. } => { + panic!("Unexpected renderbuffer"); + } Self::Texture { raw, target } => (raw, target), } } @@ -397,10 +424,22 @@ pub struct RenderPipeline { stencil: Option, } +// SAFE: WASM doesn't have threads +#[cfg(target_arch = "wasm32")] +unsafe impl Send for RenderPipeline {} +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for RenderPipeline {} + pub struct ComputePipeline { inner: PipelineInner, } +// SAFE: WASM doesn't have threads +#[cfg(target_arch = "wasm32")] +unsafe impl Send for ComputePipeline {} +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for ComputePipeline {} + #[derive(Debug)] pub struct QuerySet { queries: Box<[glow::Query]>, @@ -659,7 +698,7 @@ pub struct CommandBuffer { label: Option, commands: Vec, data_bytes: Vec, - data_words: Vec, + data_words: Vec, } //TODO: we would have something like `Arc` diff --git a/wgpu-hal/src/gles/queue.rs b/wgpu-hal/src/gles/queue.rs index 6b0a7129fab..e02eaec255f 100644 --- a/wgpu-hal/src/gles/queue.rs +++ b/wgpu-hal/src/gles/queue.rs @@ -1,8 +1,9 @@ use super::Command as C; use arrayvec::ArrayVec; use glow::HasContext; -use std::{mem, ops::Range, slice, sync::Arc}; +use std::{mem, slice, sync::Arc}; +#[cfg(not(target_arch = "wasm32"))] const DEBUG_ID: u32 = 0; const CUBEMAP_FACES: [u32; 6] = [ @@ -14,7 +15,8 @@ const CUBEMAP_FACES: [u32; 6] = [ glow::TEXTURE_CUBE_MAP_NEGATIVE_Z, ]; -fn extract_marker<'a>(data: &'a [u8], range: &Range) -> &'a str { +#[cfg(not(target_arch = "wasm32"))] +fn extract_marker<'a>(data: &'a [u8], range: &std::ops::Range) -> &'a str { std::str::from_utf8(&data[range.start as usize..range.end as usize]).unwrap() } @@ -49,6 +51,7 @@ impl super::Queue { .map(|i| glow::COLOR_ATTACHMENT0 + i) .collect::>(); gl.draw_buffers(&indices); + #[cfg(not(target_arch = "wasm32"))] for draw_buffer in 0..self.draw_buffer_count as u32 { gl.disable_draw_buffer(glow::BLEND, draw_buffer); } @@ -105,8 +108,8 @@ impl super::Queue { &mut self, gl: &glow::Context, command: &C, - data_bytes: &[u8], - data_words: &[u32], + #[cfg_attr(target_arch = "wasm32", allow(unused))] data_bytes: &[u8], + data_words: &[glow::Query], ) { match *command { C::Draw { @@ -214,21 +217,58 @@ impl super::Queue { } C::CopyBufferToBuffer { src, - src_target, + src_target: _, dst, dst_target, copy, } => { - gl.bind_buffer(src_target, Some(src)); - gl.bind_buffer(dst_target, Some(dst)); + let is_index_buffer_only_element_dst = !self + .shared + .private_caps + .contains(super::PrivateCapabilities::INDEX_BUFFER_ROLE_CHANGE) + && dst_target == glow::ELEMENT_ARRAY_BUFFER; - gl.copy_buffer_sub_data( - src_target, - dst_target, - copy.src_offset as i32, - copy.dst_offset as i32, - copy.size.get() as i32, - ); + let copy_src_target = glow::COPY_READ_BUFFER; + + // WebGL not allowed to copy data from other targets to element buffer and can't copy element data to other buffers + let copy_dst_target = if is_index_buffer_only_element_dst { + glow::ELEMENT_ARRAY_BUFFER + } else { + glow::COPY_WRITE_BUFFER + }; + + gl.bind_buffer(copy_src_target, Some(src)); + gl.bind_buffer(copy_dst_target, Some(dst)); + + if is_index_buffer_only_element_dst { + let mut buffer_data = vec![0; copy.size.get() as usize]; + gl.get_buffer_sub_data( + copy_src_target, + copy.src_offset as i32, + &mut buffer_data, + ); + gl.buffer_sub_data_u8_slice( + copy_dst_target, + copy.dst_offset as i32, + &buffer_data, + ); + } else { + gl.copy_buffer_sub_data( + copy_src_target, + copy_dst_target, + copy.src_offset as _, + copy.dst_offset as _, + copy.size.get() as _, + ); + } + + gl.bind_buffer(copy_src_target, None); + + if is_index_buffer_only_element_dst { + gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, self.current_index_buffer); + } else { + gl.bind_buffer(copy_dst_target, None); + } } C::CopyTextureToTexture { src, @@ -501,6 +541,7 @@ impl super::Queue { } C::SetIndexBuffer(buffer) => { gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(buffer)); + self.current_index_buffer = Some(buffer); } C::BeginQuery(query, target) => { gl.begin_query(target, query); @@ -593,6 +634,7 @@ impl super::Queue { .map(|i| glow::COLOR_ATTACHMENT0 + i) .collect::>(); gl.draw_buffers(&indices); + #[cfg(not(target_arch = "wasm32"))] for draw_buffer in 0..count as u32 { gl.disable_draw_buffer(glow::BLEND, draw_buffer); } @@ -854,6 +896,7 @@ impl super::Queue { gl.blend_func_draw_buffer(index, blend.color.src, blend.color.dst); } } else { + #[cfg(not(target_arch = "wasm32"))] gl.disable_draw_buffer(index, glow::BLEND); } } else { @@ -913,6 +956,7 @@ impl super::Queue { binding.format, ); } + #[cfg(not(target_arch = "wasm32"))] C::InsertDebugMarker(ref range) => { let marker = extract_marker(data_bytes, range); gl.debug_message_insert( @@ -923,11 +967,17 @@ impl super::Queue { marker, ); } + #[cfg(target_arch = "wasm32")] + C::InsertDebugMarker(_) => (), + #[cfg_attr(target_arch = "wasm32", allow(unused))] C::PushDebugGroup(ref range) => { + #[cfg(not(target_arch = "wasm32"))] let marker = extract_marker(data_bytes, range); + #[cfg(not(target_arch = "wasm32"))] gl.push_debug_group(glow::DEBUG_SOURCE_APPLICATION, DEBUG_ID, marker); } C::PopDebugGroup => { + #[cfg(not(target_arch = "wasm32"))] gl.pop_debug_group(); } } @@ -944,12 +994,16 @@ impl crate::Queue for super::Queue { let gl = &shared.context.lock(); self.reset_state(gl); for cmd_buf in command_buffers.iter() { + #[cfg(not(target_arch = "wasm32"))] if let Some(ref label) = cmd_buf.label { gl.push_debug_group(glow::DEBUG_SOURCE_APPLICATION, DEBUG_ID, label); } + for command in cmd_buf.commands.iter() { self.process(gl, command, &cmd_buf.data_bytes, &cmd_buf.data_words); } + + #[cfg(not(target_arch = "wasm32"))] if cmd_buf.label.is_some() { gl.pop_debug_group(); } @@ -971,7 +1025,18 @@ impl crate::Queue for super::Queue { surface: &mut super::Surface, texture: super::Texture, ) -> Result<(), crate::SurfaceError> { + #[cfg(not(target_arch = "wasm32"))] let gl = &self.shared.context.get_without_egl_lock(); + + #[cfg(target_arch = "wasm32")] + let gl = &self.shared.context.glow_context; + surface.present(texture, gl) } } + +// SAFE: WASM doesn't have threads +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for super::Queue {} +#[cfg(target_arch = "wasm32")] +unsafe impl Send for super::Queue {} diff --git a/wgpu-hal/src/gles/web.rs b/wgpu-hal/src/gles/web.rs new file mode 100644 index 00000000000..cd45148f30f --- /dev/null +++ b/wgpu-hal/src/gles/web.rs @@ -0,0 +1,296 @@ +use glow::HasContext; +use parking_lot::Mutex; +use wasm_bindgen::JsCast; + +use super::TextureFormatDesc; + +/// A wrapper around a [`glow::Context`] to provide a fake `lock()` api that makes it compatible +/// with the `AdapterContext` API fromt the EGL implementation. +pub struct AdapterContext { + pub glow_context: glow::Context, +} + +impl AdapterContext { + /// Obtain a lock to the EGL context and get handle to the [`glow::Context`] that can be used to + /// do rendering. + #[track_caller] + pub fn lock(&self) -> &glow::Context { + &self.glow_context + } +} + +#[derive(Debug)] +pub struct Instance { + canvas: Mutex>, +} + +// SAFE: WASM doesn't have threads +unsafe impl Sync for Instance {} +unsafe impl Send for Instance {} + +impl crate::Instance for Instance { + unsafe fn init(_desc: &crate::InstanceDescriptor) -> Result { + Ok(Instance { + canvas: Mutex::new(None), + }) + } + + unsafe fn enumerate_adapters(&self) -> Vec> { + let canvas_guard = self.canvas.lock(); + let gl = match *canvas_guard { + Some(ref canvas) => { + let window = web_sys::window().expect("Could not get browser window"); + + if canvas.get_attribute("width").is_none() { + canvas + .set_attribute( + "width", + &window + .inner_width() + .expect("Cannot read window width") + .as_f64() + .unwrap() + .to_string(), + ) + .expect("Cannot set canvas width"); + } + if canvas.get_attribute("height").is_none() { + canvas + .set_attribute( + "height", + &window + .inner_height() + .expect("Cannot read window height") + .as_f64() + .unwrap() + .to_string(), + ) + .expect("Cannot set canvas height"); + } + let context_options = js_sys::Object::new(); + js_sys::Reflect::set( + &context_options, + &"antialias".into(), + &wasm_bindgen::JsValue::FALSE, + ) + .expect("Cannot create context options"); + let webgl2_context = canvas + .get_context_with_context_options("webgl2", &context_options) + .expect("Cannot create WebGL2 context") + .and_then(|context| context.dyn_into::().ok()) + .expect("Cannot convert into WebGL2 context"); + glow::Context::from_webgl2_context(webgl2_context) + } + None => return Vec::new(), + }; + + super::Adapter::expose(AdapterContext { glow_context: gl }) + .into_iter() + .collect() + } + + unsafe fn create_surface( + &self, + has_handle: &impl raw_window_handle::HasRawWindowHandle, + ) -> Result { + if let raw_window_handle::RawWindowHandle::Web(handle) = has_handle.raw_window_handle() { + let canvas: web_sys::HtmlCanvasElement = web_sys::window() + .and_then(|win| win.document()) + .expect("Cannot get document") + .query_selector(&format!("canvas[data-raw-handle=\"{}\"]", handle.id)) + .expect("Cannot query for canvas") + .expect("Canvas is not found") + .dyn_into() + .expect("Failed to downcast to canvas type"); + + *self.canvas.lock() = Some(canvas.clone()); + + Ok(Surface { + canvas, + present_program: None, + swapchain: None, + texture: None, + presentable: true, + enable_srgb: true, // WebGL only supports sRGB + }) + } else { + unreachable!() + } + } + + unsafe fn destroy_surface(&self, surface: Surface) { + let mut canvas_option_ref = self.canvas.lock(); + + if let Some(canvas) = canvas_option_ref.as_ref() { + if canvas == &surface.canvas { + *canvas_option_ref = None; + } + } + } +} + +#[derive(Clone, Debug)] +pub struct Surface { + canvas: web_sys::HtmlCanvasElement, + pub(super) swapchain: Option, + texture: Option, + pub(super) presentable: bool, + pub(super) enable_srgb: bool, + present_program: Option, +} + +// SAFE: Because web doesn't have threads ( yet ) +unsafe impl Sync for Surface {} +unsafe impl Send for Surface {} + +#[derive(Clone, Debug)] +pub struct Swapchain { + pub(crate) extent: wgt::Extent3d, + // pub(crate) channel: f::ChannelType, + pub(super) format: wgt::TextureFormat, + pub(super) framebuffer: glow::Framebuffer, + pub(super) format_desc: TextureFormatDesc, +} + +impl Surface { + pub(super) unsafe fn present( + &mut self, + _suf_texture: super::Texture, + gl: &glow::Context, + ) -> Result<(), crate::SurfaceError> { + gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, None); + gl.bind_sampler(0, None); + gl.active_texture(glow::TEXTURE0); + gl.bind_texture(glow::TEXTURE_2D, self.texture); + gl.use_program(self.present_program); + gl.disable(glow::DEPTH_TEST); + gl.disable(glow::STENCIL_TEST); + gl.disable(glow::SCISSOR_TEST); + gl.disable(glow::BLEND); + gl.disable(glow::CULL_FACE); + gl.draw_buffers(&[glow::BACK]); + gl.draw_arrays(glow::TRIANGLES, 0, 3); + + Ok(()) + } + + unsafe fn create_present_program(gl: &glow::Context) -> glow::Program { + let program = gl + .create_program() + .expect("Could not create shader program"); + let vertex = gl + .create_shader(glow::VERTEX_SHADER) + .expect("Could not create shader"); + gl.shader_source(vertex, include_str!("./web/present.vert")); + gl.compile_shader(vertex); + let fragment = gl + .create_shader(glow::FRAGMENT_SHADER) + .expect("Could not create shader"); + gl.shader_source(fragment, include_str!("./web/present.frag")); + gl.compile_shader(fragment); + gl.attach_shader(program, vertex); + gl.attach_shader(program, fragment); + gl.link_program(program); + gl.delete_shader(vertex); + gl.delete_shader(fragment); + gl.bind_texture(glow::TEXTURE_2D, None); + + program + } +} + +impl crate::Surface for Surface { + unsafe fn configure( + &mut self, + device: &super::Device, + config: &crate::SurfaceConfiguration, + ) -> Result<(), crate::SurfaceError> { + let gl = device.shared.context.lock(); + + if let Some(swapchain) = self.swapchain.take() { + // delete all frame buffers already allocated + gl.delete_framebuffer(swapchain.framebuffer); + } + + if self.present_program.is_none() { + self.present_program = Some(Self::create_present_program(gl)); + } + + if self.texture.is_none() { + self.texture = Some(gl.create_texture().unwrap()); + } + + let desc = device.shared.describe_texture_format(config.format); + gl.bind_texture(glow::TEXTURE_2D, self.texture); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MIN_FILTER, + glow::NEAREST as _, + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MAG_FILTER, + glow::NEAREST as _, + ); + gl.tex_storage_2d( + glow::TEXTURE_2D, + 1, + desc.internal, + config.extent.width as i32, + config.extent.height as i32, + ); + + let framebuffer = gl.create_framebuffer().unwrap(); + gl.bind_framebuffer(glow::READ_FRAMEBUFFER, Some(framebuffer)); + gl.framebuffer_texture_2d( + glow::READ_FRAMEBUFFER, + glow::COLOR_ATTACHMENT0, + glow::TEXTURE_2D, + self.texture, + 0, + ); + gl.bind_texture(glow::TEXTURE_2D, None); + + self.swapchain = Some(Swapchain { + extent: config.extent, + // channel: config.format.base_format().1, + format: config.format, + format_desc: desc, + framebuffer, + }); + Ok(()) + } + + unsafe fn unconfigure(&mut self, device: &super::Device) { + let gl = device.shared.context.lock(); + if let Some(swapchain) = self.swapchain.take() { + gl.delete_framebuffer(swapchain.framebuffer); + } + if let Some(renderbuffer) = self.texture.take() { + gl.delete_texture(renderbuffer); + } + } + + unsafe fn acquire_texture( + &mut self, + _timeout_ms: u32, + ) -> Result>, crate::SurfaceError> { + let sc = self.swapchain.as_ref().unwrap(); + let texture = super::Texture { + inner: super::TextureInner::Texture { + raw: self.texture.unwrap(), + target: glow::TEXTURE_2D, + }, + array_layer_count: 1, + mip_level_count: 1, + format: sc.format, + format_desc: sc.format_desc.clone(), + }; + Ok(Some(crate::AcquiredSurfaceTexture { + texture, + suboptimal: false, + })) + } + + unsafe fn discard_texture(&mut self, _texture: super::Texture) {} +} diff --git a/wgpu-hal/src/gles/web/present.frag b/wgpu-hal/src/gles/web/present.frag new file mode 100644 index 00000000000..853f82a6ae4 --- /dev/null +++ b/wgpu-hal/src/gles/web/present.frag @@ -0,0 +1,16 @@ +#version 300 es +precision mediump float; +in vec2 uv; +uniform sampler2D present_texture; +out vec4 frag; +vec4 linear_to_srgb(vec4 linear) { + vec3 color_linear = linear.rgb; + vec3 selector = ceil(color_linear - 0.0031308); // 0 if under value, 1 if over + vec3 under = 12.92 * color_linear; + vec3 over = 1.055 * pow(color_linear, vec3(0.41666)) - 0.055; + vec3 result = mix(under, over, selector); + return vec4(result, linear.a); +} +void main() { + frag = linear_to_srgb(texture(present_texture, uv)); +} \ No newline at end of file diff --git a/wgpu-hal/src/gles/web/present.vert b/wgpu-hal/src/gles/web/present.vert new file mode 100644 index 00000000000..922f2a18482 --- /dev/null +++ b/wgpu-hal/src/gles/web/present.vert @@ -0,0 +1,18 @@ +#version 300 es +precision mediump float; +// A triangle that fills the whole screen +const vec2[3] TRIANGLE_POS = vec2[]( + vec2( 0.0, -3.0), + vec2(-3.0, 1.0), + vec2( 3.0, 1.0) +); +const vec2[3] TRIANGLE_UV = vec2[]( + vec2( 0.5, 1.), + vec2( -1.0, -1.0), + vec2( 2.0, -1.0) +); +out vec2 uv; +void main() { + uv = TRIANGLE_UV[gl_VertexID]; + gl_Position = vec4(TRIANGLE_POS[gl_VertexID], 0.0, 1.0); +} \ No newline at end of file diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index 55b9f5a32ec..7748ebf0dbc 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -507,28 +507,31 @@ impl Features { /// Represents the sets of limits an adapter/device supports. /// -/// We provide two different defaults. -/// - [`Limits::downlevel_defaults()]. This is a set of limits that is guaranteed to -/// work on all backends, including "downlevel" backends such -/// as OpenGL and D3D11. For most applications we recommend using these -/// limits, assuming they are high enough for your application. -/// - [`Limits::default()`]. This is the set of limits that is guaranteed to -/// work on all modern backends and is guaranteed to be supported by WebGPU. -/// Applications needing more modern features can use this as a reasonable set of -/// limits if they are targeting only desktop and modern mobile devices. +/// We provide three different defaults. +/// - [`Limits::downlevel_defaults()`]. This is a set of limits that is guarenteed to work on almost +/// all backends, including "downlevel" backends such as OpenGL and D3D11, other than WebGL. For +/// most applications we recommend using these limits, assuming they are high enough for your +/// application, and you do not intent to support WebGL. +/// - [`Limits::downlevel_webgl2_defaults()`] This is a set of limits that is lower even than the +/// [`downlevel_defaults()`], configured to be low enough to support running in the browser using +/// WebGL2. +/// - [`Limits::default()`]. This is the set of limits that is guarenteed to work on all modern +/// backends and is guarenteed to be supported by WebGPU. Applications needing more modern +/// features can use this as a reasonable set of limits if they are targetting only desktop and +/// modern mobile devices. /// -/// We recommend starting with the most restrictive limits you can and manually -/// increasing the limits you need boosted. This will let you stay running on -/// all hardware that supports the limits you need. +/// We recommend starting with the most restrictive limits you can and manually increasing the +/// limits you need boosted. This will let you stay running on all hardware that supports the limits +/// you need. /// /// Limits "better" than the default must be supported by the adapter and requested when requesting -/// a device. If limits "better" than the adapter supports are requested, requesting a device will panic. -/// Once a device is requested, you may only use resources up to the limits requested _even_ if the -/// adapter supports "better" limits. +/// a device. If limits "better" than the adapter supports are requested, requesting a device will +/// panic. Once a device is requested, you may only use resources up to the limits requested _even_ +/// if the adapter supports "better" limits. /// /// Requesting limits that are "better" than you need may cause performance to decrease because the -/// implementation needs to support more than is needed. You should ideally only request exactly what -/// you need. +/// implementation needs to support more than is needed. You should ideally only request exactly +/// what you need. /// /// See also: #[repr(C)] @@ -618,7 +621,7 @@ impl Default for Limits { } impl Limits { - /// These default limits are guaranteed to be compatible with GLES3, WebGL, and D3D11 + /// These default limits are guarenteed to be compatible with GLES3, and D3D11 pub fn downlevel_defaults() -> Self { Self { max_texture_dimension_1d: 2096, @@ -642,6 +645,19 @@ impl Limits { } } + /// These default limits are guarenteed to be compatible with GLES3, and D3D11, and WebGL2 + pub fn downlevel_webgl2_defaults() -> Self { + Self { + max_storage_buffers_per_shader_stage: 0, + max_storage_textures_per_shader_stage: 0, + max_storage_buffer_binding_size: 0, + max_vertex_buffer_array_stride: 255, + + // Most of the values should be the same as the downlevel defaults + ..Self::downlevel_defaults() + } + } + /// Modify the current limits to use the resolution limits of the other. /// /// This is useful because the swapchain might need to be larger than any other image in the application. @@ -733,6 +749,8 @@ bitflags::bitflags! { /// so they can comunicate to the user of aniso is enabled. const ANISOTROPIC_FILTERING = 1 << 10; + /// Allows creating and using buffers with STORAGE_* usage + const STORAGE_RESOURCES = 1 << 11; } } diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 821694c07f7..86f64e47fb4 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -25,7 +25,7 @@ default = [] spirv = ["naga/spv-in"] trace = ["serde", "wgc/trace"] replay = ["serde", "wgc/replay"] -webgl = ["wgc"] +webgl = ["wgc", "wgc/webgl", "hal"] [target.'cfg(not(target_arch = "wasm32"))'.dependencies.wgc] package = "wgpu-core" @@ -42,6 +42,11 @@ optional = true package = "wgpu-types" path = "../wgpu-types" +[target.'cfg(target_arch = "wasm32")'.dependencies.hal] +package = "wgpu-hal" +path = "../wgpu-hal" +optional = true + [target.'cfg(not(target_arch = "wasm32"))'.dependencies.hal] package = "wgpu-hal" path = "../wgpu-hal" @@ -262,3 +267,5 @@ wasm-bindgen-futures = "0.4.23" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_error_panic_hook = "0.1.6" console_log = "0.1.2" +# We need the Location feature in the framework examples +web-sys = { version = "=0.3.51", features = ["Location"] } diff --git a/wgpu/README.md b/wgpu/README.md index caa6f4ca7ca..c505df861a9 100644 --- a/wgpu/README.md +++ b/wgpu/README.md @@ -41,7 +41,14 @@ The following environment variables can be used to configure how the framework e #### Run Examples on the Web (`wasm32-unknown-unknown`) -See [wiki article](https://github.com/gfx-rs/wgpu-rs/wiki/Running-on-the-Web-with-WebGPU-and-WebGL). +From the root of the repository you can use the `run-wasm-example.sh` script to compile and serve the web example locally. The script will build WGPU with WebGPU and WebGL support with WebGL being used if WebGPU is not supported in the target browser. + +> **Note:** Not all of the examples can run on WebGL, specifically the ones that require compute shader support. + +```bash +# Run the cube example +./run-wasm-example.sh cube +``` ## Shaders diff --git a/wgpu/examples/boids/main.rs b/wgpu/examples/boids/main.rs index 2231531b753..4ccd8e6c36e 100644 --- a/wgpu/examples/boids/main.rs +++ b/wgpu/examples/boids/main.rs @@ -31,6 +31,17 @@ struct Example { } impl framework::Example for Example { + fn required_limits(_: &wgpu::Adapter) -> wgpu::Limits { + wgpu::Limits::downlevel_defaults() + } + + fn required_downlevel_capabilities() -> wgpu::DownlevelCapabilities { + wgpu::DownlevelCapabilities { + flags: wgpu::DownlevelFlags::COMPUTE_SHADERS, + ..Default::default() + } + } + /// constructs initial instance of Example struct fn init( sc_desc: &wgpu::SwapChainDescriptor, diff --git a/wgpu/examples/framework.rs b/wgpu/examples/framework.rs index a45308ebbdc..ca22ab0f486 100644 --- a/wgpu/examples/framework.rs +++ b/wgpu/examples/framework.rs @@ -40,8 +40,16 @@ pub trait Example: 'static + Sized { fn required_features() -> wgpu::Features { wgpu::Features::empty() } - fn required_limits() -> wgpu::Limits { - wgpu::Limits::downlevel_defaults() // These downlevel limits will allow the code to run on all possible hardware + fn required_downlevel_capabilities() -> wgpu::DownlevelCapabilities { + wgpu::DownlevelCapabilities { + flags: wgpu::DownlevelFlags::empty(), + shader_model: wgpu::ShaderModel::Sm5, + ..wgpu::DownlevelCapabilities::default() + } + } + fn required_limits(adapter: &wgpu::Adapter) -> wgpu::Limits { + let _ = adapter; + wgpu::Limits::downlevel_webgl2_defaults() // These downlevel limits will allow the code to run on all possible hardware } fn init( sc_desc: &wgpu::SwapChainDescriptor, @@ -95,7 +103,12 @@ async fn setup(title: &str) -> Setup { #[cfg(target_arch = "wasm32")] { use winit::platform::web::WindowExtWebSys; - console_log::init().expect("could not initialize logger"); + let query_string = web_sys::window().unwrap().location().search().unwrap(); + let level: log::Level = parse_url_query_string(&query_string, "RUST_LOG") + .map(|x| x.parse().ok()) + .flatten() + .unwrap_or(log::Level::Info); + console_log::init_with_level(level).expect("could not initialize logger"); std::panic::set_hook(Box::new(console_error_panic_hook::hook)); // On wasm, append the canvas to the document body web_sys::window() @@ -110,7 +123,7 @@ async fn setup(title: &str) -> Setup { log::info!("Initializing the surface..."); - let backend = wgpu::util::backend_bits_from_env().unwrap_or(wgpu::Backends::PRIMARY); + let backend = wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all); let instance = wgpu::Instance::new(backend); let (size, surface) = unsafe { @@ -137,8 +150,23 @@ async fn setup(title: &str) -> Setup { required_features - adapter_features ); + let required_downlevel_capabilities = E::required_downlevel_capabilities(); + let downlevel_capabilities = adapter.get_downlevel_properties(); + assert!( + downlevel_capabilities.shader_model >= required_downlevel_capabilities.shader_model, + "Adapter does not support the minimum shader model required to run this example: {:?}", + required_downlevel_capabilities.shader_model + ); + assert!( + downlevel_capabilities + .flags + .contains(required_downlevel_capabilities.flags), + "Adapter does not support the downlevel capabilities required to run this example: {:?}", + required_downlevel_capabilities.flags - downlevel_capabilities.flags + ); + // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain. - let needed_limits = E::required_limits().using_resolution(adapter.limits()); + let needed_limits = E::required_limits(&adapter).using_resolution(adapter.limits()); let trace_dir = std::env::var("WGPU_TRACE"); let (device, queue) = adapter @@ -378,6 +406,25 @@ pub fn run(title: &str) { }); } +#[cfg(target_arch = "wasm32")] +/// Parse the query string as returned by `web_sys::window()?.location().search()?` and get a +/// specific key out of it. +pub fn parse_url_query_string<'a>(query: &'a str, search_key: &str) -> Option<&'a str> { + let query_string = query.strip_prefix("?")?; + + for pair in query_string.split("&") { + let mut pair = pair.split("="); + let key = pair.next()?; + let value = pair.next()?; + + if key == search_key { + return Some(value); + } + } + + None +} + #[cfg(test)] pub struct FrameworkRefTest { pub image_path: &'static str, @@ -397,12 +444,10 @@ pub fn test(mut params: FrameworkRefTest) { assert_eq!(params.width % 64, 0, "width needs to be aligned 64"); let features = E::required_features() | params.optional_features; - let limits = E::required_limits(); test_common::initialize_test( - mem::take(&mut params.base_test_parameters) - .features(features) - .limits(limits), + mem::take(&mut params.base_test_parameters).features(features), + |adapter| E::required_limits(&adapter), |ctx| { let spawner = Spawner::new(); diff --git a/wgpu/examples/hello-compute/tests.rs b/wgpu/examples/hello-compute/tests.rs index f7e9cfd863d..522cc8f565b 100644 --- a/wgpu/examples/hello-compute/tests.rs +++ b/wgpu/examples/hello-compute/tests.rs @@ -10,6 +10,7 @@ use common::{initialize_test, TestParameters}; fn test_compute_1() { initialize_test( TestParameters::default().specific_failure(None, None, Some("V3D"), true), + |_| wgpu::Limits::downlevel_defaults(), |ctx| { let input = &[1, 2, 3, 4]; @@ -27,6 +28,7 @@ fn test_compute_1() { fn test_compute_2() { initialize_test( TestParameters::default().specific_failure(None, None, Some("V3D"), true), + |_| wgpu::Limits::downlevel_defaults(), |ctx| { let input = &[5, 23, 10, 9]; @@ -44,6 +46,7 @@ fn test_compute_2() { fn test_compute_overflow() { initialize_test( TestParameters::default().specific_failure(None, None, Some("V3D"), true), + |_| wgpu::Limits::downlevel_defaults(), |ctx| { let input = &[77031, 837799, 8400511, 63728127]; pollster::block_on(assert_execute_gpu( @@ -60,6 +63,7 @@ fn test_compute_overflow() { fn test_multithreaded_compute() { initialize_test( TestParameters::default().specific_failure(None, None, Some("V3D"), true), + |_| wgpu::Limits::downlevel_defaults(), |ctx| { use std::{sync::mpsc, thread, time::Duration}; diff --git a/wgpu/examples/shadow/main.rs b/wgpu/examples/shadow/main.rs index 7896d6a4330..0f0067c102c 100644 --- a/wgpu/examples/shadow/main.rs +++ b/wgpu/examples/shadow/main.rs @@ -213,12 +213,36 @@ impl framework::Example for Example { wgpu::Features::DEPTH_CLAMPING } + fn required_limits(adapter: &wgpu::Adapter) -> wgpu::Limits { + let downlevel_limits = wgpu::Limits::downlevel_defaults(); + let webgl_limits = wgpu::Limits::downlevel_webgl2_defaults(); + if adapter + .get_downlevel_properties() + .flags + .contains(wgpu::DownlevelFlags::STORAGE_RESOURCES) + { + wgpu::Limits { + max_storage_buffers_per_shader_stage: downlevel_limits + .max_storage_buffers_per_shader_stage, + max_storage_buffer_binding_size: downlevel_limits.max_storage_buffer_binding_size, + ..webgl_limits + } + } else { + webgl_limits + } + } + fn init( sc_desc: &wgpu::SwapChainDescriptor, - _adapter: &wgpu::Adapter, + adapter: &wgpu::Adapter, device: &wgpu::Device, _queue: &wgpu::Queue, ) -> Self { + let supports_storage_resources = adapter + .get_downlevel_properties() + .flags + .contains(wgpu::DownlevelFlags::STORAGE_RESOURCES); + // Create the vertex and index buffers let vertex_size = mem::size_of::(); let (cube_vertex_data, cube_index_data) = create_cube(); @@ -427,8 +451,11 @@ impl framework::Example for Example { let light_storage_buf = device.create_buffer(&wgpu::BufferDescriptor { label: None, size: light_uniform_size, - usage: wgpu::BufferUsages::STORAGE - | wgpu::BufferUsages::COPY_SRC + usage: if supports_storage_resources { + wgpu::BufferUsages::STORAGE + } else { + wgpu::BufferUsages::UNIFORM + } | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -544,14 +571,18 @@ impl framework::Example for Example { binding: 1, // lights visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, + ty: if supports_storage_resources { + wgpu::BufferBindingType::Storage { read_only: true } + } else { + wgpu::BufferBindingType::Uniform + }, has_dynamic_offset: false, min_binding_size: wgpu::BufferSize::new(light_uniform_size), }, count: None, }, wgpu::BindGroupLayoutEntry { - binding: 2, + binding: 3, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, @@ -561,7 +592,7 @@ impl framework::Example for Example { count: None, }, wgpu::BindGroupLayoutEntry { - binding: 3, + binding: 4, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler { comparison: true, @@ -602,11 +633,11 @@ impl framework::Example for Example { resource: light_storage_buf.as_entire_binding(), }, wgpu::BindGroupEntry { - binding: 2, + binding: 3, resource: wgpu::BindingResource::TextureView(&shadow_view), }, wgpu::BindGroupEntry { - binding: 3, + binding: 4, resource: wgpu::BindingResource::Sampler(&shadow_sampler), }, ], @@ -624,7 +655,11 @@ impl framework::Example for Example { }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: if supports_storage_resources { + "fs_main" + } else { + "fs_main_without_storage" + }, targets: &[sc_desc.format.into()], }), primitive: wgpu::PrimitiveState { diff --git a/wgpu/examples/shadow/shader.wgsl b/wgpu/examples/shadow/shader.wgsl index 49b4bb911b2..c53c2462a29 100644 --- a/wgpu/examples/shadow/shader.wgsl +++ b/wgpu/examples/shadow/shader.wgsl @@ -54,11 +54,19 @@ struct Lights { data: [[stride(96)]] array; }; +// Used when storage types are not supported +[[block]] +struct LightsWithoutStorage { + data: array; +}; + [[group(0), binding(1)]] var s_lights: [[access(read)]] Lights; -[[group(0), binding(2)]] -var t_shadow: texture_depth_2d_array; +[[group(0), binding(1)]] +var u_lights: LightsWithoutStorage; [[group(0), binding(3)]] +var t_shadow: texture_depth_2d_array; +[[group(0), binding(4)]] var sampler_shadow: sampler_comparison; fn fetch_shadow(light_id: u32, homogeneous_coords: vec4) -> f32 { @@ -102,3 +110,27 @@ fn fs_main(in: VertexOutput) -> [[location(0)]] vec4 { // multiply the light by material color return vec4(color, 1.0) * u_entity.color; } + +// The fragment entrypoint used when storage buffers are not available for the lights +[[stage(fragment)]] +fn fs_main_without_storage(in: VertexOutput) -> [[location(0)]] vec4 { + let normal = normalize(in.world_normal); + var color: vec3 = c_ambient; + var i: u32 = 0u; + loop { + if (i >= min(u_globals.num_lights.x, c_max_lights)) { + break; + } + // This line is the only difference from the entrypoint above. It uses the lights + // uniform instead of the lights storage buffer + let light = u_lights.data[i]; + let shadow = fetch_shadow(i, light.proj * in.world_position); + let light_dir = normalize(light.pos.xyz - in.world_position.xyz); + let diffuse = max(0.0, dot(normal, light_dir)); + color = color + shadow * diffuse * light.color.xyz; + continuing { + i = i + 1u; + } + } + return vec4(color, 1.0) * u_entity.color; +} \ No newline at end of file diff --git a/wgpu/examples/texture-arrays/main.rs b/wgpu/examples/texture-arrays/main.rs index 9f498babd00..d0c060b91f4 100644 --- a/wgpu/examples/texture-arrays/main.rs +++ b/wgpu/examples/texture-arrays/main.rs @@ -78,7 +78,7 @@ impl framework::Example for Example { fn required_features() -> wgpu::Features { wgpu::Features::TEXTURE_BINDING_ARRAY | wgpu::Features::SPIRV_SHADER_PASSTHROUGH } - fn required_limits() -> wgpu::Limits { + fn required_limits(_: &wgpu::Adapter) -> wgpu::Limits { wgpu::Limits { max_push_constant_size: 4, ..wgpu::Limits::default() diff --git a/wgpu/src/backend/direct.rs b/wgpu/src/backend/direct.rs index 3cd3e00ae03..b38f32bafa8 100644 --- a/wgpu/src/backend/direct.rs +++ b/wgpu/src/backend/direct.rs @@ -39,6 +39,7 @@ impl fmt::Debug for Context { } impl Context { + #[cfg(not(target_arch = "wasm32"))] pub unsafe fn from_hal_instance(hal_instance: A::Instance) -> Self { Self(wgc::hub::Global::from_hal_instance::( "wgpu", @@ -51,6 +52,7 @@ impl Context { &self.0 } + #[cfg(not(target_arch = "wasm32"))] pub fn enumerate_adapters(&self, backends: wgt::Backends) -> Vec { self.0 .enumerate_adapters(wgc::instance::AdapterInputs::Mask(backends, |_| { @@ -58,6 +60,7 @@ impl Context { })) } + #[cfg(not(target_arch = "wasm32"))] pub unsafe fn create_adapter_from_hal( &self, hal_adapter: hal::ExposedAdapter, @@ -65,6 +68,7 @@ impl Context { self.0.create_adapter_from_hal(hal_adapter, PhantomData) } + #[cfg(not(target_arch = "wasm32"))] pub unsafe fn create_device_from_hal( &self, adapter: &wgc::id::AdapterId, @@ -91,6 +95,7 @@ impl Context { Ok((device, device_id)) } + #[cfg(not(target_arch = "wasm32"))] pub unsafe fn create_texture_from_hal( &self, hal_texture: A::Texture, @@ -119,6 +124,7 @@ impl Context { } } + #[cfg(not(target_arch = "wasm32"))] pub fn generate_report(&self) -> wgc::hub::GlobalReport { self.0.generate_report() } @@ -1369,6 +1375,7 @@ impl crate::Context for Context { } } + #[cfg_attr(target_arch = "wasm32", allow(unused))] fn device_drop(&self, device: &Self::DeviceId) { #[cfg(not(target_arch = "wasm32"))] { diff --git a/wgpu/tests/common/mod.rs b/wgpu/tests/common/mod.rs index 5c1b9c69c35..7ff27d299a3 100644 --- a/wgpu/tests/common/mod.rs +++ b/wgpu/tests/common/mod.rs @@ -81,7 +81,6 @@ pub struct FailureCase { // This information determines if a test should run. pub struct TestParameters { pub required_features: Features, - pub required_limits: Limits, pub required_downlevel_properties: DownlevelCapabilities, // Backends where test should fail. pub failures: Vec, @@ -91,7 +90,6 @@ impl Default for TestParameters { fn default() -> Self { Self { required_features: Features::empty(), - required_limits: Limits::downlevel_defaults(), required_downlevel_properties: lowest_downlevel_properties(), failures: Vec::new(), } @@ -120,12 +118,6 @@ impl TestParameters { self } - /// Set the list - pub fn limits(mut self, limits: Limits) -> Self { - self.required_limits = limits; - self - } - pub fn downlevel_flags(mut self, downlevel_flags: DownlevelFlags) -> Self { self.required_downlevel_properties.flags |= downlevel_flags; self @@ -176,8 +168,11 @@ impl TestParameters { self } } - -pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(TestingContext)) { +pub fn initialize_test( + parameters: TestParameters, + limits_function: impl FnOnce(&Adapter) -> wgpu::Limits, + test_function: impl FnOnce(TestingContext), +) { // We don't actually care if it fails let _ = env_logger::try_init(); @@ -189,6 +184,7 @@ pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(Te )) .expect("could not find sutable adapter on the system"); + let required_limits = limits_function(&adapter); let adapter_info = adapter.get_info(); let adapter_lowercase_name = adapter_info.name.to_lowercase(); let adapter_features = adapter.features(); @@ -201,7 +197,7 @@ pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(Te return; } - if adapter_limits < parameters.required_limits { + if adapter_limits < required_limits { println!("TEST SKIPPED: LIMIT TOO LOW"); return; } @@ -229,7 +225,7 @@ pub fn initialize_test(parameters: TestParameters, test_function: impl FnOnce(Te let (device, queue) = pollster::block_on(initialize_device( &adapter, parameters.required_features, - parameters.required_limits, + required_limits, )); let context = TestingContext { diff --git a/wgpu/tests/device.rs b/wgpu/tests/device.rs index 2763a6a21ad..6a51838c4c4 100644 --- a/wgpu/tests/device.rs +++ b/wgpu/tests/device.rs @@ -2,7 +2,11 @@ use crate::common::{initialize_test, TestParameters}; #[test] fn device_initialization() { - initialize_test(TestParameters::default(), |_ctx| { - // intentionally empty - }) + initialize_test( + TestParameters::default(), + |_| wgt::Limits::downlevel_webgl2_defaults(), + |_ctx| { + // intentionally empty + }, + ) } diff --git a/wgpu/tests/vertex_indices/mod.rs b/wgpu/tests/vertex_indices/mod.rs index 3ddc09ce309..bc81895d881 100644 --- a/wgpu/tests/vertex_indices/mod.rs +++ b/wgpu/tests/vertex_indices/mod.rs @@ -131,11 +131,15 @@ fn pulling_common( #[test] fn draw() { - initialize_test(TestParameters::default().test_features(), |ctx| { - pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { - cmb.draw(0..6, 0..1); - }) - }) + initialize_test( + TestParameters::default().test_features(), + |_| wgt::Limits::downlevel_defaults(), + |ctx| { + pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { + cmb.draw(0..6, 0..1); + }) + }, + ) } #[test] @@ -144,6 +148,7 @@ fn draw_vertex_offset() { TestParameters::default() .test_features() .backend_failure(wgpu::Backends::DX11), + |_| wgt::Limits::downlevel_defaults(), |ctx| { pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { cmb.draw(0..3, 0..1); @@ -155,11 +160,15 @@ fn draw_vertex_offset() { #[test] fn draw_instanced() { - initialize_test(TestParameters::default().test_features(), |ctx| { - pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { - cmb.draw(0..3, 0..2); - }) - }) + initialize_test( + TestParameters::default().test_features(), + |_| wgt::Limits::downlevel_defaults(), + |ctx| { + pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { + cmb.draw(0..3, 0..2); + }) + }, + ) } #[test] @@ -168,6 +177,7 @@ fn draw_instanced_offset() { TestParameters::default() .test_features() .backend_failure(wgpu::Backends::DX11), + |_| wgt::Limits::downlevel_defaults(), |ctx| { pulling_common(ctx, &[0, 1, 2, 3, 4, 5], |cmb| { cmb.draw(0..3, 0..1);