1use crate::context::GpuContext;
30use crate::error::{GpuError, GpuResult};
31use crate::pipeline_cache::PipelineCacheKey;
32use std::sync::Arc;
33use tracing::{debug, trace};
34
35pub struct StorageTextureBinding {
45 pub texture: wgpu::Texture,
47 pub view: wgpu::TextureView,
49 pub format: wgpu::TextureFormat,
51 pub width: u32,
53 pub height: u32,
55}
56
57pub struct StorageTextureKernel {
62 pipeline: Arc<wgpu::ComputePipeline>,
63 bind_group_layout: wgpu::BindGroupLayout,
64}
65
66fn wgsl_storage_format_str(format: wgpu::TextureFormat) -> Option<&'static str> {
74 match format {
75 wgpu::TextureFormat::Rgba32Float => Some("rgba32float"),
76 wgpu::TextureFormat::Rgba8Unorm => Some("rgba8unorm"),
77 wgpu::TextureFormat::R32Float => Some("r32float"),
78 _ => None,
79 }
80}
81
82pub fn is_supported_storage_format(format: wgpu::TextureFormat) -> bool {
84 wgsl_storage_format_str(format).is_some()
85}
86
87fn bytes_per_texel(format: wgpu::TextureFormat) -> Option<u64> {
91 match format {
92 wgpu::TextureFormat::Rgba32Float => Some(16), wgpu::TextureFormat::Rgba8Unorm => Some(4), wgpu::TextureFormat::R32Float => Some(4), _ => None,
96 }
97}
98
99pub fn make_storage_texture_shader_source(format: wgpu::TextureFormat) -> String {
118 let fmt_str = wgsl_storage_format_str(format).unwrap_or("rgba32float"); format!(
123 r#"
124@group(0) @binding(0) var<storage, read> input: array<f32>;
125@group(0) @binding(1) var output: texture_storage_2d<{fmt}, write>;
126
127@compute @workgroup_size(16, 16)
128fn main(@builtin(global_invocation_id) gid: vec3<u32>) {{
129 let dims = textureDimensions(output);
130 if gid.x >= dims.x || gid.y >= dims.y {{ return; }}
131 let idx = gid.y * dims.x + gid.x;
132 let val = input[idx];
133 textureStore(output, vec2<i32>(i32(gid.x), i32(gid.y)), vec4<f32>(val, val, val, 1.0));
134}}
135"#,
136 fmt = fmt_str
137 )
138}
139
140pub fn new_storage_texture(
170 ctx: &GpuContext,
171 width: u32,
172 height: u32,
173 format: wgpu::TextureFormat,
174) -> GpuResult<StorageTextureBinding> {
175 if !is_supported_storage_format(format) {
176 return Err(GpuError::UnsupportedFormat(format!("{format:?}")));
177 }
178
179 let texture = ctx.device().create_texture(&wgpu::TextureDescriptor {
180 label: Some("oxigdal_storage_texture"),
181 size: wgpu::Extent3d {
182 width,
183 height,
184 depth_or_array_layers: 1,
185 },
186 mip_level_count: 1,
187 sample_count: 1,
188 dimension: wgpu::TextureDimension::D2,
189 format,
190 usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
191 view_formats: &[],
192 });
193
194 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
195
196 debug!(
197 "Created storage texture {}×{} format={format:?}",
198 width, height
199 );
200
201 Ok(StorageTextureBinding {
202 texture,
203 view,
204 format,
205 width,
206 height,
207 })
208}
209
210fn make_bind_group_layout(
222 device: &wgpu::Device,
223 format: wgpu::TextureFormat,
224) -> wgpu::BindGroupLayout {
225 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
226 label: Some("storage_texture_bgl"),
227 entries: &[
228 wgpu::BindGroupLayoutEntry {
230 binding: 0,
231 visibility: wgpu::ShaderStages::COMPUTE,
232 ty: wgpu::BindingType::Buffer {
233 ty: wgpu::BufferBindingType::Storage { read_only: true },
234 has_dynamic_offset: false,
235 min_binding_size: None,
236 },
237 count: None,
238 },
239 wgpu::BindGroupLayoutEntry {
241 binding: 1,
242 visibility: wgpu::ShaderStages::COMPUTE,
243 ty: wgpu::BindingType::StorageTexture {
244 access: wgpu::StorageTextureAccess::WriteOnly,
245 format,
246 view_dimension: wgpu::TextureViewDimension::D2,
247 },
248 count: None,
249 },
250 ],
251 })
252}
253
254pub fn build_storage_texture_kernel(
279 ctx: &GpuContext,
280 wgsl_source: &str,
281 entry_point: &str,
282 output_format: wgpu::TextureFormat,
283) -> GpuResult<StorageTextureKernel> {
284 if !is_supported_storage_format(output_format) {
285 return Err(GpuError::UnsupportedFormat(format!("{output_format:?}")));
286 }
287
288 let bind_group_layout = make_bind_group_layout(ctx.device(), output_format);
289
290 let pipeline_layout = ctx
291 .device()
292 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
293 label: Some("storage_texture_pipeline_layout"),
294 bind_group_layouts: &[Some(&bind_group_layout)],
295 immediate_size: 0,
296 });
297
298 let layout_tag = format!("r-tex:{output_format:?}");
300 let cache_key = PipelineCacheKey::new(wgsl_source, entry_point, &layout_tag);
301
302 let pipeline = {
304 let cache_result = ctx
305 .pipeline_cache()
306 .lock()
307 .map_err(|_| GpuError::internal("pipeline cache mutex poisoned"));
308
309 match cache_result {
310 Ok(mut guard) => {
311 let source_snapshot = wgsl_source.to_owned();
312 let device = ctx.device();
313 let entry_owned = entry_point.to_owned();
314 let layout_ref = &pipeline_layout;
315
316 guard.get_or_insert_with(cache_key.clone(), || {
317 compile_compute_pipeline(device, &source_snapshot, &entry_owned, layout_ref)
318 .map_err(|e| GpuError::pipeline_creation(e))
319 })?
320 }
321 Err(_) => {
322 tracing::warn!(
324 "Pipeline cache mutex poisoned; compiling storage_texture pipeline without cache"
325 );
326 Arc::new(
327 compile_compute_pipeline(
328 ctx.device(),
329 wgsl_source,
330 entry_point,
331 &pipeline_layout,
332 )
333 .map_err(GpuError::pipeline_creation)?,
334 )
335 }
336 }
337 };
338
339 trace!("StorageTextureKernel built, cache_key={cache_key}");
340
341 Ok(StorageTextureKernel {
342 pipeline,
343 bind_group_layout,
344 })
345}
346
347fn compile_compute_pipeline(
352 device: &wgpu::Device,
353 wgsl_source: &str,
354 entry_point: &str,
355 pipeline_layout: &wgpu::PipelineLayout,
356) -> Result<wgpu::ComputePipeline, String> {
357 let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
358 label: Some("storage_texture_shader"),
359 source: wgpu::ShaderSource::Wgsl(wgsl_source.into()),
360 });
361
362 Ok(
363 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
364 label: Some("storage_texture_pipeline"),
365 layout: Some(pipeline_layout),
366 module: &shader_module,
367 entry_point: Some(entry_point),
368 compilation_options: wgpu::PipelineCompilationOptions::default(),
369 cache: None,
370 }),
371 )
372}
373
374impl StorageTextureKernel {
379 pub fn dispatch_to_texture<T: bytemuck::Pod>(
396 &self,
397 ctx: &GpuContext,
398 input_buffer: &crate::buffer::GpuBuffer<T>,
399 texture: &StorageTextureBinding,
400 ) -> GpuResult<()> {
401 ctx.check_device_lost()?;
402
403 let bind_group = ctx.device().create_bind_group(&wgpu::BindGroupDescriptor {
404 label: Some("storage_texture_bind_group"),
405 layout: &self.bind_group_layout,
406 entries: &[
407 wgpu::BindGroupEntry {
408 binding: 0,
409 resource: input_buffer.buffer().as_entire_binding(),
410 },
411 wgpu::BindGroupEntry {
412 binding: 1,
413 resource: wgpu::BindingResource::TextureView(&texture.view),
414 },
415 ],
416 });
417
418 let wg_x = dispatch_count(texture.width, 16);
419 let wg_y = dispatch_count(texture.height, 16);
420
421 let mut encoder = ctx
422 .device()
423 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
424 label: Some("storage_texture_dispatch_encoder"),
425 });
426
427 {
428 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
429 label: Some("storage_texture_compute_pass"),
430 timestamp_writes: None,
431 });
432 compute_pass.set_pipeline(&self.pipeline);
433 compute_pass.set_bind_group(0, &bind_group, &[]);
434 compute_pass.dispatch_workgroups(wg_x, wg_y, 1);
435 }
436
437 ctx.queue().submit(std::iter::once(encoder.finish()));
438
439 trace!(
440 "Dispatched storage_texture kernel {}×{} → {}×{} workgroups",
441 texture.width, texture.height, wg_x, wg_y
442 );
443
444 Ok(())
445 }
446}
447
448pub fn read_texture_to_vec_f32(
469 ctx: &GpuContext,
470 texture: &StorageTextureBinding,
471) -> GpuResult<Vec<f32>> {
472 ctx.check_device_lost()?;
473
474 let bpp = bytes_per_texel(texture.format)
475 .ok_or_else(|| GpuError::UnsupportedFormat(format!("{:?}", texture.format)))?;
476
477 let bytes_per_row_unaligned = (texture.width as u64) * bpp;
479 let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as u64;
480 let bytes_per_row_aligned = (bytes_per_row_unaligned + alignment - 1) / alignment * alignment;
481
482 let staging_size = bytes_per_row_aligned * (texture.height as u64);
483
484 let staging_buffer = ctx.device().create_buffer(&wgpu::BufferDescriptor {
485 label: Some("storage_texture_staging"),
486 size: staging_size,
487 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
488 mapped_at_creation: false,
489 });
490
491 let mut encoder = ctx
493 .device()
494 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
495 label: Some("storage_texture_readback_encoder"),
496 });
497
498 encoder.copy_texture_to_buffer(
499 wgpu::TexelCopyTextureInfo {
500 texture: &texture.texture,
501 mip_level: 0,
502 origin: wgpu::Origin3d::ZERO,
503 aspect: wgpu::TextureAspect::All,
504 },
505 wgpu::TexelCopyBufferInfo {
506 buffer: &staging_buffer,
507 layout: wgpu::TexelCopyBufferLayout {
508 offset: 0,
509 bytes_per_row: Some(bytes_per_row_aligned as u32),
510 rows_per_image: Some(texture.height),
511 },
512 },
513 wgpu::Extent3d {
514 width: texture.width,
515 height: texture.height,
516 depth_or_array_layers: 1,
517 },
518 );
519
520 ctx.queue().submit(std::iter::once(encoder.finish()));
521
522 let result = read_staging_buffer_blocking(ctx, &staging_buffer, staging_size)?;
524
525 let floats = decode_texture_bytes(
527 &result,
528 texture.format,
529 texture.width,
530 texture.height,
531 bytes_per_row_aligned,
532 );
533
534 Ok(floats)
535}
536
537fn read_staging_buffer_blocking(
539 ctx: &GpuContext,
540 staging_buffer: &wgpu::Buffer,
541 staging_size: u64,
542) -> GpuResult<Vec<u8>> {
543 use std::sync::{Arc as StdArc, Mutex as StdMutex};
544
545 let result: StdArc<StdMutex<Option<Result<(), wgpu::BufferAsyncError>>>> =
546 StdArc::new(StdMutex::new(None));
547 let result_clone = StdArc::clone(&result);
548
549 staging_buffer
550 .slice(..)
551 .map_async(wgpu::MapMode::Read, move |r| {
552 let mut guard = result_clone.lock().unwrap_or_else(|e| e.into_inner());
553 *guard = Some(r);
554 });
555
556 let _poll_handle = ctx.spawn_poll_task();
558 loop {
559 {
560 let guard = result.lock().unwrap_or_else(|e| e.into_inner());
561 if guard.is_some() {
562 break;
563 }
564 }
565 std::thread::sleep(std::time::Duration::from_millis(1));
566 }
567
568 let map_result = result
569 .lock()
570 .unwrap_or_else(|e| e.into_inner())
571 .take()
572 .ok_or_else(|| GpuError::buffer_mapping("mapping never completed"))?;
573
574 map_result.map_err(|e| GpuError::buffer_mapping(format!("map_async failed: {e}")))?;
575
576 let view = staging_buffer.slice(..).get_mapped_range();
577 let bytes = view[..staging_size as usize].to_vec();
578 drop(view);
579 staging_buffer.unmap();
580
581 Ok(bytes)
582}
583
584fn decode_texture_bytes(
586 raw: &[u8],
587 format: wgpu::TextureFormat,
588 width: u32,
589 height: u32,
590 bytes_per_row_aligned: u64,
591) -> Vec<f32> {
592 let bpp = bytes_per_texel(format).unwrap_or(4);
593 let bytes_per_row_unaligned = (width as u64) * bpp;
594
595 match format {
596 wgpu::TextureFormat::Rgba32Float => {
597 let mut out = Vec::with_capacity((width * height * 4) as usize);
599 for row in 0..height as usize {
600 let row_start = row * bytes_per_row_aligned as usize;
601 let row_end = row_start + bytes_per_row_unaligned as usize;
602 let row_bytes = &raw[row_start..row_end];
603 let row_floats: &[f32] = bytemuck::cast_slice(row_bytes);
605 out.extend_from_slice(row_floats);
606 }
607 out
608 }
609 wgpu::TextureFormat::Rgba8Unorm => {
610 let mut out = Vec::with_capacity((width * height * 4) as usize);
612 for row in 0..height as usize {
613 let row_start = row * bytes_per_row_aligned as usize;
614 let row_end = row_start + bytes_per_row_unaligned as usize;
615 for &byte in &raw[row_start..row_end] {
616 out.push(byte as f32 / 255.0);
617 }
618 }
619 out
620 }
621 wgpu::TextureFormat::R32Float => {
622 let mut out = Vec::with_capacity((width * height) as usize);
624 for row in 0..height as usize {
625 let row_start = row * bytes_per_row_aligned as usize;
626 let row_end = row_start + bytes_per_row_unaligned as usize;
627 let row_floats: &[f32] = bytemuck::cast_slice(&raw[row_start..row_end]);
628 out.extend_from_slice(row_floats);
629 }
630 out
631 }
632 _ => vec![],
634 }
635}
636
637#[inline]
643fn dispatch_count(n: u32, d: u32) -> u32 {
644 n.div_ceil(d)
645}
646
647#[cfg(test)]
652mod tests {
653 use super::*;
654
655 #[test]
656 fn test_wgsl_format_str_rgba32float() {
657 assert_eq!(
658 wgsl_storage_format_str(wgpu::TextureFormat::Rgba32Float),
659 Some("rgba32float")
660 );
661 }
662
663 #[test]
664 fn test_wgsl_format_str_rgba8unorm() {
665 assert_eq!(
666 wgsl_storage_format_str(wgpu::TextureFormat::Rgba8Unorm),
667 Some("rgba8unorm")
668 );
669 }
670
671 #[test]
672 fn test_wgsl_format_str_r32float() {
673 assert_eq!(
674 wgsl_storage_format_str(wgpu::TextureFormat::R32Float),
675 Some("r32float")
676 );
677 }
678
679 #[test]
680 fn test_wgsl_format_str_unsupported_returns_none() {
681 assert_eq!(
682 wgsl_storage_format_str(wgpu::TextureFormat::Depth32Float),
683 None
684 );
685 }
686
687 #[test]
688 fn test_is_supported_storage_format_accepted() {
689 assert!(is_supported_storage_format(
690 wgpu::TextureFormat::Rgba32Float
691 ));
692 assert!(is_supported_storage_format(wgpu::TextureFormat::Rgba8Unorm));
693 assert!(is_supported_storage_format(wgpu::TextureFormat::R32Float));
694 }
695
696 #[test]
697 fn test_is_supported_storage_format_rejected() {
698 assert!(!is_supported_storage_format(
699 wgpu::TextureFormat::Depth32Float
700 ));
701 assert!(!is_supported_storage_format(
702 wgpu::TextureFormat::Rgba16Float
703 ));
704 }
705
706 #[test]
707 fn test_dispatch_count_exact_multiple() {
708 assert_eq!(dispatch_count(64, 16), 4);
709 }
710
711 #[test]
712 fn test_dispatch_count_rounds_up() {
713 assert_eq!(dispatch_count(65, 16), 5);
714 assert_eq!(dispatch_count(1, 16), 1);
715 assert_eq!(dispatch_count(16, 16), 1);
716 assert_eq!(dispatch_count(17, 16), 2);
717 }
718
719 #[test]
720 fn test_bytes_per_texel_rgba32float() {
721 assert_eq!(bytes_per_texel(wgpu::TextureFormat::Rgba32Float), Some(16));
722 }
723
724 #[test]
725 fn test_bytes_per_texel_rgba8unorm() {
726 assert_eq!(bytes_per_texel(wgpu::TextureFormat::Rgba8Unorm), Some(4));
727 }
728
729 #[test]
730 fn test_bytes_per_texel_r32float() {
731 assert_eq!(bytes_per_texel(wgpu::TextureFormat::R32Float), Some(4));
732 }
733
734 #[test]
735 fn test_bytes_per_texel_unsupported_returns_none() {
736 assert_eq!(bytes_per_texel(wgpu::TextureFormat::Depth32Float), None);
737 }
738}