use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock, mpsc};
use winit::event::WindowEvent;
use winit::window::Window;
use accesskit::ActionRequest;
use teksilo_render::Renderer;
#[derive(Debug, thiserror::Error)]
#[error("Surface error: {0}")]
pub struct SurfaceRenderError(pub String);
#[derive(Debug, thiserror::Error)]
pub(crate) enum SurfaceConfigureError {
#[error(
"the display server connection is gone: the window surface now reports no \
supported formats for an adapter that was selected for it"
)]
DisplayLost,
#[error("wgpu refused the surface configuration: {0}")]
Rejected(String),
}
#[derive(Debug)]
pub enum FrameOutcome {
Rendered,
Skipped,
NeedsReconfigure,
DisplayLost,
Error(SurfaceRenderError),
}
pub struct PlatformWindow {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
surface_config: wgpu::SurfaceConfiguration,
adapter: wgpu::Adapter,
display_lost: bool,
renderer: Renderer,
scale_factor: f64,
a11y_adapter: Option<accesskit_winit::Adapter>,
a11y_needs_full_tree: Arc<AtomicBool>,
a11y_action_rx: mpsc::Receiver<ActionRequest>,
a11y_bridge: Arc<AccessibilityBridge>,
}
#[derive(Debug, Default)]
pub(crate) struct AccessibilityBridge {
snapshot: Mutex<Option<accesskit::TreeUpdate>>,
active: std::sync::atomic::AtomicBool,
}
impl AccessibilityBridge {
pub(crate) fn publish(&self, update: &accesskit::TreeUpdate) {
if self.is_active() {
return;
}
if let Ok(mut slot) = self.snapshot.lock() {
*slot = Some(update.clone());
}
}
pub(crate) fn on_activate(&self) -> accesskit::TreeUpdate {
self.active
.store(true, std::sync::atomic::Ordering::Relaxed);
self.snapshot
.lock()
.ok()
.and_then(|slot| slot.clone())
.unwrap_or_else(empty_initial_tree)
}
pub(crate) fn on_deactivate(&self) {
self.active
.store(false, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn is_active(&self) -> bool {
self.active.load(std::sync::atomic::Ordering::Relaxed)
}
}
#[derive(Clone)]
struct SharedGpu {
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
}
static DISPLAY_HANDLE: OnceLock<winit::event_loop::OwnedDisplayHandle> = OnceLock::new();
pub fn install_display_handle(handle: winit::event_loop::OwnedDisplayHandle) {
let _ = DISPLAY_HANDLE.set(handle);
}
fn shared_instance() -> &'static wgpu::Instance {
static INSTANCE: OnceLock<wgpu::Instance> = OnceLock::new();
INSTANCE.get_or_init(|| {
let mut descriptor = match DISPLAY_HANDLE.get() {
Some(display) => wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(
display.clone(),
)),
None => wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
};
descriptor.flags = teksilo_render::instance_flags();
wgpu::Instance::new(descriptor)
})
}
fn window_device_limits(adapter_limits: wgpu::Limits) -> wgpu::Limits {
wgpu::Limits::downlevel_defaults().using_resolution(adapter_limits)
}
async fn open_device(
adapter: &wgpu::Adapter,
) -> Result<(wgpu::Device, wgpu::Queue), wgpu::RequestDeviceError> {
let descriptor = |limits| wgpu::DeviceDescriptor {
label: Some("teksilo_device"),
required_features: wgpu::Features::empty(),
required_limits: limits,
..Default::default()
};
match adapter
.request_device(&descriptor(window_device_limits(adapter.limits())))
.await
{
Ok(pair) => Ok(pair),
Err(err) => {
eprintln!(
"teksilo-platform: downlevel device limits refused ({err}); \
retrying with the adapter's own limits"
);
adapter.request_device(&descriptor(adapter.limits())).await
}
}
}
fn preferred_backends() -> &'static [wgpu::Backends] {
#[cfg(target_os = "windows")]
{
&[
wgpu::Backends::DX12,
wgpu::Backends::VULKAN,
wgpu::Backends::GL,
]
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
{
&[
wgpu::Backends::METAL,
wgpu::Backends::VULKAN,
wgpu::Backends::GL,
]
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "ios")))]
{
&[wgpu::Backends::VULKAN, wgpu::Backends::GL]
}
}
async fn presentable_adapters(
surface: &wgpu::Surface<'static>,
backends: wgpu::Backends,
power_preference: wgpu::PowerPreference,
) -> Vec<wgpu::Adapter> {
let mut adapters: Vec<wgpu::Adapter> = shared_instance()
.enumerate_adapters(backends)
.await
.into_iter()
.filter(|adapter| !surface.get_capabilities(adapter).formats.is_empty())
.collect();
let prefer_integrated = match power_preference {
wgpu::PowerPreference::LowPower => true,
wgpu::PowerPreference::HighPerformance => false,
_ => return adapters,
};
adapters
.sort_by_key(|adapter| device_type_rank(adapter.get_info().device_type, prefer_integrated));
adapters
}
fn device_type_rank(device_type: wgpu::DeviceType, prefer_integrated: bool) -> u8 {
match device_type {
wgpu::DeviceType::DiscreteGpu if prefer_integrated => 2,
wgpu::DeviceType::IntegratedGpu if prefer_integrated => 1,
wgpu::DeviceType::DiscreteGpu => 1,
wgpu::DeviceType::IntegratedGpu => 2,
wgpu::DeviceType::Other => 3,
wgpu::DeviceType::VirtualGpu => 4,
wgpu::DeviceType::Cpu => 5,
}
}
async fn open_gpu_for(
surface: &wgpu::Surface<'static>,
) -> (wgpu::Adapter, wgpu::Device, wgpu::Queue) {
let power_preference = wgpu::PowerPreference::from_env().unwrap_or_default();
let mut adapter_error = None;
let mut device_error = None;
for &backends in preferred_backends() {
for adapter in presentable_adapters(surface, backends, power_preference).await {
match open_device(&adapter).await {
Ok((device, queue)) => return (adapter, device, queue),
Err(err) => {
eprintln!(
"teksilo-platform: adapter {:?} could not open a device ({err}); \
trying the next one",
adapter.get_info().name
);
device_error.get_or_insert(err);
}
}
}
}
for force_fallback_adapter in [false, true] {
let adapter = match shared_instance()
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
compatible_surface: Some(surface),
force_fallback_adapter,
..Default::default()
})
.await
{
Ok(adapter) => adapter,
Err(err) => {
adapter_error.get_or_insert(err);
continue;
}
};
match open_device(&adapter).await {
Ok((device, queue)) => return (adapter, device, queue),
Err(err) => {
eprintln!(
"teksilo-platform: adapter {:?} could not open a device ({err}); \
trying the next one",
adapter.get_info().name
);
device_error.get_or_insert(err);
}
}
}
panic!(
"no usable GPU adapter for this window.\n\
Tried every backend wgpu was built with, then an explicit software \
fallback; none could both present to the window and open a device.\n\
adapter search: {adapter_error:?}\n\
device open: {device_error:?}\n\
Teksilo needs Vulkan, Metal, D3D12 or OpenGL (3.3 desktop / ES 3.0). \
On Linux, installing a Vulkan driver is usually the fix: \
`mesa-vulkan-drivers` carries both the hardware drivers and the \
software `lavapipe`. `WGPU_BACKEND=gl|vulkan|dx12|metal` forces a \
specific backend."
);
}
async fn shared_gpu_for(surface: &wgpu::Surface<'static>) -> SharedGpu {
static SHARED: Mutex<Option<SharedGpu>> = Mutex::new(None);
let cached = SHARED.lock().unwrap_or_else(|e| e.into_inner()).clone();
if let Some(gpu) = cached {
if !surface.get_capabilities(&gpu.adapter).formats.is_empty() {
return gpu;
}
}
let (adapter, device, queue) = open_gpu_for(surface).await;
let gpu = SharedGpu {
adapter,
device,
queue,
};
let mut slot = SHARED.lock().unwrap_or_else(|e| e.into_inner());
if slot.is_none() {
*slot = Some(gpu.clone());
}
gpu
}
fn configure_surface(
surface: &wgpu::Surface<'static>,
adapter: &wgpu::Adapter,
device: &wgpu::Device,
config: &wgpu::SurfaceConfiguration,
) -> Result<(), SurfaceConfigureError> {
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
surface.configure(device, config);
match pollster::block_on(scope.pop()) {
None => Ok(()),
Some(err) => Err(classify_configure_failure(
err.to_string(),
!surface.get_capabilities(adapter).formats.is_empty(),
)),
}
}
fn classify_configure_failure(
description: String,
surface_has_formats: bool,
) -> SurfaceConfigureError {
if surface_has_formats {
SurfaceConfigureError::Rejected(description)
} else {
SurfaceConfigureError::DisplayLost
}
}
struct WindowGpu {
surface: wgpu::Surface<'static>,
surface_config: wgpu::SurfaceConfiguration,
renderer: Renderer,
adapter: wgpu::Adapter,
display_lost: bool,
}
impl PlatformWindow {
async fn surface_and_renderer(window: &Arc<Window>) -> WindowGpu {
let size = window.inner_size();
let surface = shared_instance()
.create_surface(window.clone())
.expect("wgpu surface creation failed for the platform window");
let gpu = shared_gpu_for(&surface).await;
let surface_caps = surface.get_capabilities(&gpu.adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.or_else(|| surface_caps.formats.first().copied())
.unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: surface_caps
.alpha_modes
.first()
.copied()
.unwrap_or(wgpu::CompositeAlphaMode::Auto),
view_formats: vec![],
desired_maximum_frame_latency: 2,
color_space: wgpu::SurfaceColorSpace::Auto,
};
let display_lost =
match configure_surface(&surface, &gpu.adapter, &gpu.device, &surface_config) {
Ok(()) => false,
Err(err) => {
eprintln!("teksilo-platform: {err}");
matches!(err, SurfaceConfigureError::DisplayLost)
}
};
let renderer = Renderer::new(gpu.device, gpu.queue, surface_format);
WindowGpu {
surface,
surface_config,
renderer,
adapter: gpu.adapter,
display_lost,
}
}
pub async fn new_with_a11y(
window: Window,
event_loop: &winit::event_loop::ActiveEventLoop,
) -> Self {
let window = Arc::new(window);
let scale_factor = window.scale_factor();
let WindowGpu {
surface,
surface_config,
renderer,
adapter,
display_lost,
} = Self::surface_and_renderer(&window).await;
let (action_tx, action_rx) = mpsc::channel();
let a11y_needs_full_tree = Arc::new(AtomicBool::new(true));
let a11y_bridge = Arc::new(AccessibilityBridge::default());
let a11y_adapter = accesskit_winit::Adapter::with_direct_handlers(
event_loop,
&window,
TeksiloActivationHandler {
needs_full_tree: a11y_needs_full_tree.clone(),
bridge: Arc::clone(&a11y_bridge),
window: Arc::clone(&window),
},
TeksiloActionHandler {
tx: action_tx,
window: Arc::clone(&window),
},
TeksiloDeactivationHandler {
bridge: Arc::clone(&a11y_bridge),
window: Arc::clone(&window),
},
);
window.set_visible(true);
Self {
window,
surface,
surface_config,
adapter,
display_lost,
renderer,
scale_factor,
a11y_adapter: Some(a11y_adapter),
a11y_action_rx: action_rx,
a11y_needs_full_tree,
a11y_bridge,
}
}
pub async fn new(window: Window) -> Self {
let window = Arc::new(window);
let scale_factor = window.scale_factor();
let WindowGpu {
surface,
surface_config,
renderer,
adapter,
display_lost,
} = Self::surface_and_renderer(&window).await;
let (_action_tx, action_rx) = mpsc::channel();
Self {
window,
surface,
surface_config,
adapter,
display_lost,
renderer,
scale_factor,
a11y_adapter: None,
a11y_action_rx: action_rx,
a11y_needs_full_tree: Arc::new(AtomicBool::new(false)),
a11y_bridge: Arc::new(AccessibilityBridge::default()),
}
}
pub fn window(&self) -> &Window {
&self.window
}
pub fn window_arc(&self) -> Arc<Window> {
self.window.clone()
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
pub fn scale_factor(&self) -> f64 {
self.scale_factor
}
pub fn set_scale_factor(&mut self, factor: f64) {
self.scale_factor = factor;
}
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if self.display_lost || new_size.width == 0 || new_size.height == 0 {
return;
}
self.surface_config.width = new_size.width;
self.surface_config.height = new_size.height;
self.apply_surface_config();
}
pub fn surface_size(&self) -> (u32, u32) {
(self.surface_config.width, self.surface_config.height)
}
pub fn reconfigure_surface(&mut self) -> bool {
if self.display_lost {
return false;
}
self.apply_surface_config();
!self.display_lost
}
pub fn display_lost(&self) -> bool {
self.display_lost
}
fn apply_surface_config(&mut self) {
if let Err(err) = configure_surface(
&self.surface,
&self.adapter,
self.renderer.device(),
&self.surface_config,
) {
eprintln!("teksilo-platform: {err}");
if matches!(err, SurfaceConfigureError::DisplayLost) {
self.display_lost = true;
}
}
}
pub fn render_frame(
&mut self,
frame: &teksilo_canvas::RenderFrame,
clear_color: [f32; 4],
) -> FrameOutcome {
if self.display_lost {
return FrameOutcome::DisplayLost;
}
let current = self.surface.get_current_texture();
let output = match current {
wgpu::CurrentSurfaceTexture::Success(tex)
| wgpu::CurrentSurfaceTexture::Suboptimal(tex) => tex,
wgpu::CurrentSurfaceTexture::Occluded | wgpu::CurrentSurfaceTexture::Timeout => {
return FrameOutcome::Skipped;
}
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
return FrameOutcome::NeedsReconfigure;
}
other => return FrameOutcome::Error(SurfaceRenderError(format!("{other:?}"))),
};
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let (w, h) = self.surface_size();
self.renderer
.render(frame, &view, self.scale_factor as f32, w, h, clear_color);
self.renderer.queue().present(output);
FrameOutcome::Rendered
}
pub fn capture_offscreen(
&mut self,
frame: &teksilo_canvas::RenderFrame,
clear_color: [f32; 4],
crop: Option<teksilo_canvas::Rect>,
) -> (Vec<u8>, u32, u32) {
fn crop_rgba(
src: &[u8],
w: u32,
h: u32,
rect: teksilo_canvas::Rect,
) -> (Vec<u8>, u32, u32) {
let x0 = (rect.x.floor().max(0.0) as u32).min(w);
let y0 = (rect.y.floor().max(0.0) as u32).min(h);
let x1 = ((rect.x + rect.width).ceil().max(0.0) as u32).min(w);
let y1 = ((rect.y + rect.height).ceil().max(0.0) as u32).min(h);
if x1 <= x0 || y1 <= y0 {
return (Vec::new(), 0, 0);
}
let cw = x1 - x0;
let ch = y1 - y0;
let mut out = Vec::with_capacity((cw * ch * 4) as usize);
for y in y0..y1 {
let row_start = ((y * w + x0) * 4) as usize;
let row_end = row_start + (cw * 4) as usize;
out.extend_from_slice(&src[row_start..row_end]);
}
(out, cw, ch)
}
let (w, h) = self.surface_size();
let format = self.surface_config.format;
debug_assert!(
matches!(
format,
wgpu::TextureFormat::Rgba8Unorm
| wgpu::TextureFormat::Rgba8UnormSrgb
| wgpu::TextureFormat::Bgra8Unorm
| wgpu::TextureFormat::Bgra8UnormSrgb
),
"capture_offscreen: unsupported surface format {format:?} (expected 8-bit RGBA/BGRA)"
);
let texture = self
.renderer
.device()
.create_texture(&wgpu::TextureDescriptor {
label: Some("teksilo-automation capture"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.renderer
.render(frame, &view, self.scale_factor as f32, w, h, clear_color);
let mut bytes = teksilo_render::test_support::read_texture_rgba(
self.renderer.device(),
self.renderer.queue(),
&texture,
w,
h,
);
if matches!(
format,
wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
) {
for px in bytes.as_chunks_mut::<4>().0 {
px.swap(0, 2);
}
}
match crop {
Some(rect) => crop_rgba(&bytes, w, h, rect),
None => (bytes, w, h),
}
}
pub fn request_redraw(&self) {
self.window.request_redraw();
}
pub fn update_accessibility(&mut self, update: accesskit::TreeUpdate) {
self.a11y_bridge.publish(&update);
if let Some(adapter) = &mut self.a11y_adapter {
adapter.update_if_active(|| update);
}
}
pub fn update_accessibility_with(
&mut self,
build: impl FnOnce() -> Option<accesskit::TreeUpdate>,
previous: impl FnOnce() -> accesskit::TreeUpdate,
) {
if let Some(adapter) = &mut self.a11y_adapter {
adapter.update_if_active(|| build().unwrap_or_else(previous));
}
}
pub fn accessibility_needs_full_tree(&self) -> bool {
self.a11y_needs_full_tree.load(Ordering::Relaxed)
}
pub fn take_accessibility_needs_full_tree(&self) -> bool {
self.a11y_needs_full_tree.swap(false, Ordering::Relaxed)
}
pub fn accessibility_active(&self) -> bool {
self.a11y_bridge.is_active()
}
pub fn process_accessibility_event(&mut self, event: &WindowEvent) {
if let Some(adapter) = &mut self.a11y_adapter {
adapter.process_event(&self.window, event);
}
}
pub fn drain_accessibility_actions(&self) -> Vec<ActionRequest> {
let mut actions = Vec::new();
while let Ok(req) = self.a11y_action_rx.try_recv() {
actions.push(req);
}
actions
}
}
struct TeksiloActivationHandler {
needs_full_tree: Arc<AtomicBool>,
bridge: Arc<AccessibilityBridge>,
window: Arc<Window>,
}
fn empty_initial_tree() -> accesskit::TreeUpdate {
let root = accesskit::Node::new(accesskit::Role::Window);
let root_id = teksilo_core::accessibility::root_node_id();
accesskit::TreeUpdate {
nodes: vec![(root_id, root)],
tree: Some(accesskit::TreeInfo::new(root_id)),
tree_id: accesskit::TreeId::ROOT,
focus: root_id,
}
}
impl accesskit::ActivationHandler for TeksiloActivationHandler {
fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
self.needs_full_tree.store(true, Ordering::Relaxed);
let update = self.bridge.on_activate();
self.window.request_redraw();
Some(update)
}
}
struct TeksiloActionHandler {
tx: mpsc::Sender<ActionRequest>,
window: Arc<Window>,
}
impl accesskit::ActionHandler for TeksiloActionHandler {
fn do_action(&mut self, request: ActionRequest) {
let _ = self.tx.send(request);
self.window.request_redraw();
}
}
struct TeksiloDeactivationHandler {
bridge: Arc<AccessibilityBridge>,
window: Arc<Window>,
}
impl accesskit::DeactivationHandler for TeksiloDeactivationHandler {
fn deactivate_accessibility(&mut self) {
self.bridge.on_deactivate();
self.window.request_redraw();
}
}
#[cfg(test)]
mod surface_configure_tests {
use super::{SurfaceConfigureError, classify_configure_failure};
#[test]
fn a_surface_with_no_formats_left_means_the_display_server_is_gone() {
let err = classify_configure_failure(
"Surface does not support the adapter's queue family".to_string(),
false,
);
assert!(matches!(err, SurfaceConfigureError::DisplayLost));
assert!(err.to_string().contains("display server"));
}
#[test]
fn a_surface_that_still_has_formats_keeps_wgpus_own_message() {
let err = classify_configure_failure(
"Requested format Rgba8Unorm is not in the list of supported formats".to_string(),
true,
);
assert!(matches!(err, SurfaceConfigureError::Rejected(_)));
assert!(err.to_string().contains("Rgba8Unorm"));
assert!(!err.to_string().contains("display server"));
}
}
#[cfg(test)]
mod accessibility_bridge_tests {
use super::{AccessibilityBridge, empty_initial_tree};
fn published_tree() -> accesskit::TreeUpdate {
let root_id = teksilo_core::accessibility::root_node_id();
let child_id = accesskit::NodeId(4242);
let mut root = accesskit::Node::new(accesskit::Role::Window);
root.push_child(child_id);
let mut child = accesskit::Node::new(accesskit::Role::Button);
child.set_label("Save");
accesskit::TreeUpdate {
nodes: vec![(root_id, root), (child_id, child)],
tree: Some(accesskit::TreeInfo::new(root_id)),
tree_id: accesskit::TreeId::ROOT,
focus: root_id,
}
}
#[test]
fn a_fresh_bridge_reports_no_client() {
assert!(!AccessibilityBridge::default().is_active());
}
#[test]
fn activation_before_the_first_frame_answers_with_the_placeholder() {
let bridge = AccessibilityBridge::default();
let update = bridge.on_activate();
assert_eq!(update.nodes.len(), empty_initial_tree().nodes.len());
assert_eq!(update.nodes[0].1.children().len(), 0);
assert!(bridge.is_active());
}
#[test]
fn activation_after_a_frame_answers_with_the_real_tree() {
let bridge = AccessibilityBridge::default();
bridge.publish(&published_tree());
let update = bridge.on_activate();
assert_eq!(
update.nodes.len(),
2,
"the published tree, not a placeholder"
);
assert_eq!(update.nodes[0].1.children().len(), 1);
}
#[test]
fn the_snapshot_is_the_latest_published_tree() {
let bridge = AccessibilityBridge::default();
bridge.publish(&empty_initial_tree());
bridge.publish(&published_tree());
assert_eq!(bridge.on_activate().nodes.len(), 2);
}
#[test]
fn publishing_while_a_client_is_attached_is_skipped() {
let bridge = AccessibilityBridge::default();
bridge.publish(&published_tree());
let _ = bridge.on_activate();
bridge.publish(&empty_initial_tree());
bridge.on_deactivate();
assert_eq!(
bridge.on_activate().nodes.len(),
2,
"the tree published while attached must not have replaced the snapshot"
);
}
#[test]
fn deactivation_clears_the_attached_flag() {
let bridge = AccessibilityBridge::default();
let _ = bridge.on_activate();
assert!(bridge.is_active());
bridge.on_deactivate();
assert!(!bridge.is_active());
bridge.publish(&published_tree());
assert_eq!(bridge.on_activate().nodes.len(), 2);
assert!(bridge.is_active());
}
}
#[cfg(test)]
mod device_limits_tests {
use super::*;
fn pi4_class_limits() -> wgpu::Limits {
wgpu::Limits {
max_texture_dimension_1d: 4096,
max_texture_dimension_2d: 4096,
max_texture_dimension_3d: 256,
max_color_attachments: 4,
..wgpu::Limits::downlevel_defaults()
}
}
#[test]
fn the_default_limits_are_refused_by_gles_class_hardware() {
assert!(
!wgpu::Limits::default().check_limits(&pi4_class_limits()),
"the wgpu default limits are supposed to over-ask for a Pi-4 class \
adapter; that refusal is the crash this module exists to prevent"
);
}
#[test]
fn the_window_ask_is_satisfiable_on_gles_class_hardware() {
let adapter = pi4_class_limits();
assert!(
window_device_limits(adapter.clone()).check_limits(&adapter),
"a Pi-4 class adapter must be able to grant what a window asks for"
);
}
#[test]
fn the_window_never_asks_past_the_downlevel_floor() {
let generous = wgpu::Limits::default();
let asked = window_device_limits(generous.clone());
let floor = wgpu::Limits::downlevel_defaults();
assert_eq!(asked.max_color_attachments, floor.max_color_attachments);
assert_eq!(
asked.max_uniform_buffer_binding_size,
floor.max_uniform_buffer_binding_size
);
assert_eq!(
asked.max_inter_stage_shader_variables,
floor.max_inter_stage_shader_variables
);
assert_eq!(
asked.max_storage_buffers_per_shader_stage,
floor.max_storage_buffers_per_shader_stage
);
assert_ne!(
asked, generous,
"asking for the full default set is exactly the regression"
);
}
#[test]
fn texture_dimensions_follow_the_adapter() {
const PATH_ATLAS_MAX: u32 = 4096;
for adapter in [pi4_class_limits(), wgpu::Limits::default()] {
let asked = window_device_limits(adapter.clone());
assert_eq!(
asked.max_texture_dimension_1d,
adapter.max_texture_dimension_1d
);
assert_eq!(
asked.max_texture_dimension_2d,
adapter.max_texture_dimension_2d
);
assert_eq!(
asked.max_texture_dimension_3d,
adapter.max_texture_dimension_3d
);
assert!(
asked.max_texture_dimension_2d >= PATH_ATLAS_MAX,
"the path atlas grows to {PATH_ATLAS_MAX}; a device that cannot \
hold it would fail on a path-heavy frame instead of at startup"
);
}
}
#[test]
fn the_floor_still_covers_what_the_renderer_binds() {
const ANIM_UNIFORM_BYTES: u64 = 128 * 64;
let asked = window_device_limits(pi4_class_limits());
assert!(asked.max_color_attachments >= 1);
assert!(asked.max_uniform_buffer_binding_size >= ANIM_UNIFORM_BYTES);
}
}