use std::sync::{OnceLock, mpsc};
use crate::Renderer;
#[derive(Debug, thiserror::Error)]
pub enum ReadbackError {
#[error("readback failed: the GPU device was lost while mapping")]
DeviceLost,
#[error("readback failed: buffer mapping was refused ({0})")]
MapFailed(String),
#[error("readback failed: polling the device failed ({0})")]
PollFailed(String),
}
static SHARED_DEVICE: OnceLock<Option<(wgpu::Device, wgpu::Queue)>> = OnceLock::new();
async fn open_shared_device(label: &'static str) -> Option<(wgpu::Device, wgpu::Queue)> {
#[cfg(test)]
DEVICE_OPENS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
for force_fallback_adapter in [false, true] {
let Ok(adapter) = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface: None,
force_fallback_adapter,
..Default::default()
})
.await
else {
continue;
};
let limits = wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits());
if let Ok((device, queue)) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some(label),
required_features: wgpu::Features::empty(),
required_limits: limits,
..Default::default()
})
.await
{
return Some((device, queue));
}
}
None
}
pub async fn create_test_renderer(
label: &'static str,
) -> Option<(Renderer, wgpu::Device, wgpu::Queue)> {
let (device, queue) = shared_device(label)?;
let renderer = Renderer::new(
device.clone(),
queue.clone(),
wgpu::TextureFormat::Rgba8UnormSrgb,
);
Some((renderer, device.clone(), queue.clone()))
}
fn shared_device(label: &'static str) -> Option<&'static (wgpu::Device, wgpu::Queue)> {
SHARED_DEVICE
.get_or_init(|| pollster::block_on(open_shared_device(label)))
.as_ref()
}
pub fn read_texture_rgba(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture: &wgpu::Texture,
width: u32,
height: u32,
) -> Vec<u8> {
try_read_texture_rgba(device, queue, texture, width, height).expect("texture readback failed")
}
pub fn try_read_texture_rgba(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture: &wgpu::Texture,
width: u32,
height: u32,
) -> Result<Vec<u8>, ReadbackError> {
let bytes_per_pixel = 4u32;
let unpadded_bytes_per_row = width * bytes_per_pixel;
let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
* wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let buffer_size = padded_bytes_per_row as u64 * height as u64;
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("teksilo_render_test_readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("teksilo_render_test_copy"),
});
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),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
queue.submit(std::iter::once(encoder.finish()));
let slice = buffer.slice(..);
let (tx, rx) = mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
device
.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
})
.map_err(|e| ReadbackError::PollFailed(e.to_string()))?;
rx.recv()
.map_err(|_| ReadbackError::DeviceLost)?
.map_err(|e| ReadbackError::MapFailed(e.to_string()))?;
let mapped = slice
.get_mapped_range()
.map_err(|e| ReadbackError::MapFailed(e.to_string()))?;
let mut pixels = vec![0u8; (width * height * bytes_per_pixel) as usize];
for row in 0..height as usize {
let src_offset = row * padded_bytes_per_row as usize;
let dst_offset = row * unpadded_bytes_per_row as usize;
pixels[dst_offset..dst_offset + unpadded_bytes_per_row as usize]
.copy_from_slice(&mapped[src_offset..src_offset + unpadded_bytes_per_row as usize]);
}
drop(mapped);
buffer.unmap();
Ok(pixels)
}
#[cfg(test)]
static DEVICE_OPENS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
mod shared_device_tests {
use super::*;
#[test]
fn exactly_one_device_is_opened_per_process() {
use std::sync::atomic::Ordering;
let barrier = std::sync::Arc::new(std::sync::Barrier::new(4));
let handles: Vec<_> = (0..4)
.map(|_| {
let b = barrier.clone();
std::thread::spawn(move || {
b.wait();
pollster::block_on(create_test_renderer("shared-device-test")).is_some()
})
})
.collect();
let got: Vec<bool> = handles.into_iter().map(|h| h.join().unwrap()).collect();
assert!(
got.iter().all(|g| *g) || got.iter().all(|g| !*g),
"callers disagreed about whether a GPU exists: {got:?}"
);
let opens = DEVICE_OPENS.load(Ordering::Relaxed);
assert_eq!(
opens, 1,
"the device must be opened exactly once per process, not {opens} times - a second \
concurrent WARP device is an access violation inside d3d10warp.dll, not a slowdown"
);
}
}