From 95a760bb421ffab138fd243b7cd5096c210527f4 Mon Sep 17 00:00:00 2001 From: Connor Fitzgerald Date: Tue, 24 Jan 2023 13:44:15 -0500 Subject: [PATCH] Implement queue.copy_external_image_to_texture for WebGL2 and improve WebGPU Impl (#3288) Co-authored-by: Teodor Tanasoaia <28601907+teoxoy@users.noreply.github.com> Closes https://github.com/gfx-rs/wgpu/issues/1888 --- CHANGELOG.md | 6 +- Cargo.lock | 48 +++- Cargo.toml | 1 + wgpu-core/src/command/transfer.rs | 21 +- wgpu-core/src/conv.rs | 24 ++ wgpu-core/src/device/queue.rs | 210 +++++++++++++++++- wgpu-hal/src/dx11/adapter.rs | 3 +- wgpu-hal/src/empty.rs | 12 + wgpu-hal/src/gles/adapter.rs | 4 + wgpu-hal/src/gles/command.rs | 25 +++ wgpu-hal/src/gles/mod.rs | 9 + wgpu-hal/src/gles/queue.rs | 128 +++++++++++ wgpu-hal/src/lib.rs | 14 ++ wgpu-hal/src/vulkan/adapter.rs | 3 +- wgpu-info/src/main.rs | 2 +- wgpu-types/Cargo.toml | 9 + wgpu-types/src/lib.rs | 216 +++++++++++++++++- wgpu/Cargo.toml | 5 +- wgpu/src/backend/direct.rs | 42 ++++ wgpu/src/backend/web.rs | 55 +++-- wgpu/src/context.rs | 32 +++ wgpu/src/lib.rs | 58 +++-- wgpu/tests/3x3_colors.png | Bin 0 -> 87 bytes wgpu/tests/external_texture.rs | 355 ++++++++++++++++++++++++++++++ wgpu/tests/root.rs | 1 + 25 files changed, 1220 insertions(+), 63 deletions(-) create mode 100644 wgpu/tests/3x3_colors.png create mode 100644 wgpu/tests/external_texture.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ec7a595..7ccb3d8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,10 @@ Additionally `Surface::get_default_config` now returns an Option and returns Non `Instance::create_surface()` now returns `Result` instead of `Surface`. This allows an error to be returned instead of panicking if the given window is a HTML canvas and obtaining a WebGPU or WebGL 2 context fails. (No other platforms currently report any errors through this path.) By @kpreid in [#3052](https://github.com/gfx-rs/wgpu/pull/3052/) +#### `Queue::copy_external_image_to_texture` on WebAssembly + +There's a new api `Queue::copy_external_image_to_texture` which allows you to create wgpu textures from various web image primitives. Specificically from HtmlVideoElement, HtmlCanvasElement, OffscreenCanvas, and ImageBitmap. This provides multiple low-copy ways of interacting with the browser. WebGL is also supported, though WebGL has some additional restrictions, represented by the UNRESTRICTED_EXTERNAL_IMAGE_COPIES downlevel flag. By @cwfitzgerald in [#3288](https://github.com/gfx-rs/wgpu/pull/3288) + #### Instance creation now takes `InstanceDescriptor` instead of `Backends` `Instance::new()` and `hub::Global::new()` now take an `InstanceDescriptor` struct which cointains both the existing `Backends` selection as well as a new `Dx12Compiler` field for selecting which Dx12 shader compiler to use. @@ -300,14 +304,12 @@ let texture = device.create_texture(&wgpu::TextureDescriptor { - Don't use a pointer to a local copy of a `PhysicalDeviceDriverProperties` struct after it has gone out of scope. In fact, don't make a local copy at all. Introduce a helper function for building `CStr`s from C character arrays, and remove some `unsafe` blocks. By @jimblandy in [#3076](https://github.com/gfx-rs/wgpu/pull/3076). - ## wgpu-0.14.2 (2022-11-28) ### Bug Fixes - Fix incorrect offset in `get_mapped_range` by @nical in [#3233](https://github.com/gfx-rs/wgpu/pull/3233) - ## wgpu-0.14.1 (2022-11-02) ### Bug Fixes diff --git a/Cargo.lock b/Cargo.lock index 08eebc9dc..32fe03982 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,6 +340,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "com-rs" version = "0.2.1" @@ -1257,6 +1263,20 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "image" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits 0.2.15", + "png", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -1566,6 +1586,27 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits 0.2.15", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits 0.2.15", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -2563,9 +2604,9 @@ dependencies = [ [[package]] name = "v8" -version = "0.60.0" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5867543c19b87c45ed3f2bc49eb6135474ed6a1803cac40c278620b53e9865ef" +checksum = "07fd5b3ed559897ff02c0f62bc0a5f300bfe79bb4c77a50031b8df771701c628" dependencies = [ "bitflags", "fslock", @@ -2925,6 +2966,7 @@ dependencies = [ "env_logger", "futures-intrusive", "glam", + "image", "js-sys", "log", "naga", @@ -3028,8 +3070,10 @@ name = "wgpu-types" version = "0.14.0" dependencies = [ "bitflags", + "js-sys", "serde", "serde_json", + "web-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9fa6412f1..9ede2a782 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ env_logger = "0.9" futures-intrusive = "0.4" fxhash = "0.2.1" glam = "0.21.3" +image = { version = "0.24", default-features = false, features = ["png"] } libloading = "0.7" libc = "0.2" log = "0.4" diff --git a/wgpu-core/src/command/transfer.rs b/wgpu-core/src/command/transfer.rs index 073872f26..29d19e653 100644 --- a/wgpu-core/src/command/transfer.rs +++ b/wgpu-core/src/command/transfer.rs @@ -24,6 +24,7 @@ use std::iter; pub type ImageCopyBuffer = wgt::ImageCopyBuffer; pub type ImageCopyTexture = wgt::ImageCopyTexture; +pub type ImageCopyTextureTagged = wgt::ImageCopyTextureTagged; #[derive(Clone, Copy, Debug)] pub enum CopySide { @@ -44,6 +45,8 @@ pub enum TransferError { MissingCopySrcUsageFlag, #[error("destination buffer/texture is missing the `COPY_DST` usage flag")] MissingCopyDstUsageFlag(Option, Option), + #[error("destination texture is missing the `RENDER_ATTACHMENT` usage flag")] + MissingRenderAttachmentUsageFlag(TextureId), #[error("copy of {start_offset}..{end_offset} would end up overrunning the bounds of the {side:?} buffer of size {buffer_size}")] BufferOverrun { start_offset: BufferAddress, @@ -66,6 +69,8 @@ pub enum TransferError { }, #[error("unable to select texture mip level {level} out of {total}")] InvalidTextureMipLevel { level: u32, total: u32 }, + #[error("texture dimension must be 2D when copying from an external texture")] + InvalidDimensionExternal(TextureId), #[error("buffer offset {0} is not aligned to block size or `COPY_BUFFER_ALIGNMENT`")] UnalignedBufferOffset(BufferAddress), #[error("copy size {0} does not respect `COPY_BUFFER_ALIGNMENT`")] @@ -102,6 +107,10 @@ pub enum TransferError { format: wgt::TextureFormat, aspect: wgt::TextureAspect, }, + #[error( + "copying to textures with format {0:?} is forbidden when copying from external texture" + )] + ExternalCopyToForbiddenTextureFormat(wgt::TextureFormat), #[error("the entire texture must be copied when copying from depth texture")] InvalidDepthTextureExtent, #[error( @@ -701,8 +710,8 @@ impl Global { #[cfg(feature = "trace")] if let Some(ref mut list) = cmd_buf.commands { list.push(TraceCommand::CopyBufferToTexture { - src: source.clone(), - dst: destination.clone(), + src: *source, + dst: *destination, size: *copy_size, }); } @@ -837,8 +846,8 @@ impl Global { #[cfg(feature = "trace")] if let Some(ref mut list) = cmd_buf.commands { list.push(TraceCommand::CopyTextureToBuffer { - src: source.clone(), - dst: destination.clone(), + src: *source, + dst: *destination, size: *copy_size, }); } @@ -1002,8 +1011,8 @@ impl Global { #[cfg(feature = "trace")] if let Some(ref mut list) = cmd_buf.commands { list.push(TraceCommand::CopyTextureToTexture { - src: source.clone(), - dst: destination.clone(), + src: *source, + dst: *destination, size: *copy_size, }); } diff --git a/wgpu-core/src/conv.rs b/wgpu-core/src/conv.rs index e83c0b6c7..52daa335a 100644 --- a/wgpu-core/src/conv.rs +++ b/wgpu-core/src/conv.rs @@ -33,6 +33,30 @@ pub fn is_valid_copy_dst_texture_format( } } +#[cfg_attr( + any(not(target_arch = "wasm32"), feature = "emscripten"), + allow(unused) +)] +pub fn is_valid_external_image_copy_dst_texture_format(format: wgt::TextureFormat) -> bool { + use wgt::TextureFormat as Tf; + match format { + Tf::R8Unorm + | Tf::R16Float + | Tf::R32Float + | Tf::Rg8Unorm + | Tf::Rg16Float + | Tf::Rg32Float + | Tf::Rgba8Unorm + | Tf::Rgba8UnormSrgb + | Tf::Bgra8Unorm + | Tf::Bgra8UnormSrgb + | Tf::Rgb10a2Unorm + | Tf::Rgba16Float + | Tf::Rgba32Float => true, + _ => false, + } +} + pub fn map_buffer_usage(usage: wgt::BufferUsages) -> hal::BufferUses { let mut u = hal::BufferUses::empty(); u.set( diff --git a/wgpu-core/src/device/queue.rs b/wgpu-core/src/device/queue.rs index 03a76edf9..fe86b0ad1 100644 --- a/wgpu-core/src/device/queue.rs +++ b/wgpu-core/src/device/queue.rs @@ -585,7 +585,7 @@ impl Global { let mut trace = trace.lock(); let data_path = trace.make_binary("bin", data); trace.add(Action::WriteTexture { - to: destination.clone(), + to: *destination, data: data_path, layout: *data_layout, size: *size, @@ -663,12 +663,6 @@ impl Global { (size.depth_or_array_layers - 1) * block_rows_per_image + height_blocks; let stage_size = stage_bytes_per_row as u64 * block_rows_in_copy as u64; - if !dst.desc.usage.contains(wgt::TextureUsages::COPY_DST) { - return Err( - TransferError::MissingCopyDstUsageFlag(None, Some(destination.texture)).into(), - ); - } - let mut trackers = device.trackers.lock(); let encoder = device.pending_writes.activate(); @@ -820,6 +814,208 @@ impl Global { Ok(()) } + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + pub fn queue_copy_external_image_to_texture( + &self, + queue_id: id::QueueId, + source: &wgt::ImageCopyExternalImage, + destination: crate::command::ImageCopyTextureTagged, + size: wgt::Extent3d, + ) -> Result<(), QueueWriteError> { + profiling::scope!("Queue::copy_external_image_to_texture"); + + let hub = A::hub(self); + let mut token = Token::root(); + let (mut device_guard, mut token) = hub.devices.write(&mut token); + let device = device_guard + .get_mut(queue_id) + .map_err(|_| DeviceError::Invalid)?; + + if size.width == 0 || size.height == 0 || size.depth_or_array_layers == 0 { + log::trace!("Ignoring write_texture of size 0"); + return Ok(()); + } + + let mut needs_flag = false; + needs_flag |= matches!(source.source, wgt::ExternalImageSource::OffscreenCanvas(_)); + needs_flag |= source.origin != wgt::Origin2d::ZERO; + needs_flag |= destination.color_space != wgt::PredefinedColorSpace::Srgb; + #[allow(clippy::bool_comparison)] + if matches!(source.source, wgt::ExternalImageSource::ImageBitmap(_)) { + needs_flag |= source.flip_y != false; + needs_flag |= destination.premultiplied_alpha != false; + } + + if needs_flag { + device + .require_downlevel_flags(wgt::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES) + .map_err(TransferError::from)?; + } + + let src_width = source.source.width(); + let src_height = source.source.height(); + + let (mut texture_guard, _) = hub.textures.write(&mut token); // For clear we need write access to the texture. TODO: Can we acquire write lock later? + let dst = texture_guard.get_mut(destination.texture).unwrap(); + + let (selector, dst_base, _) = + extract_texture_selector(&destination.to_untagged(), &size, dst)?; + + if !conv::is_valid_external_image_copy_dst_texture_format(dst.desc.format) { + return Err( + TransferError::ExternalCopyToForbiddenTextureFormat(dst.desc.format).into(), + ); + } + if dst.desc.dimension != wgt::TextureDimension::D2 { + return Err(TransferError::InvalidDimensionExternal(destination.texture).into()); + } + if !dst.desc.usage.contains(wgt::TextureUsages::COPY_DST) { + return Err( + TransferError::MissingCopyDstUsageFlag(None, Some(destination.texture)).into(), + ); + } + if !dst + .desc + .usage + .contains(wgt::TextureUsages::RENDER_ATTACHMENT) + { + return Err( + TransferError::MissingRenderAttachmentUsageFlag(destination.texture).into(), + ); + } + if dst.desc.sample_count != 1 { + return Err(TransferError::InvalidSampleCount { + sample_count: dst.desc.sample_count, + } + .into()); + } + + if source.origin.x + size.width > src_width { + return Err(TransferError::TextureOverrun { + start_offset: source.origin.x, + end_offset: source.origin.x + size.width, + texture_size: src_width, + dimension: crate::resource::TextureErrorDimension::X, + side: CopySide::Source, + } + .into()); + } + if source.origin.y + size.height > src_height { + return Err(TransferError::TextureOverrun { + start_offset: source.origin.y, + end_offset: source.origin.y + size.height, + texture_size: src_height, + dimension: crate::resource::TextureErrorDimension::Y, + side: CopySide::Source, + } + .into()); + } + if size.depth_or_array_layers != 1 { + return Err(TransferError::TextureOverrun { + start_offset: 0, + end_offset: size.depth_or_array_layers, + texture_size: 1, + dimension: crate::resource::TextureErrorDimension::Z, + side: CopySide::Source, + } + .into()); + } + + // Note: Doing the copy range validation early is important because ensures that the + // dimensions are not going to cause overflow in other parts of the validation. + let (hal_copy_size, _) = validate_texture_copy_range( + &destination.to_untagged(), + &dst.desc, + CopySide::Destination, + &size, + )?; + + let mut trackers = device.trackers.lock(); + let encoder = device.pending_writes.activate(); + + // If the copy does not fully cover the layers, we need to initialize to + // zero *first* as we don't keep track of partial texture layer inits. + // + // Strictly speaking we only need to clear the areas of a layer + // untouched, but this would get increasingly messy. + let init_layer_range = if dst.desc.dimension == wgt::TextureDimension::D3 { + // volume textures don't have a layer range as array volumes aren't supported + 0..1 + } else { + destination.origin.z..destination.origin.z + size.depth_or_array_layers + }; + if dst.initialization_status.mips[destination.mip_level as usize] + .check(init_layer_range.clone()) + .is_some() + { + if has_copy_partial_init_tracker_coverage(&size, destination.mip_level, &dst.desc) { + for layer_range in dst.initialization_status.mips[destination.mip_level as usize] + .drain(init_layer_range) + .collect::>>() + { + crate::command::clear_texture( + &*texture_guard, + id::Valid(destination.texture), + TextureInitRange { + mip_range: destination.mip_level..(destination.mip_level + 1), + layer_range, + }, + encoder, + &mut trackers.textures, + &device.alignments, + &device.zero_buffer, + ) + .map_err(QueueWriteError::from)?; + } + } else { + dst.initialization_status.mips[destination.mip_level as usize] + .drain(init_layer_range); + } + } + + let dst = texture_guard.get(destination.texture).unwrap(); + + let transitions = trackers + .textures + .set_single( + dst, + destination.texture, + selector, + hal::TextureUses::COPY_DST, + ) + .ok_or(TransferError::InvalidTexture(destination.texture))?; + + dst.life_guard.use_at(device.active_submission_index + 1); + + let dst_raw = dst + .inner + .as_raw() + .ok_or(TransferError::InvalidTexture(destination.texture))?; + + let regions = hal::TextureCopy { + src_base: hal::TextureCopyBase { + mip_level: 0, + array_layer: 0, + origin: source.origin.to_3d(0), + aspect: hal::FormatAspects::COLOR, + }, + dst_base, + size: hal_copy_size, + }; + + unsafe { + encoder.transition_textures(transitions.map(|pending| pending.into_hal(dst))); + encoder.copy_external_image_to_texture( + source, + dst_raw, + destination.premultiplied_alpha, + iter::once(regions), + ); + } + + Ok(()) + } + pub fn queue_submit( &self, queue_id: id::QueueId, diff --git a/wgpu-hal/src/dx11/adapter.rs b/wgpu-hal/src/dx11/adapter.rs index 6e14b42f5..13b3e92cf 100644 --- a/wgpu-hal/src/dx11/adapter.rs +++ b/wgpu-hal/src/dx11/adapter.rs @@ -97,7 +97,8 @@ impl super::Adapter { | wgt::Features::ADDRESS_MODE_CLAMP_TO_ZERO; let mut downlevel = wgt::DownlevelFlags::BASE_VERTEX | wgt::DownlevelFlags::READ_ONLY_DEPTH_STENCIL - | wgt::DownlevelFlags::UNRESTRICTED_INDEX_BUFFER; + | wgt::DownlevelFlags::UNRESTRICTED_INDEX_BUFFER + | wgt::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES; // Features from queries downlevel.set( diff --git a/wgpu-hal/src/empty.rs b/wgpu-hal/src/empty.rs index 30e8156e8..5e112b126 100644 --- a/wgpu-hal/src/empty.rs +++ b/wgpu-hal/src/empty.rs @@ -264,6 +264,18 @@ impl crate::CommandEncoder for Encoder { unsafe fn copy_buffer_to_buffer(&mut self, src: &Resource, dst: &Resource, regions: T) {} + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + unsafe fn copy_external_image_to_texture( + &mut self, + src: &wgt::ImageCopyExternalImage, + dst: &Resource, + dst_premultiplication: bool, + regions: T, + ) where + T: Iterator, + { + } + unsafe fn copy_texture_to_texture( &mut self, src: &Resource, diff --git a/wgpu-hal/src/gles/adapter.rs b/wgpu-hal/src/gles/adapter.rs index ad7cdd475..ccdbd564b 100644 --- a/wgpu-hal/src/gles/adapter.rs +++ b/wgpu-hal/src/gles/adapter.rs @@ -317,6 +317,10 @@ impl super::Adapter { wgt::DownlevelFlags::UNRESTRICTED_INDEX_BUFFER, !cfg!(target_arch = "wasm32"), ); + downlevel_flags.set( + wgt::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES, + !cfg!(target_arch = "wasm32"), + ); downlevel_flags.set( wgt::DownlevelFlags::FULL_DRAW_INDEX_UINT32, max_element_index == u32::MAX, diff --git a/wgpu-hal/src/gles/command.rs b/wgpu-hal/src/gles/command.rs index 4c478ee51..0756637c6 100644 --- a/wgpu-hal/src/gles/command.rs +++ b/wgpu-hal/src/gles/command.rs @@ -309,6 +309,31 @@ impl crate::CommandEncoder for super::CommandEncoder { } } + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + unsafe fn copy_external_image_to_texture( + &mut self, + src: &wgt::ImageCopyExternalImage, + dst: &super::Texture, + dst_premultiplication: bool, + regions: T, + ) where + T: Iterator, + { + let (dst_raw, dst_target) = dst.inner.as_native(); + for copy in regions { + self.cmd_buffer + .commands + .push(C::CopyExternalImageToTexture { + src: src.clone(), + dst: dst_raw, + dst_target, + dst_format: dst.format, + dst_premultiplication, + copy, + }) + } + } + unsafe fn copy_texture_to_texture( &mut self, src: &super::Texture, diff --git a/wgpu-hal/src/gles/mod.rs b/wgpu-hal/src/gles/mod.rs index 31b11a187..4ff293b54 100644 --- a/wgpu-hal/src/gles/mod.rs +++ b/wgpu-hal/src/gles/mod.rs @@ -684,6 +684,15 @@ enum Command { dst_target: BindTarget, copy: crate::BufferCopy, }, + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + CopyExternalImageToTexture { + src: wgt::ImageCopyExternalImage, + dst: glow::Texture, + dst_target: BindTarget, + dst_format: wgt::TextureFormat, + dst_premultiplication: bool, + copy: crate::TextureCopy, + }, CopyTextureToTexture { src: glow::Texture, src_target: BindTarget, diff --git a/wgpu-hal/src/gles/queue.rs b/wgpu-hal/src/gles/queue.rs index 75770c501..e57fecabb 100644 --- a/wgpu-hal/src/gles/queue.rs +++ b/wgpu-hal/src/gles/queue.rs @@ -375,6 +375,134 @@ impl super::Queue { unsafe { gl.bind_buffer(copy_dst_target, None) }; } } + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + C::CopyExternalImageToTexture { + ref src, + dst, + dst_target, + dst_format, + dst_premultiplication, + ref copy, + } => { + const UNPACK_FLIP_Y_WEBGL: u32 = + web_sys::WebGl2RenderingContext::UNPACK_FLIP_Y_WEBGL; + const UNPACK_PREMULTIPLY_ALPHA_WEBGL: u32 = + web_sys::WebGl2RenderingContext::UNPACK_PREMULTIPLY_ALPHA_WEBGL; + + unsafe { + if src.flip_y { + gl.pixel_store_bool(UNPACK_FLIP_Y_WEBGL, true); + } + if dst_premultiplication { + gl.pixel_store_bool(UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + } + } + + unsafe { gl.bind_texture(dst_target, Some(dst)) }; + let format_desc = self.shared.describe_texture_format(dst_format); + if is_layered_target(dst_target) { + match src.source { + wgt::ExternalImageSource::ImageBitmap(ref b) => unsafe { + gl.tex_sub_image_3d_with_image_bitmap( + dst_target, + copy.dst_base.mip_level as i32, + copy.dst_base.origin.x as i32, + copy.dst_base.origin.y as i32, + copy.dst_base.origin.z as i32, + copy.size.width as i32, + copy.size.height as i32, + copy.size.depth as i32, + format_desc.external, + format_desc.data_type, + b, + ); + }, + wgt::ExternalImageSource::HTMLVideoElement(ref v) => unsafe { + gl.tex_sub_image_3d_with_html_video_element( + dst_target, + copy.dst_base.mip_level as i32, + copy.dst_base.origin.x as i32, + copy.dst_base.origin.y as i32, + copy.dst_base.origin.z as i32, + copy.size.width as i32, + copy.size.height as i32, + copy.size.depth as i32, + format_desc.external, + format_desc.data_type, + v, + ); + }, + wgt::ExternalImageSource::HTMLCanvasElement(ref c) => unsafe { + gl.tex_sub_image_3d_with_html_canvas_element( + dst_target, + copy.dst_base.mip_level as i32, + copy.dst_base.origin.x as i32, + copy.dst_base.origin.y as i32, + copy.dst_base.origin.z as i32, + copy.size.width as i32, + copy.size.height as i32, + copy.size.depth as i32, + format_desc.external, + format_desc.data_type, + c, + ); + }, + wgt::ExternalImageSource::OffscreenCanvas(_) => unreachable!(), + } + } else { + match src.source { + wgt::ExternalImageSource::ImageBitmap(ref b) => unsafe { + gl.tex_sub_image_2d_with_image_bitmap_and_width_and_height( + dst_target, + copy.dst_base.mip_level as i32, + copy.dst_base.origin.x as i32, + copy.dst_base.origin.y as i32, + copy.size.width as i32, + copy.size.height as i32, + format_desc.external, + format_desc.data_type, + b, + ); + }, + wgt::ExternalImageSource::HTMLVideoElement(ref v) => unsafe { + gl.tex_sub_image_2d_with_html_video_and_width_and_height( + dst_target, + copy.dst_base.mip_level as i32, + copy.dst_base.origin.x as i32, + copy.dst_base.origin.y as i32, + copy.size.width as i32, + copy.size.height as i32, + format_desc.external, + format_desc.data_type, + v, + ) + }, + wgt::ExternalImageSource::HTMLCanvasElement(ref c) => unsafe { + gl.tex_sub_image_2d_with_html_canvas_and_width_and_height( + dst_target, + copy.dst_base.mip_level as i32, + copy.dst_base.origin.x as i32, + copy.dst_base.origin.y as i32, + copy.size.width as i32, + copy.size.height as i32, + format_desc.external, + format_desc.data_type, + c, + ) + }, + wgt::ExternalImageSource::OffscreenCanvas(_) => unreachable!(), + } + } + + unsafe { + if src.flip_y { + gl.pixel_store_bool(UNPACK_FLIP_Y_WEBGL, false); + } + if dst_premultiplication { + gl.pixel_store_bool(UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + } + } + } C::CopyTextureToTexture { src, src_target, diff --git a/wgpu-hal/src/lib.rs b/wgpu-hal/src/lib.rs index 8d9b876a7..710891fac 100644 --- a/wgpu-hal/src/lib.rs +++ b/wgpu-hal/src/lib.rs @@ -395,6 +395,20 @@ pub trait CommandEncoder: Send + Sync + fmt::Debug { where T: Iterator; + /// Copy from an external image to an internal texture. + /// Works with a single array layer. + /// Note: `dst` current usage has to be `TextureUses::COPY_DST`. + /// Note: the copy extent is in physical size (rounded to the block size) + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + unsafe fn copy_external_image_to_texture( + &mut self, + src: &wgt::ImageCopyExternalImage, + dst: &A::Texture, + dst_premultiplication: bool, + regions: T, + ) where + T: Iterator; + /// Copy from one texture to another. /// Works with a single array layer. /// Note: `dst` current usage has to be `TextureUses::COPY_DST`. diff --git a/wgpu-hal/src/vulkan/adapter.rs b/wgpu-hal/src/vulkan/adapter.rs index f76d3cc79..cdf06e936 100644 --- a/wgpu-hal/src/vulkan/adapter.rs +++ b/wgpu-hal/src/vulkan/adapter.rs @@ -319,7 +319,8 @@ impl PhysicalDeviceFeatures { | Df::BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED | Df::UNRESTRICTED_INDEX_BUFFER | Df::INDIRECT_EXECUTION - | Df::VIEW_FORMATS; + | Df::VIEW_FORMATS + | Df::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES; dl_flags.set(Df::CUBE_ARRAY_TEXTURES, self.core.image_cube_array != 0); dl_flags.set(Df::ANISOTROPIC_FILTERING, self.core.sampler_anisotropy != 0); diff --git a/wgpu-info/src/main.rs b/wgpu-info/src/main.rs index 18112a336..76879a8d5 100644 --- a/wgpu-info/src/main.rs +++ b/wgpu-info/src/main.rs @@ -225,7 +225,7 @@ mod inner { let bit = wgpu::DownlevelFlags::from_bits(1 << i as u64); if let Some(bit) = bit { if wgpu::DownlevelFlags::all().contains(bit) { - println!("\t\t{:>36} {}", format!("{:?}:", bit), flags.contains(bit)); + println!("\t\t{:>37} {}", format!("{:?}:", bit), flags.contains(bit)); } } } diff --git a/wgpu-types/Cargo.toml b/wgpu-types/Cargo.toml index 4ecef7427..d1f47e9ff 100644 --- a/wgpu-types/Cargo.toml +++ b/wgpu-types/Cargo.toml @@ -24,6 +24,15 @@ strict_asserts = [] bitflags = "1" serde = { version = "1", features = ["serde_derive"], optional = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys.workspace = true +web-sys = { workspace = true, features = [ + "ImageBitmap", + "HtmlVideoElement", + "HtmlCanvasElement", + "OffscreenCanvas", +] } + [dev-dependencies] serde = { version = "1", features = ["serde_derive"] } serde_json = "1.0.85" diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index 2c91e915b..6338ae176 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -1129,7 +1129,7 @@ bitflags::bitflags! { /// Supports buffers to combine [`BufferUsages::INDEX`] with usages other than [`BufferUsages::COPY_DST`] and [`BufferUsages::COPY_SRC`]. /// Furthermore, in absence of this feature it is not allowed to copy index buffers from/to buffers with a set of usage flags containing - /// [`BufferUsages::VERTEX`]/[`BufferUsages::UNIFORM`]/[`BufferUsages::STORAGE`] or [`BufferUsages::INDIRECT`]. + /// [`BufferUsages::VERTEX`]/[`BufferUsages::UNIFORM`]/[`BufferUsages::STORAGE`] or [`BufferUsages::INDIRECT`]. /// /// WebGL doesn't support this. const UNRESTRICTED_INDEX_BUFFER = 1 << 16; @@ -1148,6 +1148,17 @@ bitflags::bitflags! { /// /// The WebGL and GLES backends doesn't support this. const VIEW_FORMATS = 1 << 19; + + /// With this feature not present, there are the following restrictions on `Queue::copy_external_image_to_texture`: + /// - The source must not be [`web_sys::OffscreenCanvas`] + /// - [`ImageCopyExternalImage::origin`] must be zero. + /// - [`ImageCopyTextureTagged::color_space`] must be srgb. + /// - If the source is an [`web_sys::ImageBitmap`]: + /// - [`ImageCopyExternalImage::flip_y`] must be false. + /// - [`ImageCopyTextureTagged::premultiplied_alpha`] must be false. + /// + /// WebGL doesn't support this. WebGPU does. + const UNRESTRICTED_EXTERNAL_TEXTURE_COPIES = 1 << 20; } } @@ -4124,10 +4135,40 @@ pub enum TextureDimension { D3, } +/// Origin of a copy from a 2D image. +/// +/// Corresponds to [WebGPU `GPUOrigin2D`]( +/// https://gpuweb.github.io/gpuweb/#dictdef-gpuorigin2ddict). +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "trace", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct Origin2d { + /// + pub x: u32, + /// + pub y: u32, +} + +impl Origin2d { + /// Zero origin. + pub const ZERO: Self = Self { x: 0, y: 0 }; + + /// Adds the third dimension to this origin + pub fn to_3d(self, z: u32) -> Origin3d { + Origin3d { + x: self.x, + y: self.y, + z, + } + } +} + /// Origin of a copy to/from a texture. /// /// Corresponds to [WebGPU `GPUOrigin3D`]( -/// https://gpuweb.github.io/gpuweb/#typedefdef-gpuorigin3d). +/// https://gpuweb.github.io/gpuweb/#dictdef-gpuorigin3ddict). #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trace", derive(Serialize))] @@ -4145,6 +4186,14 @@ pub struct Origin3d { impl Origin3d { /// Zero origin. pub const ZERO: Self = Self { x: 0, y: 0, z: 0 }; + + /// Removes the third dimension from this origin + pub fn to_2d(self) -> Origin2d { + Origin2d { + x: self.x, + y: self.y, + } + } } impl Default for Origin3d { @@ -4156,7 +4205,7 @@ impl Default for Origin3d { /// Extent of a texture related operation. /// /// Corresponds to [WebGPU `GPUExtent3D`]( -/// https://gpuweb.github.io/gpuweb/#typedefdef-gpuextent3d). +/// https://gpuweb.github.io/gpuweb/#dictdef-gpuextent3ddict). #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "trace", derive(Serialize))] @@ -5048,7 +5097,7 @@ pub struct BindGroupLayoutEntry { /// Corresponds to [WebGPU `GPUImageCopyBuffer`]( /// https://gpuweb.github.io/gpuweb/#dictdef-gpuimagecopybuffer). #[repr(C)] -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] #[cfg_attr(feature = "trace", derive(serde::Serialize))] #[cfg_attr(feature = "replay", derive(serde::Deserialize))] pub struct ImageCopyBuffer { @@ -5063,7 +5112,7 @@ pub struct ImageCopyBuffer { /// Corresponds to [WebGPU `GPUImageCopyTexture`]( /// https://gpuweb.github.io/gpuweb/#dictdef-gpuimagecopytexture). #[repr(C)] -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] #[cfg_attr(feature = "trace", derive(serde::Serialize))] #[cfg_attr(feature = "replay", derive(serde::Deserialize))] pub struct ImageCopyTexture { @@ -5071,7 +5120,9 @@ pub struct ImageCopyTexture { pub texture: T, /// The target mip level of the texture. pub mip_level: u32, - /// The base texel of the texture in the selected `mip_level`. + /// The base texel of the texture in the selected `mip_level`. Together + /// with the `copy_size` argument to copy functions, defines the + /// sub-region of the texture to copy. #[cfg_attr(any(feature = "trace", feature = "replay"), serde(default))] pub origin: Origin3d, /// The copy aspect. @@ -5079,6 +5130,159 @@ pub struct ImageCopyTexture { pub aspect: TextureAspect, } +impl ImageCopyTexture { + /// Adds color space and premultiplied alpha information to make this + /// descriptor tagged. + pub fn to_tagged( + self, + color_space: PredefinedColorSpace, + premultiplied_alpha: bool, + ) -> ImageCopyTextureTagged { + ImageCopyTextureTagged { + texture: self.texture, + mip_level: self.mip_level, + origin: self.origin, + aspect: self.aspect, + color_space, + premultiplied_alpha, + } + } +} + +/// View of an external texture that cna be used to copy to a texture. +/// +/// Corresponds to [WebGPU `GPUImageCopyExternalImage`]( +/// https://gpuweb.github.io/gpuweb/#dictdef-gpuimagecopyexternalimage). +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Debug)] +pub struct ImageCopyExternalImage { + /// The texture to be copied from. The copy source data is captured at the moment + /// the copy is issued. + pub source: ExternalImageSource, + /// The base texel used for copying from the external image. Together + /// with the `copy_size` argument to copy functions, defines the + /// sub-region of the image to copy. + /// + /// Relative to the top left of the image. + /// + /// Must be [`Origin2d::ZERO`] if [`DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES`] is not supported. + pub origin: Origin2d, + /// If the Y coordinate of the image should be flipped. Even if this is + /// true, `origin` is still relative to the top left. + pub flip_y: bool, +} + +/// Source of an external texture copy. +/// +/// Corresponds to the [implicit union type on WebGPU `GPUImageCopyExternalImage.source`]( +/// https://gpuweb.github.io/gpuweb/#dom-gpuimagecopyexternalimage-source). +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Debug)] +pub enum ExternalImageSource { + /// Copy from a previously-decoded image bitmap. + ImageBitmap(web_sys::ImageBitmap), + /// Copy from a current frame of a video element. + HTMLVideoElement(web_sys::HtmlVideoElement), + /// Copy from a on-screen canvas. + HTMLCanvasElement(web_sys::HtmlCanvasElement), + /// Copy from a off-screen canvas. + /// + /// Requies [`DownlevelFlags::EXTERNAL_TEXTURE_OFFSCREEN_CANVAS`] + OffscreenCanvas(web_sys::OffscreenCanvas), +} + +#[cfg(target_arch = "wasm32")] +impl ExternalImageSource { + /// Gets the pixel, not css, width of the source. + pub fn width(&self) -> u32 { + match self { + ExternalImageSource::ImageBitmap(b) => b.width(), + ExternalImageSource::HTMLVideoElement(v) => v.video_width(), + ExternalImageSource::HTMLCanvasElement(c) => c.width(), + ExternalImageSource::OffscreenCanvas(c) => c.width(), + } + } + + /// Gets the pixel, not css, height of the source. + pub fn height(&self) -> u32 { + match self { + ExternalImageSource::ImageBitmap(b) => b.height(), + ExternalImageSource::HTMLVideoElement(v) => v.video_height(), + ExternalImageSource::HTMLCanvasElement(c) => c.height(), + ExternalImageSource::OffscreenCanvas(c) => c.height(), + } + } +} + +#[cfg(target_arch = "wasm32")] +impl std::ops::Deref for ExternalImageSource { + type Target = js_sys::Object; + + fn deref(&self) -> &Self::Target { + match self { + Self::ImageBitmap(b) => b, + Self::HTMLVideoElement(v) => v, + Self::HTMLCanvasElement(c) => c, + Self::OffscreenCanvas(c) => c, + } + } +} + +#[cfg(target_arch = "wasm32")] +unsafe impl Send for ExternalImageSource {} +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for ExternalImageSource {} + +/// Color spaces supported on the web. +/// +/// Corresponds to [HTML Canvas `PredefinedColorSpace`]( +/// https://html.spec.whatwg.org/multipage/canvas.html#predefinedcolorspace). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "trace", derive(serde::Serialize))] +#[cfg_attr(feature = "replay", derive(serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] +pub enum PredefinedColorSpace { + /// sRGB color space + Srgb, + /// Display-P3 color space + DisplayP3, +} + +/// View of a texture which can be used to copy to a texture, including +/// color space and alpha premultiplication information. +/// +/// Corresponds to [WebGPU `GPUImageCopyTextureTagged`]( +/// https://gpuweb.github.io/gpuweb/#dictdef-gpuimagecopytexturetagged). +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "trace", derive(serde::Serialize))] +#[cfg_attr(feature = "replay", derive(serde::Deserialize))] +pub struct ImageCopyTextureTagged { + /// The texture to be copied to/from. + pub texture: T, + /// The target mip level of the texture. + pub mip_level: u32, + /// The base texel of the texture in the selected `mip_level`. + pub origin: Origin3d, + /// The copy aspect. + pub aspect: TextureAspect, + /// The color space of this texture. + pub color_space: PredefinedColorSpace, + /// The premultiplication of this texture + pub premultiplied_alpha: bool, +} + +impl ImageCopyTextureTagged { + /// Removes the colorspace information from the type. + pub fn to_untagged(self) -> ImageCopyTexture { + ImageCopyTexture { + texture: self.texture, + mip_level: self.mip_level, + origin: self.origin, + aspect: self.aspect, + } + } +} + /// Subresource range within an image #[repr(C)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index f10f10b9d..2fe881d98 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -165,6 +165,7 @@ glam.workspace = true ddsfile.workspace = true futures-intrusive.workspace = true env_logger.workspace = true +image.workspace = true log.workspace = true noise = { workspace = true } obj.workspace = true @@ -241,6 +242,7 @@ web-sys = { workspace = true, features = [ "GpuDeviceLostReason", "GpuError", "GpuErrorFilter", + # "GpuExtent2dDict", Not yet implemented in web_sys "GpuExtent3dDict", "GpuFeatureName", "GpuFilterMode", @@ -337,5 +339,6 @@ web-sys = { workspace = true, features = [ "RequestMode", "Request", "Response", - "WebGl2RenderingContext" + "WebGl2RenderingContext", + "CanvasRenderingContext2d" ] } diff --git a/wgpu/src/backend/direct.rs b/wgpu/src/backend/direct.rs index 77ad50ea2..df3eb4c3a 100644 --- a/wgpu/src/backend/direct.rs +++ b/wgpu/src/backend/direct.rs @@ -352,6 +352,23 @@ fn map_texture_copy_view(view: crate::ImageCopyTexture) -> wgc::command::ImageCo } } +#[cfg_attr( + any(not(target_arch = "wasm32"), feature = "emscripten"), + allow(unused) +)] +fn map_texture_tagged_copy_view( + view: crate::ImageCopyTextureTagged, +) -> wgc::command::ImageCopyTextureTagged { + wgc::command::ImageCopyTextureTagged { + texture: view.texture.id.into(), + mip_level: view.mip_level, + origin: view.origin, + aspect: view.aspect, + color_space: view.color_space, + premultiplied_alpha: view.premultiplied_alpha, + } +} + fn map_pass_channel( ops: Option<&Operations>, ) -> wgc::command::PassChannel { @@ -2193,6 +2210,31 @@ impl crate::Context for Context { } } + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + fn queue_copy_external_image_to_texture( + &self, + queue: &Self::QueueId, + queue_data: &Self::QueueData, + source: &wgt::ImageCopyExternalImage, + dest: crate::ImageCopyTextureTagged, + size: wgt::Extent3d, + ) { + let global = &self.0; + match wgc::gfx_select!(*queue => global.queue_copy_external_image_to_texture( + *queue, + source, + map_texture_tagged_copy_view(dest), + size + )) { + Ok(()) => (), + Err(err) => self.handle_error_nolabel( + &queue_data.error_sink, + err, + "Queue::copy_external_image_to_texture", + ), + } + } + fn queue_submit>( &self, queue: &Self::QueueId, diff --git a/wgpu/src/backend/web.rs b/wgpu/src/backend/web.rs index 8bb7b5ad6..658559adb 100644 --- a/wgpu/src/backend/web.rs +++ b/wgpu/src/backend/web.rs @@ -419,6 +419,13 @@ fn map_extent_3d(extent: wgt::Extent3d) -> web_sys::GpuExtent3dDict { mapped } +fn map_origin_2d(extent: wgt::Origin2d) -> web_sys::GpuOrigin2dDict { + let mut mapped = web_sys::GpuOrigin2dDict::new(); + mapped.x(extent.x); + mapped.y(extent.y); + mapped +} + fn map_origin_3d(origin: wgt::Origin3d) -> web_sys::GpuOrigin3dDict { let mut mapped = web_sys::GpuOrigin3dDict::new(); mapped.x(origin.x); @@ -471,12 +478,24 @@ fn map_texture_copy_view(view: crate::ImageCopyTexture) -> web_sys::GpuImageCopy } fn map_tagged_texture_copy_view( - view: crate::ImageCopyTexture, + view: crate::ImageCopyTextureTagged, ) -> web_sys::GpuImageCopyTextureTagged { let texture = &<::TextureId>::from(view.texture.id).0; let mut mapped = web_sys::GpuImageCopyTextureTagged::new(texture); mapped.mip_level(view.mip_level); mapped.origin(&map_origin_3d(view.origin)); + mapped.aspect(map_texture_aspect(view.aspect)); + // mapped.color_space(map_color_space(view.color_space)); + mapped.premultiplied_alpha(view.premultiplied_alpha); + mapped +} + +fn map_external_texture_copy_view( + view: &crate::ImageCopyExternalImage, +) -> web_sys::GpuImageCopyExternalImage { + let mut mapped = web_sys::GpuImageCopyExternalImage::new(&view.source); + mapped.origin(&map_origin_2d(view.origin)); + mapped.flip_y(view.flip_y); mapped } @@ -703,22 +722,6 @@ impl Context { Ok(create_identified(context)) } - - pub fn queue_copy_external_image_to_texture( - &self, - queue: &Identified, - image: &web_sys::ImageBitmap, - texture: crate::ImageCopyTexture, - size: wgt::Extent3d, - ) { - queue - .0 - .copy_external_image_to_texture_with_gpu_extent_3d_dict( - &web_sys::GpuImageCopyExternalImage::new(image), - &map_tagged_texture_copy_view(texture), - &map_extent_3d(size), - ); - } } // Represents the global object in the JavaScript context. @@ -2398,6 +2401,24 @@ impl crate::context::Context for Context { ); } + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + fn queue_copy_external_image_to_texture( + &self, + queue: &Self::QueueId, + _queue_data: &Self::QueueData, + source: &wgt::ImageCopyExternalImage, + dest: crate::ImageCopyTextureTagged, + size: wgt::Extent3d, + ) { + queue + .0 + .copy_external_image_to_texture_with_gpu_extent_3d_dict( + &map_external_texture_copy_view(source), + &map_tagged_texture_copy_view(dest), + &map_extent_3d(size), + ); + } + fn queue_submit>( &self, queue: &Self::QueueId, diff --git a/wgpu/src/context.rs b/wgpu/src/context.rs index 6ebc516d0..e8eebbbe2 100644 --- a/wgpu/src/context.rs +++ b/wgpu/src/context.rs @@ -561,6 +561,15 @@ pub trait Context: Debug + Send + Sized + Sync { data_layout: ImageDataLayout, size: Extent3d, ); + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + fn queue_copy_external_image_to_texture( + &self, + queue: &Self::QueueId, + queue_data: &Self::QueueData, + source: &wgt::ImageCopyExternalImage, + dest: crate::ImageCopyTextureTagged, + size: wgt::Extent3d, + ); fn queue_submit>( &self, queue: &Self::QueueId, @@ -1479,6 +1488,15 @@ pub(crate) trait DynContext: Debug + Send + Sync { data_layout: ImageDataLayout, size: Extent3d, ); + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + fn queue_copy_external_image_to_texture( + &self, + queue: &ObjectId, + queue_data: &crate::Data, + source: &wgt::ImageCopyExternalImage, + dest: crate::ImageCopyTextureTagged, + size: wgt::Extent3d, + ); fn queue_submit<'a>( &self, queue: &ObjectId, @@ -2866,6 +2884,20 @@ where Context::queue_write_texture(self, &queue, queue_data, texture, data, data_layout, size) } + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] + fn queue_copy_external_image_to_texture( + &self, + queue: &ObjectId, + queue_data: &crate::Data, + source: &wgt::ImageCopyExternalImage, + dest: crate::ImageCopyTextureTagged, + size: wgt::Extent3d, + ) { + let queue = ::from(*queue); + let queue_data = downcast_ref(queue_data); + Context::queue_copy_external_image_to_texture(self, &queue, queue_data, source, dest, size) + } + fn queue_submit<'a>( &self, queue: &ObjectId, diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 780436773..512b70eaf 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -35,18 +35,26 @@ pub use wgt::{ CommandBufferDescriptor, CompareFunction, CompositeAlphaMode, DepthBiasState, DepthStencilState, DeviceType, DownlevelCapabilities, DownlevelFlags, Dx12Compiler, DynamicOffset, Extent3d, Face, Features, FilterMode, FrontFace, ImageDataLayout, - ImageSubresourceRange, IndexFormat, InstanceDescriptor, Limits, MultisampleState, Origin3d, - PipelineStatisticsTypes, PolygonMode, PowerPreference, PresentMode, PresentationTimestamp, - PrimitiveState, PrimitiveTopology, PushConstantRange, QueryType, RenderBundleDepthStencil, - SamplerBindingType, SamplerBorderColor, ShaderLocation, ShaderModel, ShaderStages, - StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, SurfaceCapabilities, - SurfaceConfiguration, SurfaceStatus, TextureAspect, TextureDimension, TextureFormat, - TextureFormatFeatureFlags, TextureFormatFeatures, TextureSampleType, TextureUsages, - TextureViewDimension, VertexAttribute, VertexFormat, VertexStepMode, COPY_BUFFER_ALIGNMENT, - COPY_BYTES_PER_ROW_ALIGNMENT, MAP_ALIGNMENT, PUSH_CONSTANT_ALIGNMENT, - QUERY_RESOLVE_BUFFER_ALIGNMENT, QUERY_SET_MAX_QUERIES, QUERY_SIZE, VERTEX_STRIDE_ALIGNMENT, + ImageSubresourceRange, IndexFormat, InstanceDescriptor, Limits, MultisampleState, Origin2d, + Origin3d, PipelineStatisticsTypes, PolygonMode, PowerPreference, PredefinedColorSpace, + PresentMode, PresentationTimestamp, PrimitiveState, PrimitiveTopology, PushConstantRange, + QueryType, RenderBundleDepthStencil, SamplerBindingType, SamplerBorderColor, ShaderLocation, + ShaderModel, ShaderStages, StencilFaceState, StencilOperation, StencilState, + StorageTextureAccess, SurfaceCapabilities, SurfaceConfiguration, SurfaceStatus, TextureAspect, + TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, + TextureSampleType, TextureUsages, TextureViewDimension, VertexAttribute, VertexFormat, + VertexStepMode, COPY_BUFFER_ALIGNMENT, COPY_BYTES_PER_ROW_ALIGNMENT, MAP_ALIGNMENT, + PUSH_CONSTANT_ALIGNMENT, QUERY_RESOLVE_BUFFER_ALIGNMENT, QUERY_SET_MAX_QUERIES, QUERY_SIZE, + VERTEX_STRIDE_ALIGNMENT, }; +// wasm-only types, we try to keep as many types non-platform +// specific, but these need to depend on web-sys. +#[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] +pub use wgt::{ExternalImageSource, ImageCopyExternalImage}; +#[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] +static_assertions::assert_impl_all!(ExternalImageSource: Send, Sync); + /// Filter for error scopes. #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)] pub enum ErrorFilter { @@ -60,7 +68,7 @@ static_assertions::assert_impl_all!(ErrorFilter: Send, Sync); type C = dyn DynContext; type Data = dyn Any + Send + Sync; -/// Context for all other wgpu objects. Instan ce of wgpu. +/// Context for all other wgpu objects. Instance of wgpu. /// /// This is the first thing you create when using wgpu. /// Its primary use is to create [`Adapter`]s and [`Surface`]s. @@ -1201,6 +1209,15 @@ pub use wgt::ImageCopyTexture as ImageCopyTextureBase; pub type ImageCopyTexture<'a> = ImageCopyTextureBase<&'a Texture>; static_assertions::assert_impl_all!(ImageCopyTexture: Send, Sync); +pub use wgt::ImageCopyTextureTagged as ImageCopyTextureTaggedBase; +/// View of a texture which can be used to copy to a texture, including +/// color space and alpha premultiplication information. +/// +/// Corresponds to [WebGPU `GPUImageCopyTextureTagged`]( +/// https://gpuweb.github.io/gpuweb/#dictdef-gpuimagecopytexturetagged). +pub type ImageCopyTextureTagged<'a> = ImageCopyTextureTaggedBase<&'a Texture>; +static_assertions::assert_impl_all!(ImageCopyTexture: Send, Sync); + /// Describes a [`BindGroupLayout`]. /// /// For use with [`Device::create_bind_group_layout`]. @@ -3949,18 +3966,21 @@ impl Queue { } /// Schedule a copy of data from `image` into `texture`. - #[cfg(all(target_arch = "wasm32", not(feature = "webgl")))] + #[cfg(all(target_arch = "wasm32", not(feature = "emscripten")))] pub fn copy_external_image_to_texture( &self, - image: &web_sys::ImageBitmap, - texture: ImageCopyTexture, + source: &wgt::ImageCopyExternalImage, + dest: ImageCopyTextureTagged, size: Extent3d, ) { - self.context - .as_any() - .downcast_ref::() - .unwrap() - .queue_copy_external_image_to_texture(&self.id.into(), image, texture, size) + DynContext::queue_copy_external_image_to_texture( + &*self.context, + &self.id, + self.data.as_ref(), + source, + dest, + size, + ) } /// Submits a series of finished command buffers for execution. diff --git a/wgpu/tests/3x3_colors.png b/wgpu/tests/3x3_colors.png new file mode 100644 index 0000000000000000000000000000000000000000..082158392aaaa091f59dde700636304c4840af49 GIT binary patch literal 87 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1|-8Yw(bW~a-J@ZAsp9P4>B?`FmN;|RNY(s kOwz$!a9x(SGv_g8w|D%HO{}kS164A3y85}Sb4q9e0Pd<3>;M1& literal 0 HcmV?d00001 diff --git a/wgpu/tests/external_texture.rs b/wgpu/tests/external_texture.rs new file mode 100644 index 000000000..49640a517 --- /dev/null +++ b/wgpu/tests/external_texture.rs @@ -0,0 +1,355 @@ +#![cfg(all(target_arch = "wasm32", not(features = "emscripten")))] + +use std::num::NonZeroU32; + +use crate::common::{fail_if, initialize_test, TestParameters}; +use wasm_bindgen::JsCast; +use wasm_bindgen_test::*; +use wgpu::ExternalImageSource; + +#[wasm_bindgen_test] +async fn image_bitmap_import() { + let image_encoded = include_bytes!("3x3_colors.png"); + + // Create an array-of-arrays for Blob's constructor + let array = js_sys::Array::new(); + array.push(&js_sys::Uint8Array::from(&image_encoded[..])); + + // We're passing an array of Uint8Arrays + let blob = web_sys::Blob::new_with_u8_array_sequence(&array).unwrap(); + + // Parse the image from the blob + + // Because we need to call the function in a way that isn't bound by + // web_sys, we need to manually construct the options struct and call + // the function. + let image_bitmap_function: js_sys::Function = web_sys::window() + .unwrap() + .get("createImageBitmap") + .unwrap() + .dyn_into() + .unwrap(); + + let options_arg = js_sys::Object::new(); + js_sys::Reflect::set( + &options_arg, + &wasm_bindgen::JsValue::from_str("premultiplyAlpha"), + &wasm_bindgen::JsValue::from_str("none"), + ) + .unwrap(); + let image_bitmap_promise: js_sys::Promise = image_bitmap_function + .call2(&wasm_bindgen::JsValue::UNDEFINED, &blob, &options_arg) + .unwrap() + .dyn_into() + .unwrap(); + + // Wait for the parsing to be done + let image_bitmap: web_sys::ImageBitmap = + wasm_bindgen_futures::JsFuture::from(image_bitmap_promise) + .await + .unwrap() + .dyn_into() + .unwrap(); + + // Sanity checks + assert_eq!(image_bitmap.width(), 3); + assert_eq!(image_bitmap.height(), 3); + + // Due to restrictions with premultiplication with ImageBitmaps, we also create an HtmlCanvasElement + // by drawing the image bitmap onto the canvas. + let canvas: web_sys::HtmlCanvasElement = web_sys::window() + .unwrap() + .document() + .unwrap() + .create_element("canvas") + .unwrap() + .dyn_into() + .unwrap(); + canvas.set_width(3); + canvas.set_height(3); + + let d2_context: web_sys::CanvasRenderingContext2d = canvas + .get_context("2d") + .unwrap() + .unwrap() + .dyn_into() + .unwrap(); + d2_context + .draw_image_with_image_bitmap(&image_bitmap, 0.0, 0.0) + .unwrap(); + + // Decode it cpu side + let raw_image = image::load_from_memory_with_format(image_encoded, image::ImageFormat::Png) + .unwrap() + .into_rgba8(); + + // Set of test cases to test with image import + #[derive(Debug, Copy, Clone)] + enum TestCase { + // Import the image as normal + Normal, + // Sets the FlipY flag. Deals with global state on GLES, so run before other tests to ensure it's reset. + // + // Only works on canvases. + FlipY, + // Sets the premultiplied alpha flag. Deals with global state on GLES, so run before other tests to ensure it's reset. + // + // Only works on canvases. + Premultiplied, + // Sets the color space to P3. + // + // Only works on canvases. + ColorSpace, + // Sets the premultiplied alpha flag. Deals with global state on GLES, so run before other tests to ensure it's reset. + // Set both the input offset and output offset to 1 in x, so the first column is omitted. + TrimLeft, + // Set the size to 2 in x, so the last column is omitted + TrimRight, + // Set only the output offset to 1, so the second column gets the first column's data. + SlideRight, + // Try to copy from out of bounds of the source image + SourceOutOfBounds, + // Try to copy from out of bounds of the destination image + DestOutOfBounds, + // Try to copy more than one slice from the source + MultiSliceCopy, + // Copy into the second slice of a 2D array texture, + SecondSliceCopy, + } + let sources = [ + ExternalImageSource::ImageBitmap(image_bitmap), + ExternalImageSource::HTMLCanvasElement(canvas), + ]; + let cases = [ + TestCase::Normal, + TestCase::FlipY, + TestCase::Premultiplied, + TestCase::ColorSpace, + TestCase::TrimLeft, + TestCase::TrimRight, + TestCase::SlideRight, + TestCase::SourceOutOfBounds, + TestCase::DestOutOfBounds, + TestCase::MultiSliceCopy, + TestCase::SecondSliceCopy, + ]; + + initialize_test(TestParameters::default(), |ctx| { + for source in sources { + for case in cases { + // Copy the data, so we can modify it for tests + let mut raw_image = raw_image.clone(); + // The origin used for the external copy on the source side. + let mut src_origin = wgpu::Origin2d::ZERO; + // If the source should be flipped in Y + let mut src_flip_y = false; + // The origin used for the external copy on the destination side. + let mut dest_origin = wgpu::Origin3d::ZERO; + // The layer the external image's data should end up in. + let mut dest_data_layer = 0; + // Color space the destination is in. + let mut dest_color_space = wgt::PredefinedColorSpace::Srgb; + // If the destination image is premultiplied. + let mut dest_premultiplied = false; + // Size of the external copy + let mut copy_size = wgpu::Extent3d { + width: 3, + height: 3, + depth_or_array_layers: 1, + }; + // Width of the destination texture + let mut dest_width = 3; + // Layer count of the destination texture + let mut dest_layers = 1; + + // If the test is suppoed to be valid call to copyExternal. + let mut valid = true; + // If the result is incorrect + let mut correct = true; + match case { + TestCase::Normal => {} + TestCase::FlipY => { + valid = !matches!(source, wgt::ExternalImageSource::ImageBitmap(_)); + src_flip_y = true; + for x in 0..3 { + let top = raw_image[(x, 0)]; + let bottom = raw_image[(x, 2)]; + raw_image[(x, 0)] = bottom; + raw_image[(x, 2)] = top; + } + } + TestCase::Premultiplied => { + valid = !matches!(source, wgt::ExternalImageSource::ImageBitmap(_)); + dest_premultiplied = true; + for pixel in raw_image.pixels_mut() { + let mut float_pix = pixel.0.map(|v| v as f32 / 255.0); + float_pix[0] *= float_pix[3]; + float_pix[1] *= float_pix[3]; + float_pix[2] *= float_pix[3]; + pixel.0 = float_pix.map(|v| (v * 255.0).round() as u8); + } + } + TestCase::ColorSpace => { + valid = ctx + .adapter_downlevel_capabilities + .flags + .contains(wgt::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES); + dest_color_space = wgt::PredefinedColorSpace::DisplayP3; + + // As we don't test, we don't bother converting the color spaces + // in the image as that's relatively annoying. + } + TestCase::TrimLeft => { + valid = ctx + .adapter_downlevel_capabilities + .flags + .contains(wgt::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES); + src_origin.x = 1; + dest_origin.x = 1; + copy_size.width = 2; + for y in 0..3 { + raw_image[(0, y)].0 = [0; 4]; + } + } + TestCase::TrimRight => { + copy_size.width = 2; + for y in 0..3 { + raw_image[(2, y)].0 = [0; 4]; + } + } + TestCase::SlideRight => { + dest_origin.x = 1; + copy_size.width = 2; + for x in (1..3).rev() { + for y in 0..3 { + raw_image[(x, y)].0 = raw_image[(x - 1, y)].0; + } + } + for y in 0..3 { + raw_image[(0, y)].0 = [0; 4]; + } + } + TestCase::SourceOutOfBounds => { + valid = false; + // It's now in bounds for the destination + dest_width = 4; + copy_size.width = 4; + } + TestCase::DestOutOfBounds => { + valid = false; + // It's now out bounds for the destination + dest_width = 2; + } + TestCase::MultiSliceCopy => { + valid = false; + copy_size.depth_or_array_layers = 2; + dest_layers = 2; + } + TestCase::SecondSliceCopy => { + correct = false; // TODO: what? + dest_origin.z = 1; + dest_data_layer = 1; + dest_layers = 2; + } + } + + let texture = ctx.device.create_texture(&wgpu::TextureDescriptor { + label: Some("import dest"), + size: wgpu::Extent3d { + width: dest_width, + height: 3, + depth_or_array_layers: dest_layers, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + fail_if(&ctx.device, !valid, || { + ctx.queue.copy_external_image_to_texture( + &wgpu::ImageCopyExternalImage { + source: source.clone(), + origin: src_origin, + flip_y: src_flip_y, + }, + wgpu::ImageCopyTextureTagged { + texture: &texture, + mip_level: 0, + origin: dest_origin, + aspect: wgpu::TextureAspect::All, + color_space: dest_color_space, + premultiplied_alpha: dest_premultiplied, + }, + copy_size, + ); + }); + + let readback_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("readback buffer"), + size: 4 * 64 * 3, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let mut encoder = ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + encoder.copy_texture_to_buffer( + wgpu::ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: 0, + y: 0, + z: dest_data_layer, + }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::ImageCopyBuffer { + buffer: &readback_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(NonZeroU32::new(256).unwrap()), + rows_per_image: None, + }, + }, + wgpu::Extent3d { + width: dest_width, + height: 3, + depth_or_array_layers: 1, + }, + ); + + ctx.queue.submit(Some(encoder.finish())); + readback_buffer + .slice(..) + .map_async(wgpu::MapMode::Read, |_| ()); + ctx.device.poll(wgpu::Maintain::Wait); + + let buffer = readback_buffer.slice(..).get_mapped_range(); + + // 64 because of 256 byte alignment / 4. + let gpu_image = image::RgbaImage::from_vec(64, 3, buffer.to_vec()).unwrap(); + let gpu_image_cropped = + image::imageops::crop_imm(&gpu_image, 0, 0, 3, 3).to_image(); + + if valid && correct { + assert_eq!( + raw_image, gpu_image_cropped, + "Failed on test case {case:?} {source:?}" + ); + } else { + assert_ne!( + raw_image, gpu_image_cropped, + "Failed on test case {case:?} {source:?}" + ); + } + } + } + }) +} diff --git a/wgpu/tests/root.rs b/wgpu/tests/root.rs index 122bf790a..a8676fb95 100644 --- a/wgpu/tests/root.rs +++ b/wgpu/tests/root.rs @@ -10,6 +10,7 @@ mod clear_texture; mod device; mod encoder; mod example_wgsl; +mod external_texture; mod instance; mod poll; mod queue_transfer;