use crate::context::GpuContext;
use crate::error::{GpuError, GpuResult};
use crate::pipeline_cache::PipelineCacheKey;
use std::sync::Arc;
use tracing::{debug, trace};
pub struct StorageTextureBinding {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub format: wgpu::TextureFormat,
pub width: u32,
pub height: u32,
}
pub struct StorageTextureKernel {
pipeline: Arc<wgpu::ComputePipeline>,
bind_group_layout: wgpu::BindGroupLayout,
}
fn wgsl_storage_format_str(format: wgpu::TextureFormat) -> Option<&'static str> {
match format {
wgpu::TextureFormat::Rgba32Float => Some("rgba32float"),
wgpu::TextureFormat::Rgba8Unorm => Some("rgba8unorm"),
wgpu::TextureFormat::R32Float => Some("r32float"),
_ => None,
}
}
pub fn is_supported_storage_format(format: wgpu::TextureFormat) -> bool {
wgsl_storage_format_str(format).is_some()
}
fn bytes_per_texel(format: wgpu::TextureFormat) -> Option<u64> {
match format {
wgpu::TextureFormat::Rgba32Float => Some(16), wgpu::TextureFormat::Rgba8Unorm => Some(4), wgpu::TextureFormat::R32Float => Some(4), _ => None,
}
}
pub fn make_storage_texture_shader_source(format: wgpu::TextureFormat) -> String {
let fmt_str = wgsl_storage_format_str(format).unwrap_or("rgba32float");
format!(
r#"
@group(0) @binding(0) var<storage, read> input: array<f32>;
@group(0) @binding(1) var output: texture_storage_2d<{fmt}, write>;
@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
let dims = textureDimensions(output);
if gid.x >= dims.x || gid.y >= dims.y {{ return; }}
let idx = gid.y * dims.x + gid.x;
let val = input[idx];
textureStore(output, vec2<i32>(i32(gid.x), i32(gid.y)), vec4<f32>(val, val, val, 1.0));
}}
"#,
fmt = fmt_str
)
}
pub fn new_storage_texture(
ctx: &GpuContext,
width: u32,
height: u32,
format: wgpu::TextureFormat,
) -> GpuResult<StorageTextureBinding> {
if !is_supported_storage_format(format) {
return Err(GpuError::UnsupportedFormat(format!("{format:?}")));
}
let texture = ctx.device().create_texture(&wgpu::TextureDescriptor {
label: Some("oxigdal_storage_texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
debug!(
"Created storage texture {}×{} format={format:?}",
width, height
);
Ok(StorageTextureBinding {
texture,
view,
format,
width,
height,
})
}
fn make_bind_group_layout(
device: &wgpu::Device,
format: wgpu::TextureFormat,
) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("storage_texture_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format,
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
],
})
}
pub fn build_storage_texture_kernel(
ctx: &GpuContext,
wgsl_source: &str,
entry_point: &str,
output_format: wgpu::TextureFormat,
) -> GpuResult<StorageTextureKernel> {
if !is_supported_storage_format(output_format) {
return Err(GpuError::UnsupportedFormat(format!("{output_format:?}")));
}
let bind_group_layout = make_bind_group_layout(ctx.device(), output_format);
let pipeline_layout = ctx
.device()
.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("storage_texture_pipeline_layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let layout_tag = format!("r-tex:{output_format:?}");
let cache_key = PipelineCacheKey::new(wgsl_source, entry_point, &layout_tag);
let pipeline = {
let cache_result = ctx
.pipeline_cache()
.lock()
.map_err(|_| GpuError::internal("pipeline cache mutex poisoned"));
match cache_result {
Ok(mut guard) => {
let source_snapshot = wgsl_source.to_owned();
let device = ctx.device();
let entry_owned = entry_point.to_owned();
let layout_ref = &pipeline_layout;
guard.get_or_insert_with(cache_key.clone(), || {
compile_compute_pipeline(device, &source_snapshot, &entry_owned, layout_ref)
.map_err(|e| GpuError::pipeline_creation(e))
})?
}
Err(_) => {
tracing::warn!(
"Pipeline cache mutex poisoned; compiling storage_texture pipeline without cache"
);
Arc::new(
compile_compute_pipeline(
ctx.device(),
wgsl_source,
entry_point,
&pipeline_layout,
)
.map_err(GpuError::pipeline_creation)?,
)
}
}
};
trace!("StorageTextureKernel built, cache_key={cache_key}");
Ok(StorageTextureKernel {
pipeline,
bind_group_layout,
})
}
fn compile_compute_pipeline(
device: &wgpu::Device,
wgsl_source: &str,
entry_point: &str,
pipeline_layout: &wgpu::PipelineLayout,
) -> Result<wgpu::ComputePipeline, String> {
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("storage_texture_shader"),
source: wgpu::ShaderSource::Wgsl(wgsl_source.into()),
});
Ok(
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("storage_texture_pipeline"),
layout: Some(pipeline_layout),
module: &shader_module,
entry_point: Some(entry_point),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
}),
)
}
impl StorageTextureKernel {
pub fn dispatch_to_texture<T: bytemuck::Pod>(
&self,
ctx: &GpuContext,
input_buffer: &crate::buffer::GpuBuffer<T>,
texture: &StorageTextureBinding,
) -> GpuResult<()> {
ctx.check_device_lost()?;
let bind_group = ctx.device().create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("storage_texture_bind_group"),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: input_buffer.buffer().as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&texture.view),
},
],
});
let wg_x = dispatch_count(texture.width, 16);
let wg_y = dispatch_count(texture.height, 16);
let mut encoder = ctx
.device()
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("storage_texture_dispatch_encoder"),
});
{
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("storage_texture_compute_pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.pipeline);
compute_pass.set_bind_group(0, &bind_group, &[]);
compute_pass.dispatch_workgroups(wg_x, wg_y, 1);
}
ctx.queue().submit(std::iter::once(encoder.finish()));
trace!(
"Dispatched storage_texture kernel {}×{} → {}×{} workgroups",
texture.width, texture.height, wg_x, wg_y
);
Ok(())
}
}
pub fn read_texture_to_vec_f32(
ctx: &GpuContext,
texture: &StorageTextureBinding,
) -> GpuResult<Vec<f32>> {
ctx.check_device_lost()?;
let bpp = bytes_per_texel(texture.format)
.ok_or_else(|| GpuError::UnsupportedFormat(format!("{:?}", texture.format)))?;
let bytes_per_row_unaligned = (texture.width as u64) * bpp;
let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as u64;
let bytes_per_row_aligned = (bytes_per_row_unaligned + alignment - 1) / alignment * alignment;
let staging_size = bytes_per_row_aligned * (texture.height as u64);
let staging_buffer = ctx.device().create_buffer(&wgpu::BufferDescriptor {
label: Some("storage_texture_staging"),
size: staging_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = ctx
.device()
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("storage_texture_readback_encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &texture.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &staging_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row_aligned as u32),
rows_per_image: Some(texture.height),
},
},
wgpu::Extent3d {
width: texture.width,
height: texture.height,
depth_or_array_layers: 1,
},
);
ctx.queue().submit(std::iter::once(encoder.finish()));
let result = read_staging_buffer_blocking(ctx, &staging_buffer, staging_size)?;
let floats = decode_texture_bytes(
&result,
texture.format,
texture.width,
texture.height,
bytes_per_row_aligned,
);
Ok(floats)
}
fn read_staging_buffer_blocking(
ctx: &GpuContext,
staging_buffer: &wgpu::Buffer,
staging_size: u64,
) -> GpuResult<Vec<u8>> {
use std::sync::{Arc as StdArc, Mutex as StdMutex};
let result: StdArc<StdMutex<Option<Result<(), wgpu::BufferAsyncError>>>> =
StdArc::new(StdMutex::new(None));
let result_clone = StdArc::clone(&result);
staging_buffer
.slice(..)
.map_async(wgpu::MapMode::Read, move |r| {
let mut guard = result_clone.lock().unwrap_or_else(|e| e.into_inner());
*guard = Some(r);
});
let _poll_handle = ctx.spawn_poll_task();
loop {
{
let guard = result.lock().unwrap_or_else(|e| e.into_inner());
if guard.is_some() {
break;
}
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
let map_result = result
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
.ok_or_else(|| GpuError::buffer_mapping("mapping never completed"))?;
map_result.map_err(|e| GpuError::buffer_mapping(format!("map_async failed: {e}")))?;
let view = staging_buffer.slice(..).get_mapped_range();
let bytes = view[..staging_size as usize].to_vec();
drop(view);
staging_buffer.unmap();
Ok(bytes)
}
fn decode_texture_bytes(
raw: &[u8],
format: wgpu::TextureFormat,
width: u32,
height: u32,
bytes_per_row_aligned: u64,
) -> Vec<f32> {
let bpp = bytes_per_texel(format).unwrap_or(4);
let bytes_per_row_unaligned = (width as u64) * bpp;
match format {
wgpu::TextureFormat::Rgba32Float => {
let mut out = Vec::with_capacity((width * height * 4) as usize);
for row in 0..height as usize {
let row_start = row * bytes_per_row_aligned as usize;
let row_end = row_start + bytes_per_row_unaligned as usize;
let row_bytes = &raw[row_start..row_end];
let row_floats: &[f32] = bytemuck::cast_slice(row_bytes);
out.extend_from_slice(row_floats);
}
out
}
wgpu::TextureFormat::Rgba8Unorm => {
let mut out = Vec::with_capacity((width * height * 4) as usize);
for row in 0..height as usize {
let row_start = row * bytes_per_row_aligned as usize;
let row_end = row_start + bytes_per_row_unaligned as usize;
for &byte in &raw[row_start..row_end] {
out.push(byte as f32 / 255.0);
}
}
out
}
wgpu::TextureFormat::R32Float => {
let mut out = Vec::with_capacity((width * height) as usize);
for row in 0..height as usize {
let row_start = row * bytes_per_row_aligned as usize;
let row_end = row_start + bytes_per_row_unaligned as usize;
let row_floats: &[f32] = bytemuck::cast_slice(&raw[row_start..row_end]);
out.extend_from_slice(row_floats);
}
out
}
_ => vec![],
}
}
#[inline]
fn dispatch_count(n: u32, d: u32) -> u32 {
n.div_ceil(d)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wgsl_format_str_rgba32float() {
assert_eq!(
wgsl_storage_format_str(wgpu::TextureFormat::Rgba32Float),
Some("rgba32float")
);
}
#[test]
fn test_wgsl_format_str_rgba8unorm() {
assert_eq!(
wgsl_storage_format_str(wgpu::TextureFormat::Rgba8Unorm),
Some("rgba8unorm")
);
}
#[test]
fn test_wgsl_format_str_r32float() {
assert_eq!(
wgsl_storage_format_str(wgpu::TextureFormat::R32Float),
Some("r32float")
);
}
#[test]
fn test_wgsl_format_str_unsupported_returns_none() {
assert_eq!(
wgsl_storage_format_str(wgpu::TextureFormat::Depth32Float),
None
);
}
#[test]
fn test_is_supported_storage_format_accepted() {
assert!(is_supported_storage_format(
wgpu::TextureFormat::Rgba32Float
));
assert!(is_supported_storage_format(wgpu::TextureFormat::Rgba8Unorm));
assert!(is_supported_storage_format(wgpu::TextureFormat::R32Float));
}
#[test]
fn test_is_supported_storage_format_rejected() {
assert!(!is_supported_storage_format(
wgpu::TextureFormat::Depth32Float
));
assert!(!is_supported_storage_format(
wgpu::TextureFormat::Rgba16Float
));
}
#[test]
fn test_dispatch_count_exact_multiple() {
assert_eq!(dispatch_count(64, 16), 4);
}
#[test]
fn test_dispatch_count_rounds_up() {
assert_eq!(dispatch_count(65, 16), 5);
assert_eq!(dispatch_count(1, 16), 1);
assert_eq!(dispatch_count(16, 16), 1);
assert_eq!(dispatch_count(17, 16), 2);
}
#[test]
fn test_bytes_per_texel_rgba32float() {
assert_eq!(bytes_per_texel(wgpu::TextureFormat::Rgba32Float), Some(16));
}
#[test]
fn test_bytes_per_texel_rgba8unorm() {
assert_eq!(bytes_per_texel(wgpu::TextureFormat::Rgba8Unorm), Some(4));
}
#[test]
fn test_bytes_per_texel_r32float() {
assert_eq!(bytes_per_texel(wgpu::TextureFormat::R32Float), Some(4));
}
#[test]
fn test_bytes_per_texel_unsupported_returns_none() {
assert_eq!(bytes_per_texel(wgpu::TextureFormat::Depth32Float), None);
}
}