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, crate::DeviceHealth)>> =
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 mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle_from_env();
descriptor.flags = crate::instance_flags();
let instance = wgpu::Instance::new(descriptor);
for force_fallback_adapter in [false, true] {
let Ok(adapter) = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::from_env()
.unwrap_or(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_offscreen_renderer(
label: &'static str,
) -> Option<(Renderer, wgpu::Device, wgpu::Queue)> {
let (device, queue, health) = shared_device(label)?;
let renderer = Renderer::with_device_health(
device.clone(),
queue.clone(),
wgpu::TextureFormat::Rgba8UnormSrgb,
health.clone(),
);
Some((renderer, device.clone(), queue.clone()))
}
pub async fn create_test_renderer(
label: &'static str,
) -> Option<(Renderer, wgpu::Device, wgpu::Queue)> {
create_offscreen_renderer(label).await
}
pub const REQUIRE_ADAPTER_VAR: &str = "TEKSILO_TEST_REQUIRE_ADAPTER";
pub fn adapter_required() -> bool {
std::env::var_os(REQUIRE_ADAPTER_VAR).is_some_and(|v| !v.is_empty())
}
pub async fn require_test_renderer(
label: &'static str,
) -> Option<(Renderer, wgpu::Device, wgpu::Queue)> {
let requirement = std::env::var(REQUIRE_ADAPTER_VAR).ok();
let renderer = create_offscreen_renderer(label).await;
let found = renderer
.as_ref()
.map(|(_, device, _)| device.adapter_info());
if let Err(message) = check_adapter_requirement(requirement.as_deref(), found.as_ref()) {
panic!("{message}");
}
renderer
}
fn check_adapter_requirement(
requirement: Option<&str>,
found: Option<&wgpu::AdapterInfo>,
) -> Result<(), String> {
match (requirement, found) {
(None, _) => Ok(()),
(Some("any"), Some(_)) => Ok(()),
(Some("lavapipe"), Some(info)) if is_lavapipe(info) => Ok(()),
(Some(wanted @ ("any" | "lavapipe")), None) => Err(format!(
"{REQUIRE_ADAPTER_VAR}={wanted}, but no GPU adapter could be opened"
)),
(Some("lavapipe"), Some(info)) => Err(format!(
"{REQUIRE_ADAPTER_VAR}=lavapipe, but the adapter opened is not lavapipe: {info:?}"
)),
(Some(other), _) => Err(format!(
"{REQUIRE_ADAPTER_VAR}={other:?} is not one of \"any\" or \"lavapipe\""
)),
}
}
pub fn is_lavapipe(info: &wgpu::AdapterInfo) -> bool {
info.backend == wgpu::Backend::Vulkan
&& info.device_type == wgpu::DeviceType::Cpu
&& info.driver == "llvmpipe"
}
fn shared_device(
label: &'static str,
) -> Option<&'static (wgpu::Device, wgpu::Queue, crate::DeviceHealth)> {
SHARED_DEVICE
.get_or_init(|| {
pollster::block_on(open_shared_device(label)).map(|(device, queue)| {
let health = crate::DeviceHealth::install(&device);
(device, queue, health)
})
})
.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> {
try_read_texture_level_rgba(device, queue, texture, 0, width, height)
}
pub fn try_read_texture_level_rgba(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture: &wgpu::Texture,
level: u32,
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: level,
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"
);
}
fn adapter(
backend: wgpu::Backend,
device_type: wgpu::DeviceType,
driver: &str,
) -> wgpu::AdapterInfo {
let mut info = wgpu::AdapterInfo::new(device_type, backend);
info.driver = driver.into();
info
}
#[test]
fn lavapipe_is_told_apart_from_other_software_rasterisers() {
use wgpu::{Backend, DeviceType};
assert!(is_lavapipe(&adapter(
Backend::Vulkan,
DeviceType::Cpu,
"llvmpipe"
)));
assert!(!is_lavapipe(&adapter(Backend::Gl, DeviceType::Cpu, "")));
assert!(!is_lavapipe(&adapter(Backend::Dx12, DeviceType::Cpu, "")));
assert!(!is_lavapipe(&adapter(
Backend::Vulkan,
DeviceType::IntegratedGpu,
"radv"
)));
}
#[test]
fn the_adapter_requirement_fails_only_when_it_is_unmet() {
use wgpu::{Backend, DeviceType};
let lavapipe = adapter(Backend::Vulkan, DeviceType::Cpu, "llvmpipe");
let radv = adapter(Backend::Vulkan, DeviceType::IntegratedGpu, "radv");
assert!(check_adapter_requirement(None, None).is_ok());
assert!(check_adapter_requirement(None, Some(&radv)).is_ok());
assert!(check_adapter_requirement(Some("any"), Some(&radv)).is_ok());
assert!(check_adapter_requirement(Some("lavapipe"), Some(&lavapipe)).is_ok());
assert!(check_adapter_requirement(Some("any"), None).is_err());
assert!(check_adapter_requirement(Some("lavapipe"), None).is_err());
let wrong = check_adapter_requirement(Some("lavapipe"), Some(&radv)).unwrap_err();
assert!(
wrong.contains("radv"),
"the message names the adapter found: {wrong}"
);
assert!(check_adapter_requirement(Some("vulkan"), Some(&lavapipe)).is_err());
}
}