diff --git a/Cargo.lock b/Cargo.lock index 22c613592..d9cce2f7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5955,8 +5955,10 @@ dependencies = [ "all-is-cubes-render", "all-is-cubes-ui", "async_fn_traits", + "bytemuck", "clap", "exhaust", + "futures-channel", "futures-core", "futures-util", "glam 0.24.2", @@ -5977,6 +5979,7 @@ dependencies = [ "time", "tinytemplate", "tokio", + "wgpu 0.19.4", "wgpu 22.1.0", ] diff --git a/test-renderers/Cargo.toml b/test-renderers/Cargo.toml index 262604458..58d2a1bdb 100644 --- a/test-renderers/Cargo.toml +++ b/test-renderers/Cargo.toml @@ -52,11 +52,14 @@ auto-threads = [ ] gltf = [ "dep:all-is-cubes-port", + "dep:bytemuck", + "dep:futures-channel", "dep:rend3", "dep:rend3-gltf", "dep:rend3-routine", "dep:glam", # activates mint feature for interop "dep:mint", + "dep:wgpu_for_rend3", "tokio/fs", ] @@ -69,10 +72,12 @@ all-is-cubes-port = { workspace = true, optional = true, features = ["export", " all-is-cubes-render = { workspace = true, features = ["raytracer", "std"] } all-is-cubes-ui = { workspace = true } async_fn_traits = { workspace = true } +bytemuck = { workspace = true, optional = true } # Using clap for parsing test command-line arguments clap = { workspace = true } exhaust = { workspace = true } futures-core = { workspace = true } +futures-channel = { workspace = true, optional = true } futures-util = { workspace = true, features = ["std"] } # Must be the same version of glam as rend3 uses glam = { version = "0.24", optional = true, features = ["mint"] } @@ -99,6 +104,7 @@ tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "park # In addition to depending on wgpu directly, we also need to select backends to be enabled in our binary. # (As of wgpu 0.19, Vulkan is implicitly enabled on Linux.) wgpu = { workspace = true, features = ["dx12", "metal"] } +wgpu_for_rend3 = { package = "wgpu", version = "0.19", optional = true, features = ["dx12", "metal"] } [lints] workspace = true diff --git a/test-renderers/tests/gltf-render.rs b/test-renderers/tests/gltf-render.rs index 4d536e7e4..464913f35 100644 --- a/test-renderers/tests/gltf-render.rs +++ b/test-renderers/tests/gltf-render.rs @@ -9,12 +9,14 @@ use std::sync::Arc; use clap::Parser as _; use futures_core::future::BoxFuture; +use wgpu_for_rend3 as wgpu; use all_is_cubes::space::{Sky, Space}; use all_is_cubes::universe::Handle; use all_is_cubes_content::palette; use all_is_cubes_port as port; -use all_is_cubes_render::camera::{Flaws, HeadlessRenderer, RenderError, StandardCameras}; +use all_is_cubes_render::camera::StandardCameras; +use all_is_cubes_render::{Flaws, HeadlessRenderer, RenderError, Rendering}; use test_renderers::{RendererFactory, RendererId}; const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; @@ -256,17 +258,14 @@ impl HeadlessRenderer for GltfRend3Renderer { }) } - fn draw<'a>( - &'a mut self, - info_text: &'a str, - ) -> BoxFuture<'a, Result> { + fn draw<'a>(&'a mut self, info_text: &'a str) -> BoxFuture<'a, Result> { Box::pin(async { let aic_camera = &self.cameras.cameras().world; let viewport = aic_camera.viewport(); if viewport.pixel_count() == Some(0) { // Empty viewport; must not try to use the GPU at all. - return Ok(all_is_cubes_render::camera::Rendering { + return Ok(Rendering { size: viewport.framebuffer_size, data: Vec::new(), flaws: Flaws::empty(), @@ -278,7 +277,7 @@ impl HeadlessRenderer for GltfRend3Renderer { let (_objects, mut flaws) = self.load_latest_gltf().await?; if !info_text.is_empty() { - flaws |= Flaws::MISSING_TEXTURES; // TODO: should have a flaw for this, or just actually implement it + flaws |= Flaws::OTHER; // TODO: should have a flaw for this, or just actually implement it } // not bothering to reuse texture @@ -302,7 +301,7 @@ impl HeadlessRenderer for GltfRend3Renderer { self.render_to_rend3(&frame_texture); - let rendering = all_is_cubes_gpu::in_wgpu::init::get_image_from_gpu( + let rendering = old_wgpu_helpers::get_image_from_gpu( &self.renderer.device, &self.renderer.queue, &frame_texture, @@ -344,3 +343,134 @@ fn convert_camera(aic_camera: &all_is_cubes_render::camera::Camera) -> rend3::ty view: rotation_matrix * translation_matrix, } } + +/// This is a copy and paste of all-is-cubes-gpu/src/in_wgpu/init.rs +/// except that it uses `rend3`'s version of wgpu instead. Yes +mod old_wgpu_helpers { + use super::*; + use all_is_cubes_render::camera; + use std::future::Future; + use std::sync::Arc; + + /// Copy the contents of a texture into a [`Rendering`], + /// assuming that its byte layout is RGBA8. + /// + /// Panics if the pixel type or viewport size are incorrect. + #[doc(hidden)] + pub fn get_image_from_gpu( + device: &Arc, + queue: &wgpu::Queue, + texture: &wgpu::Texture, + flaws: Flaws, + ) -> impl Future + 'static { + // By making this an explicit `Future` return we avoid capturing the queue and texture + // references. + + let size = camera::ImageSize::new(texture.width(), texture.height()); + let data_future = get_texels_from_gpu::<[u8; 4]>(device, queue, texture, 1); + + async move { + Rendering { + size, + data: data_future.await, + flaws, + } + } + } + + /// Fetch the contents of a 2D texture, assuming that its byte layout is the same as that + /// of `[C; components]` and returning a vector of length + /// `dimensions.x * dimensions.y * components`. + /// + /// Panics if the provided sizes are incorrect. + #[doc(hidden)] + pub fn get_texels_from_gpu( + device: &Arc, + queue: &wgpu::Queue, + texture: &wgpu::Texture, + components: usize, + ) -> impl Future> + 'static + where + C: bytemuck::AnyBitPattern, + { + // By making this an explicit `Future` return we avoid capturing the queue and texture + // references. + + let dimensions = camera::ImageSize::new(texture.width(), texture.height()); + assert_eq!(texture.depth_or_array_layers(), 1); + + // Check that the format matches + let format = texture.format(); + let size_of_texel = components * size_of::(); + assert_eq!( + (format.block_copy_size(None), format.block_dimensions()), + (Some(size_of_texel as u32), (1, 1)), + "Texture format does not match requested size", + ); + + let dense_bytes_per_row = dimensions.width * u32::try_from(size_of_texel).unwrap(); + let padded_bytes_per_row = dense_bytes_per_row.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) + * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + + let temp_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("GPU-to-CPU image copy buffer"), + size: u64::from(padded_bytes_per_row) * u64::from(dimensions.height), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + { + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &temp_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padded_bytes_per_row), + rows_per_image: None, + }, + }, + texture.size(), + ); + queue.submit(Some(encoder.finish())); + } + + // Start the buffer mapping + let (sender, receiver) = futures_channel::oneshot::channel(); + temp_buffer + .slice(..) + .map_async(wgpu::MapMode::Read, |result| { + let _ = sender.send(result); + }); + device.poll(wgpu::Maintain::Wait); + + // Await the buffer being available and build the image. + async move { + receiver + .await + .expect("communication failed") + .expect("buffer reading failed"); + let mapped: &[u8] = &temp_buffer.slice(..).get_mapped_range(); + + let element_count = camera::area_usize(dimensions).unwrap() * components; + + // Copy the mapped buffer data into a Rust vector, removing row padding if present + // by copying it one row at a time. + let mut texel_vector: Vec = Vec::with_capacity(element_count); + for row in 0..dimensions.height { + let byte_start_of_row = padded_bytes_per_row * row; + // TODO: this cast_slice() could fail if `C`’s alignment is higher than the buffer. + texel_vector.extend(bytemuck::cast_slice::( + &mapped[byte_start_of_row as usize..][..dense_bytes_per_row as usize], + )); + } + debug_assert_eq!(texel_vector.len(), element_count); + + temp_buffer.destroy(); + + texel_vector + } + } +}