Compare commits

...
Author SHA1 Message Date
Horizon agentandClaude Opus 5.5 a7bb2259d6 Renderer: fall back when an adapter can't present to the window (#13493)
On a hybrid laptop under Wayland, request_adapter(HighPerformance) returns
the NVIDIA GPU, whose Surface::configure then fails with "Invalid surface";
wgpu's default handler panicked outside the device search.

Surface setup is now part of open_backend: an adapter whose surface offers
no format is skipped before its device opens, and configure runs in error
scopes (Renderer::configure). On either failure the backend's other
adapters are enumerated and the best one that presents is tried
(pick_adapter: HighPerformance order, never one already tried, software
only on the last try); with none, the search goes on to the next backend.
One warning line names the skipped adapter and why. The normal path opens
one device and configures once, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 04:10:36 +01:00
+261 -67
View File
@@ -1458,6 +1458,9 @@ struct Opened {
device: wgpu::Device,
queue: wgpu::Queue,
device_lost: Arc<std::sync::atomic::AtomicBool>,
/// The window surface's configuration, set on `device` during the search, and the
/// surface's capabilities on `adapter`. None offscreen.
configured: Option<(wgpu::SurfaceConfiguration, wgpu::SurfaceCapabilities)>,
}
/// Logs the adapter and records it for telemetry and crash reports.
fn describe_adapter(adapter: &wgpu::Adapter) {
@@ -1520,6 +1523,88 @@ fn surface_format(
.or_else(|| formats.iter().copied().find(|f| !f.is_srgb()))
.unwrap_or(default)
}
/// Sets the window surface up on `device`: the swapchain format (`surface_format`), the
/// copies screenshots and frame generation make, and Mailbox when the surface offers it
/// (`keep_mode`, a replaced renderer's mode, first). `caps` are the surface's capabilities
/// on `adapter`. Errs, instead of wgpu's panic, when the surface can't be configured on
/// this device: a hybrid laptop's discrete GPU under Wayland accepts the surface in
/// `request_adapter` and fails only here (#13493).
#[allow(clippy::too_many_arguments)]
fn configure_surface(
surface: &wgpu::Surface<'_>,
adapter: &wgpu::Adapter,
device: &wgpu::Device,
device_lost: &std::sync::atomic::AtomicBool,
caps: wgpu::SurfaceCapabilities,
size: winit::dpi::PhysicalSize<u32>,
keep_mode: Option<wgpu::PresentMode>,
log_step: bool,
) -> std::result::Result<(wgpu::SurfaceConfiguration, wgpu::SurfaceCapabilities), String> {
let max_side = device.limits().max_texture_dimension_2d;
let mut config = surface
.get_default_config(
adapter,
size.width.clamp(1, max_side),
size.height.clamp(1, max_side),
)
.ok_or("surface has no supported configuration")?;
config.format = surface_format(&caps.formats, config.format);
config.view_formats = vec![config.format.remove_srgb_suffix()];
// Screenshots copy the presented frame back from the swapchain, frame
// generation's present thread copies its frames in.
if caps.usages.contains(wgpu::TextureUsages::COPY_SRC) {
config.usage |= wgpu::TextureUsages::COPY_SRC;
}
if caps.usages.contains(wgpu::TextureUsages::COPY_DST) {
config.usage |= wgpu::TextureUsages::COPY_DST;
}
config.present_mode = keep_mode
.filter(|mode| caps.present_modes.contains(mode))
.unwrap_or_else(|| {
if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
wgpu::PresentMode::Mailbox
} else {
wgpu::PresentMode::Fifo
}
});
if log_step {
startup_step(format_args!(
"device ready; configuring the surface ({}x{} {:?}, {:?})",
config.width, config.height, config.format, config.present_mode
));
}
Renderer::configure(device, surface, &config, None, device_lost)?;
Ok((config, caps))
}
/// What `pick_adapter` knows of one of a backend's adapters.
#[derive(Clone, Copy, Debug)]
struct AdapterChoice {
kind: wgpu::DeviceType,
/// The window surface offers it formats (`Surface::get_capabilities`).
presents: bool,
/// Already tried, and it could not present.
tried: bool,
}
/// The adapter to try after one that could not present to the window: one not tried yet
/// that the surface offers formats on, a software one only when `allow_cpu`, ranked as
/// `request_adapter` ranks for `HighPerformance` (discrete, integrated, other, virtual,
/// CPU), the first listed among equals. A hybrid laptop's integrated GPU drives the
/// Wayland compositor and presents when its discrete GPU can't (#13493).
fn pick_adapter(adapters: &[AdapterChoice], allow_cpu: bool) -> Option<usize> {
let rank = |kind| match kind {
wgpu::DeviceType::DiscreteGpu => 0,
wgpu::DeviceType::IntegratedGpu => 1,
wgpu::DeviceType::Other => 2,
wgpu::DeviceType::VirtualGpu => 3,
wgpu::DeviceType::Cpu => 4,
};
adapters
.iter()
.enumerate()
.filter(|(_, a)| a.presents && !a.tried && (allow_cpu || !is_software(a.kind)))
.min_by_key(|(_, a)| rank(a.kind))
.map(|(i, _)| i)
}
/// Opens a device on the first of `backend_order`'s backends that gives one.
/// Offscreen rendering needs no surface, so it can use a GPU the window's
/// display cannot present from (a headless compositor).
@@ -1688,11 +1773,92 @@ async fn open_backend(
adapter.get_info().name
)));
}
describe_adapter(&adapter);
// Adapters that could not present to the window: the next one of this backend
// that can is tried before the next backend (#13493).
let mut tried = Vec::new();
let mut adapter = adapter;
loop {
let why = match open_adapter(name, window, surface.as_ref(), &adapter, layers).await {
Ok(adapted) => {
return Ok(Opened {
instance,
surface,
adapter,
device: adapted.device,
queue: adapted.queue,
device_lost: adapted.device_lost,
configured: adapted.configured,
});
}
Err(AdapterFailure::Failed(e)) => return Err(NoDevice::Failed(e)),
Err(AdapterFailure::CannotPresent(why)) => why,
};
let info = adapter.get_info();
tried.push(info.clone());
let others = instance.enumerate_adapters(wgpu::Backends::all()).await;
let choices: Vec<_> = others
.iter()
.map(|other| AdapterChoice {
kind: other.get_info().device_type,
presents: surface
.as_ref()
.is_some_and(|s| !s.get_capabilities(other).formats.is_empty()),
tried: tried.contains(&other.get_info()),
})
.collect();
let Some(next) = pick_adapter(&choices, allow_cpu) else {
log::warn!(
"Horizon renderer: skipping {} on {name}: it can't present to the window \
({why}), and no other {name} adapter can",
info.name
);
return Err(NoDevice::Failed(format!(
"{name}: {} can't present to the window ({why})",
info.name
)));
};
adapter = others[next].clone();
log::warn!(
"Horizon renderer: skipping {} on {name}: it can't present to the window ({why}); \
trying {}",
info.name,
adapter.get_info().name
);
}
}
/// Why an adapter gave no device: it can't present to the window, so another adapter of
/// the backend may, or another failure.
enum AdapterFailure {
CannotPresent(String),
Failed(String),
}
/// A device opened on one adapter, with the window surface set up on it.
struct Adapted {
device: wgpu::Device,
queue: wgpu::Queue,
device_lost: Arc<std::sync::atomic::AtomicBool>,
configured: Option<(wgpu::SurfaceConfiguration, wgpu::SurfaceCapabilities)>,
}
/// Opens a device on `adapter` and configures the window's `surface` on it (none
/// offscreen). An adapter the surface offers no format on fails before its device opens.
async fn open_adapter(
name: &str,
window: &Arc<Window>,
surface: Option<&wgpu::Surface<'static>>,
adapter: &wgpu::Adapter,
layers: u32,
) -> std::result::Result<Adapted, AdapterFailure> {
let caps = surface.map(|surface| surface.get_capabilities(adapter));
if caps.as_ref().is_some_and(|caps| caps.formats.is_empty()) {
return Err(AdapterFailure::CannotPresent(
"the window surface offers it no format".into(),
));
}
describe_adapter(adapter);
let info = adapter.get_info();
let limits = adapter.limits();
if let Err(e) = check_layers(&adapter, layers) {
return Err(NoDevice::Failed(format!("{name}: {e}")));
if let Err(e) = check_layers(adapter, layers) {
return Err(AdapterFailure::Failed(format!("{name}: {e}")));
}
// Mappable primary buffers: section pages that no longer fit in
// video memory go to system memory. Shader packs' 16-bit normalized buffers (render
@@ -1700,9 +1866,9 @@ async fn open_backend(
// stand-in neither clamps nor drops NaN. Subgroups: shader packs' compute programs
// (`MC_GL_KHR_shader_subgroup_*`); storage buffers in vertex shaders, clearing and
// atomics on images: their buffers and images.
let features = timing::GpuTimer::features(&adapter)
| cull::Cull::features(&adapter)
| oit::features(&adapter)
let features = timing::GpuTimer::features(adapter)
| cull::Cull::features(adapter)
| oit::features(adapter)
| (adapter.features()
& (wgpu::Features::INDIRECT_FIRST_INSTANCE
| wgpu::Features::MAPPABLE_PRIMARY_BUFFERS
@@ -1737,7 +1903,7 @@ async fn open_backend(
};
#[cfg(not(target_os = "macos"))]
let opened = match info.backend {
wgpu::Backend::Vulkan => horizon_hdr::open_vulkan_device(&adapter, &desc),
wgpu::Backend::Vulkan => horizon_hdr::open_vulkan_device(adapter, &desc),
_ => None,
};
#[cfg(target_os = "macos")]
@@ -1746,31 +1912,42 @@ async fn open_backend(
Some(opened) => opened,
None => adapter.request_device(&desc).await,
};
match opened {
Ok((device, queue)) => {
let device_lost = Arc::new(std::sync::atomic::AtomicBool::new(false));
let lost = device_lost.clone();
device.set_device_lost_callback(move |reason, message| {
// A device destroyed on purpose was not lost.
if reason == wgpu::DeviceLostReason::Destroyed {
return;
}
log::warn!(
"Horizon renderer: the graphics device was lost ({reason:?}: {message})"
);
lost.store(true, std::sync::atomic::Ordering::Relaxed);
});
Ok(Opened {
instance,
let (device, queue) = match opened {
Ok(opened) => opened,
Err(e) => return Err(AdapterFailure::Failed(format!("{name} device: {e}"))),
};
let device_lost = Arc::new(std::sync::atomic::AtomicBool::new(false));
let lost = device_lost.clone();
device.set_device_lost_callback(move |reason, message| {
// A device destroyed on purpose was not lost.
if reason == wgpu::DeviceLostReason::Destroyed {
return;
}
log::warn!("Horizon renderer: the graphics device was lost ({reason:?}: {message})");
lost.store(true, std::sync::atomic::Ordering::Relaxed);
});
let configured = match (surface, caps) {
(Some(surface), Some(caps)) => Some(
configure_surface(
surface,
adapter,
device,
queue,
device_lost,
})
}
Err(e) => Err(NoDevice::Failed(format!("{name} device: {e}"))),
}
&device,
&device_lost,
caps,
window.inner_size(),
None,
true,
)
.map_err(AdapterFailure::CannotPresent)?,
),
_ => None,
};
Ok(Adapted {
device,
queue,
device_lost,
configured,
})
}
/// What to tell a player whose system offers no usable graphics adapter.
/// Vulkan is loaded at run time (the game links no graphics library), so a
@@ -1870,6 +2047,7 @@ impl Renderer {
device,
queue,
device_lost,
configured,
} = match previous {
// Reload resources on the same device; replacing live Vulkan
// devices can invalidate the driver's swapchain state on the same
@@ -1896,6 +2074,7 @@ impl Renderer {
device: renderer.device.clone(),
queue: renderer.queue.clone(),
device_lost: renderer.device_lost.clone(),
configured: None,
}
}
None => {
@@ -1913,42 +2092,21 @@ impl Renderer {
crate::graphics::suggest_reduced_far_detail(crate::graphics::lod_capped(&info));
let size = window.inner_size();
let max_side = device.limits().max_texture_dimension_2d;
let (config, present_modes, hdr_output) = if let Some(surface) = &surface {
let mut config = surface
.get_default_config(
&adapter,
size.width.clamp(1, max_side),
size.height.clamp(1, max_side),
)
.ok_or("surface has no supported configuration")?;
let caps = surface.get_capabilities(&adapter);
config.format = surface_format(&caps.formats, config.format);
config.view_formats = vec![config.format.remove_srgb_suffix()];
// Screenshots copy the presented frame back from the swapchain, frame
// generation's present thread copies its frames in.
if caps.usages.contains(wgpu::TextureUsages::COPY_SRC) {
config.usage |= wgpu::TextureUsages::COPY_SRC;
}
if caps.usages.contains(wgpu::TextureUsages::COPY_DST) {
config.usage |= wgpu::TextureUsages::COPY_DST;
}
config.present_mode = previous
.map(|renderer| renderer.config.present_mode)
.filter(|mode| caps.present_modes.contains(mode))
.unwrap_or_else(|| {
if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
wgpu::PresentMode::Mailbox
} else {
wgpu::PresentMode::Fifo
}
});
if previous.is_none() {
startup_step(format_args!(
"device ready; configuring the surface ({}x{} {:?}, {:?})",
config.width, config.height, config.format, config.present_mode
));
}
surface.configure(&device, &config);
let configured = match (&surface, configured) {
(Some(_), Some(configured)) => Some(configured),
(Some(surface), None) => Some(configure_surface(
surface,
&adapter,
&device,
&device_lost,
surface.get_capabilities(&adapter),
size,
previous.map(|renderer| renderer.config.present_mode),
previous.is_none(),
)?),
(None, _) => None,
};
let (config, present_modes, hdr_output) = if let Some((config, caps)) = configured {
let hdr_output = hdr::output(&caps);
crate::graphics::offer_hdr(hdr_output.is_some());
if previous.is_none() {
@@ -1957,7 +2115,7 @@ impl Renderer {
hdr_output.map_or("not offered by this display", hdr::Output::name)
);
}
(config, caps.present_modes.clone(), hdr_output)
(config, caps.present_modes, hdr_output)
} else {
let format = wgpu::TextureFormat::Bgra8Unorm;
let config = wgpu::SurfaceConfiguration {
@@ -9416,6 +9574,42 @@ mod tests {
);
}
}
/// #13493: a hybrid laptop under Wayland. The NVIDIA GPU can't present to the window,
/// so the integrated GPU that drives the compositor is next, never the one that failed.
#[test]
fn an_adapter_that_cannot_present_gives_way_to_one_that_can() {
use super::{AdapterChoice, pick_adapter};
use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
let a = |kind, presents, tried| AdapterChoice {
kind,
presents,
tried,
};
// The NVIDIA GPU failed to configure, the Intel one presents: the Intel one.
let hybrid = [a(DiscreteGpu, true, true), a(IntegratedGpu, true, false)];
assert_eq!(pick_adapter(&hybrid, false), Some(1));
// Among adapters that present, HighPerformance's order: discrete first, then
// integrated, other, virtual; the first listed among equals.
let many = [
a(VirtualGpu, true, false),
a(Other, true, false),
a(IntegratedGpu, true, false),
a(DiscreteGpu, false, false),
a(DiscreteGpu, true, false),
a(DiscreteGpu, true, false),
];
assert_eq!(pick_adapter(&many, false), Some(4));
assert_eq!(pick_adapter(&many[..4], false), Some(2));
assert_eq!(pick_adapter(&many[..2], false), Some(1));
// Nothing left that presents: the next backend (OpenGL) is tried.
let none = [a(DiscreteGpu, true, true), a(IntegratedGpu, false, false)];
assert_eq!(pick_adapter(&none, false), None);
assert_eq!(pick_adapter(&[], false), None);
// A software adapter only on the search's last, software-allowed try.
let llvmpipe = [a(DiscreteGpu, true, true), a(Cpu, true, false)];
assert_eq!(pick_adapter(&llvmpipe, false), None);
assert_eq!(pick_adapter(&llvmpipe, true), Some(1));
}
/// The second try at a backend shows the adapters wgpu hides for reporting
/// no Vulkan conformance version (Mesa's hasvk, which drives Ivy Bridge and
/// Haswell, reports 0.0.0.0 and works).