Files
3sixty2cubemap/src/gpu/mod.rs
T

469 lines
17 KiB
Rust

//! 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<GpuContext> {
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<wgpu::Adapter> = 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<u8>` (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<Vec<u8>> {
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<Vec<Vec<u8>>> {
assert_eq!(srcs.len(), sizes.len(), "readback_many: len mismatch");
let n = srcs.len();
if n == 0 {
return Ok(Vec::new());
}
let stagers: Vec<wgpu::Buffer> = (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<Vec<u8>> = (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<u8>`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<Vec<Vec<u8>>> {
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<wgpu::Buffer> = (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<Vec<u8>> = (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<u8>`.
pub fn readback(&self, src: &wgpu::Buffer, size: u64) -> anyhow::Result<Vec<u8>> {
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");
}