mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 17:19:10 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7bb2259d6 |
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user