use crate::WgpuExecutor; use core_types::Color; use core_types::color::SRGBA8; use core_types::list::{Item, List}; use core_types::ops::Convert; use core_types::transform::Footprint; use raster_types::Image; use raster_types::{CPU, GPU, Raster}; use wgpu::util::{DeviceExt, TextureDataOrder}; use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; /// Uploads CPU image data to a GPU texture /// /// Creates a new WGPU texture with RGBA8UnormSrgb format and uploads the provided /// image data. The texture is configured for binding, copying, and source operations. fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster) -> wgpu::Texture { let rgba8_data: Vec = image.data.iter().map(|x| (*x).into()).collect(); device.create_texture_with_data( queue, &TextureDescriptor { label: Some("upload_to_texture staging texture"), size: Extent3d { width: image.width, height: image.height, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, format: TextureFormat::Rgba8UnormSrgb, usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC, view_formats: &[], }, TextureDataOrder::LayerMajor, bytemuck::cast_slice(rgba8_data.as_slice()), ) } /// Converts a Raster texture to Raster by downloading the underlying texture data. /// /// Assumptions: /// - 2D texture, mip level 0 /// - 4 bytes-per-pixel RGBA8 /// - Texture has COPY_SRC usage struct RasterGpuToRasterCpuConverter { buffer: wgpu::Buffer, width: u32, height: u32, unpadded_bytes_per_row: u32, padded_bytes_per_row: u32, _source: raster_types::Texture, } impl RasterGpuToRasterCpuConverter { fn new(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster) -> Self { let texture = data_gpu.data(); let width = texture.width(); let height = texture.height(); let bytes_per_pixel = 4; // RGBA8 let unpadded_bytes_per_row = width * bytes_per_pixel; let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align; let buffer_size = padded_bytes_per_row as u64 * height as u64; let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("texture_download_buffer"), size: buffer_size, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &buffer, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded_bytes_per_row), rows_per_image: Some(height), }, }, Extent3d { width, height, depth_or_array_layers: 1, }, ); Self { buffer, width, height, unpadded_bytes_per_row, padded_bytes_per_row, // Keep source texture alive _source: data_gpu.texture.clone(), } } async fn convert(self, device: &wgpu::Device) -> Result, wgpu::BufferAsyncError> { let buffer_slice = self.buffer.slice(..); let (sender, receiver) = futures::channel::oneshot::channel(); buffer_slice.map_async(wgpu::MapMode::Read, move |result| { let _ = sender.send(result); }); let _ = device.poll(wgpu::wgt::PollType::wait_indefinitely()); receiver.await.expect("Failed to receive map result")?; let view = buffer_slice.get_mapped_range(); let row_stride = self.padded_bytes_per_row as usize; let row_bytes = self.unpadded_bytes_per_row as usize; let mut cpu_data: Vec = Vec::with_capacity((self.width * self.height) as usize); for row in 0..self.height as usize { let start = row * row_stride; let row_slice = &view[start..start + row_bytes]; for px in row_slice.chunks_exact(4) { // `Image` pixels are stored linear-light with associated (premultiplied) alpha let srgba = SRGBA8::new(px[0], px[1], px[2], px[3]); cpu_data.push(Color::from(srgba).apply_opacity(px[3] as f32 / 255.)); } } drop(view); self.buffer.unmap(); let cpu_image = Image { data: cpu_data, width: self.width, height: self.height, base64_string: None, }; Ok(Raster::new_cpu(cpu_image)) } } /// Passthrough conversion for GPU `List`s - no conversion needed impl<'i> Convert>, &'i WgpuExecutor> for List> { async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { self } } /// Converts a `List>` to `List>` by uploading each image to a texture impl<'i> Convert>, &'i WgpuExecutor> for List> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { let device = &executor.context().device; let queue = executor.context().queue.lock(); let list = self .into_iter() .map(|row| { let (image, attributes) = row.into_parts(); let texture = upload_to_texture(device, &queue, &image); Item::from_parts(Raster::new_gpu(texture), attributes) }) .collect(); queue.submit([]); list } } /// Converts single CPU raster to GPU by uploading to texture impl<'i> Convert, &'i WgpuExecutor> for Raster { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { let device = &executor.context().device; let queue = executor.context().queue.lock(); let texture = upload_to_texture(device, &queue, &self); queue.submit([]); Raster::new_gpu(texture) } } /// Passthrough conversion for CPU `List`s - no conversion needed impl<'i> Convert>, &'i WgpuExecutor> for List> { async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { self } } /// Converts a `List>` to `List>` by downloading texture data in one go then asynchronously maps all buffers and processes the results. impl<'i> Convert>, &'i WgpuExecutor> for List> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { let device = &executor.context().device; let queue = &executor.context().queue; let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("batch_texture_download_encoder"), }); let mut converters = Vec::new(); let mut rows_meta = Vec::new(); for row in self { let (element, attributes) = row.into_parts(); converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, element)); rows_meta.push(Item::from_parts((), attributes)); } queue.submit([encoder.finish()]); let mut map_futures = Vec::new(); for converter in converters { map_futures.push(converter.convert(device)); } let map_results = futures::future::try_join_all(map_futures) .await .map_err(|_| "Failed to receive map result") .expect("Buffer mapping communication failed"); map_results .into_iter() .zip(rows_meta) .map(|(element, row)| { let (_, attributes) = row.into_parts(); Item::from_parts(element, attributes) }) .collect() } } /// Converts single GPU raster to CPU by downloading texture data impl<'i> Convert, &'i WgpuExecutor> for Raster { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { let device = &executor.context().device; let queue = &executor.context().queue; let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("single_texture_download_encoder"), }); let converter = RasterGpuToRasterCpuConverter::new(device, &mut encoder, self); queue.submit([encoder.finish()]); converter.convert(device).await.expect("Failed to download texture data") } }