//! WebGPU (`wgpu`) compute layer. //! //! `GpuContext` owns the wgpu instance/device/queue so every GPU stage //! (extract sharpness, cubemap projection, later mask inference) shares one //! device and one queue. `wgpu` maps to Vulkan/Metal/DX12 on native and WebGPU //! in the browser — one WGSL codebase, cross-vendor. //! //! Added for stage 3 (cubemap): texture upload, storage-texture output, //! and a `read_texture_rgba` readback that correctly handles the WebGPU //! 256-byte `bytes_per_row` alignment requirement. use wgpu::util::DeviceExt; /// A ready-to-use wgpu device + queue plus the instance/adapter used to get it. pub struct GpuContext { pub device: wgpu::Device, pub queue: wgpu::Queue, } /// WebGPU requires `bytes_per_row` to be a multiple of this for /// buffer<->texture copies. pub const COPY_BYTES_PER_ROW_ALIGNMENT: u64 = 256; /// Round `n` up to the nearest multiple of `256` (WebGPU copy alignment). pub fn align_bytes_per_row(n: u64) -> u64 { n.div_ceil(COPY_BYTES_PER_ROW_ALIGNMENT) * COPY_BYTES_PER_ROW_ALIGNMENT } impl GpuContext { /// Request a compute-capable adapter, preferring the *discrete* GPU. /// /// `RequestAdapterOptions::default()` uses `power_preference: LowPower`, which /// on a dual-GPU system (integrated + discrete) often selects the weak iGPU /// or a software (llvmpipe) adapter — making every GPU stage look like it /// "isn't using the GPU". We explicitly ask for `HighPerformance` and, as a /// fallback, enumerate all adapters and pick the first discrete one. pub async fn new(label: &str) -> anyhow::Result { let instance = wgpu::Instance::default(); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, force_fallback_adapter: false, compatible_surface: None, }) .await .or_else(|_| { // enumerate_adapter is blocking-ish; do it on a blocking context. // wgpu exposes enumerate_adapters synchronously. let mut best: Option = None; for a in instance.enumerate_adapters(wgpu::Backends::all()) { let info = a.get_info(); if info.device_type == wgpu::DeviceType::DiscreteGpu { best = Some(a); break; } if best.is_none() { best = Some(a); } } best.ok_or_else(|| { anyhow::anyhow!("no wgpu adapter found (no Vulkan/Metal/DX12 device?)") }) })?; let info = adapter.get_info(); eprintln!( "[gpu] using {} ({:?}) via {:?}", info.name, info.device_type, info.backend ); let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { label: Some(label), required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::default(), memory_hints: wgpu::MemoryHints::default(), trace: wgpu::Trace::Off, }) .await?; Ok(GpuContext { device, queue }) } /// Build a compute pipeline from an embedded WGSL source string. pub fn compute_pipeline( &self, label: &str, source: &'static str, layout: &wgpu::PipelineLayout, ) -> wgpu::ComputePipeline { let module = self .device .create_shader_module(wgpu::ShaderModuleDescriptor { label: Some(label), source: wgpu::ShaderSource::Wgsl(source.into()), }); self.device .create_compute_pipeline(&wgpu::ComputePipelineDescriptor { label: Some(label), layout: Some(layout), module: &module, entry_point: Some("main"), compilation_options: Default::default(), cache: None, }) } /// Create a storage buffer of `size` bytes (used for readback or GPU output). pub fn create_storage_buffer( &self, label: &str, size: u64, contents: Option<&[u8]>, ) -> wgpu::Buffer { let usage = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST; match contents { Some(data) => self .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: data, usage, }), None => self.device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size, usage, mapped_at_creation: false, }), } } /// Create a uniform buffer initialised from `data`. pub fn create_uniform_buffer(&self, label: &str, data: &[u8]) -> wgpu::Buffer { let usage = wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST; self.device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(label), contents: data, usage, }) } /// Create a read-only RGBA8 input texture (sampled by the compute kernels). pub fn create_texture_2d(&self, label: &str, w: u32, h: u32) -> wgpu::Texture { self.device.create_texture(&wgpu::TextureDescriptor { label: Some(label), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }) } /// Create a non-square output storage texture (written by kernels, read back). pub fn create_storage_texture_2d_wh(&self, label: &str, w: u32, h: u32) -> wgpu::Texture { self.device.create_texture(&wgpu::TextureDescriptor { label: Some(label), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }) } /// Create an output storage texture (written by the kernels, read back). pub fn create_storage_texture_2d(&self, label: &str, n: u32) -> wgpu::Texture { self.device.create_texture(&wgpu::TextureDescriptor { label: Some(label), size: wgpu::Extent3d { width: n, height: n, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }) } /// Upload RGBA8 `data` (width*height*4 bytes) into `tex`. pub fn upload_rgba(&self, tex: &wgpu::Texture, w: u32, h: u32, data: &[u8]) { self.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, data, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w * 4), rows_per_image: Some(h), }, wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1, }, ); } /// Copy an RGBA storage texture to a tightly packed `Vec` (w*h*4). /// /// Handles the WebGPU 256-byte `bytes_per_row` padding: a padded staging /// buffer is mapped and then destrided into the tight layout expected by /// the `image` crate. pub fn read_texture_rgba( &self, tex: &wgpu::Texture, w: u32, h: u32, ) -> anyhow::Result> { let bytes_per_row = u64::from(w) * 4; let padded_row = align_bytes_per_row(bytes_per_row); let total = padded_row * u64::from(h); let staging = self.device.create_buffer(&wgpu::BufferDescriptor { label: Some("texture-readback-staging"), size: total, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &staging, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded_row as u32), rows_per_image: Some(h), }, }, wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1, }, ); self.queue.submit(Some(encoder.finish())); let slice = staging.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |res| { let _ = tx.send(res); }); let _ = self.device.poll(wgpu::PollType::Wait); rx.recv()??; let mapped = slice.get_mapped_range(); let mut tight = vec![0u8; (bytes_per_row * u64::from(h)) as usize]; if padded_row == bytes_per_row { tight.copy_from_slice(&mapped); } else { let row_w = bytes_per_row as usize; let row_p = padded_row as usize; for r in 0..h as usize { let src_start = r * row_p; tight[r * row_w..(r + 1) * row_w] .copy_from_slice(&mapped[src_start..src_start + row_w]); } } Ok(tight) } /// Map several already-submitted buffers in one `poll(Wait)` and return their /// contents in order. Amortises the per-readback poll latency across a batch. #[allow(clippy::needless_range_loop)] pub fn readback_many( &self, srcs: &[&wgpu::Buffer], sizes: &[u64], ) -> anyhow::Result>> { assert_eq!(srcs.len(), sizes.len(), "readback_many: len mismatch"); let n = srcs.len(); if n == 0 { return Ok(Vec::new()); } let stagers: Vec = (0..n) .map(|i| { self.device.create_buffer(&wgpu::BufferDescriptor { label: Some("rb-many-staging"), size: sizes[i], usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }) }) .collect(); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); for i in 0..n { encoder.copy_buffer_to_buffer(srcs[i], 0, &stagers[i], 0, sizes[i]); } self.queue.submit(Some(encoder.finish())); let (tx, rx) = std::sync::mpsc::channel(); for i in 0..n { let tx = tx.clone(); stagers[i] .slice(..) .map_async(wgpu::MapMode::Read, move |res| { let _ = tx.send((i, res)); }); } let _ = self.device.poll(wgpu::PollType::Wait); let mut results: Vec> = (0..n).map(|_| Vec::new()).collect(); for _ in 0..n { let (i, res) = rx .recv() .map_err(|_| anyhow::anyhow!("readback_many: channel closed"))?; res?; results[i] = stagers[i].slice(..).get_mapped_range().to_vec(); } Ok(results) } /// Copy several storage textures to tightly packed RGBA `Vec`s with one /// submit + one poll. Handles the 256-byte `bytes_per_row` padding per tex. #[allow(clippy::needless_range_loop)] pub fn read_textures_rgba( &self, texs: &[&wgpu::Texture], w: u32, h: u32, ) -> anyhow::Result>> { let n = texs.len(); if n == 0 { return Ok(Vec::new()); } let bytes_per_row = u64::from(w) * 4; let padded_row = align_bytes_per_row(bytes_per_row); let total = padded_row * u64::from(h); let stagers: Vec = (0..n) .map(|_| { self.device.create_buffer(&wgpu::BufferDescriptor { label: Some("tex-rb-many-staging"), size: total, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }) }) .collect(); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); for i in 0..n { encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: texs[i], mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &stagers[i], layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded_row as u32), rows_per_image: Some(h), }, }, wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1, }, ); } self.queue.submit(Some(encoder.finish())); let (tx, rx) = std::sync::mpsc::channel(); for i in 0..n { let tx = tx.clone(); stagers[i] .slice(..) .map_async(wgpu::MapMode::Read, move |res| { let _ = tx.send((i, res)); }); } let _ = self.device.poll(wgpu::PollType::Wait); let mut results: Vec> = (0..n).map(|_| Vec::new()).collect(); for _ in 0..n { let (i, res) = rx .recv() .map_err(|_| anyhow::anyhow!("read_textures_rgba: channel closed"))?; res?; let mapped = stagers[i].slice(..).get_mapped_range(); let mut tight = vec![0u8; (bytes_per_row * u64::from(h)) as usize]; if padded_row == bytes_per_row { tight.copy_from_slice(&mapped); } else { let row_w = bytes_per_row as usize; let row_p = padded_row as usize; for r in 0..h as usize { let src_start = r * row_p; tight[r * row_w..(r + 1) * row_w] .copy_from_slice(&mapped[src_start..src_start + row_w]); } } results[i] = tight; } Ok(results) } /// Snapshot the contents of a (storage or uniform) buffer into a `Vec`. pub fn readback(&self, src: &wgpu::Buffer, size: u64) -> anyhow::Result> { let snapshot = self.device.create_buffer(&wgpu::BufferDescriptor { label: Some("readback"), size, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); encoder.copy_buffer_to_buffer(src, 0, &snapshot, 0, size); self.queue.submit(Some(encoder.finish())); let slice = snapshot.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |res| { let _ = tx.send(res); }); let _ = self.device.poll(wgpu::PollType::Wait); rx.recv()??; let data = slice.get_mapped_range().to_vec(); Ok(data) } } // Shader sources (embedded at compile time). pub mod shaders { /// Equirect -> 6 cube faces (images: bilinear; masks: nearest). pub const EQUIRECT_TO_CUBEMAP: &str = include_str!("shaders/equirect_to_cubemap.wgsl"); /// Downscale + grayscale + Laplacian variance (sharpness scoring). pub const SHARPNESS: &str = include_str!("shaders/sharpness.wgsl"); /// Cubemap -> equirect (nearest, for mask reprojection). pub const C2E: &str = include_str!("shaders/c2e.wgsl"); }