Moving a window between monitors of different scale can briefly ask for a surface larger than the device's largest texture (11512 x 6808 against 8192 in the report), and Surface::configure failed validation, which wgpu turns into a panic. The surface is now clamped to the device limit with its aspect ratio kept, and egui draws the whole window at the matching smaller scale, so the frame is only softer until the size settles. The device also asks for the adapter's own texture limit (16384 on most desktop GPUs) instead of 8192, and the resize path and an unsupported surface log instead of unwrapping. Fixes #869 Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
897b7d93b5
commit
d23593d3eb
6 files changed
+115
-12
No files matched your search
@@ -48,6 +48,8 @@ pub mod keymap;
|
||||
#[cfg(test)]
|
||||
mod menu_tests;
|
||||
#[cfg(test)]
|
||||
mod surface_clamp_tests;
|
||||
#[cfg(test)]
|
||||
mod status_tests {
|
||||
#[test]
|
||||
fn the_status_bar_names_commands_by_label() {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//! The window surface is clamped to the device's largest texture (#869): a window moved between
|
||||
//! monitors of different scale asked for 11512 x 6808 against a maximum of 8192 and the
|
||||
//! surface configure was a validation panic. The clamp lives in the vendored egui-wgpu.
|
||||
|
||||
use egui_wgpu::clamp_surface_extent;
|
||||
|
||||
#[test]
|
||||
fn a_window_that_fits_is_unchanged() {
|
||||
assert_eq!(clamp_surface_extent(3840, 2160, 8192), (3840, 2160, 1.0));
|
||||
assert_eq!(clamp_surface_extent(8192, 8192, 8192), (8192, 8192, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reported_window_is_clamped_to_the_device_limit() {
|
||||
// #869: 11512 x 6808 requested against a maximum of 8192.
|
||||
let (w, h, s) = clamp_surface_extent(11512, 6808, 8192);
|
||||
assert_eq!(w, 8192);
|
||||
assert!(h <= 8192 && h > 0);
|
||||
assert!((s - 8192.0 / 11512.0).abs() < 1e-6);
|
||||
// Aspect ratio kept within a pixel.
|
||||
let expected_h = 6808.0 * 8192.0 / 11512.0;
|
||||
assert!((f64::from(h) - expected_h).abs() <= 1.0, "{h} vs {expected_h}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tall_window_is_clamped_on_its_height() {
|
||||
let (w, h, s) = clamp_surface_extent(2000, 20000, 8192);
|
||||
assert_eq!(h, 8192);
|
||||
assert_eq!(w, 819);
|
||||
assert!(s < 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extreme_sizes_never_give_zero_or_exceed_the_limit() {
|
||||
for (w, h, m) in [(u32::MAX, 1, 8192), (1, u32::MAX, 8192), (u32::MAX, u32::MAX, 16384), (0, 0, 8192), (5000, 3000, 0), (100_000, 3, 2048)] {
|
||||
let (cw, ch, s) = clamp_surface_extent(w, h, m);
|
||||
assert!(cw >= 1 && ch >= 1, "{w}x{h} max {m} gave {cw}x{ch}");
|
||||
assert!(cw <= m.max(1) && ch <= m.max(1), "{w}x{h} max {m} gave {cw}x{ch}");
|
||||
assert!(s > 0.0 && s <= 1.0);
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -116,3 +116,9 @@ SolidCraft changes:
|
||||
- `src/setup.rs`: the default device requests `TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES` when
|
||||
the adapter has it, so 2x and 8x work where the adapter supports them (WebGPU only
|
||||
guarantees 1x and 4x).
|
||||
- `src/setup.rs`: the default device asks for the adapter's largest 2D texture (at least 8192)
|
||||
instead of exactly 8192.
|
||||
- `src/lib.rs`, `src/winit.rs`: a window larger than the device's largest texture gets a surface
|
||||
clamped to it (`clamp_surface_extent`, aspect ratio kept) and egui is drawn at the matching
|
||||
smaller scale, instead of a `Surface::configure` validation panic (#869). The resize path and
|
||||
an unsupported surface no longer unwrap.
|
||||
Vendored
+20
@@ -670,3 +670,23 @@ mod msaa_tests {
|
||||
assert_eq!(super::clamp_msaa_samples(&[], 8), 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// SolidCraft: the surface size for a window of `width` x `height` pixels on a device whose
|
||||
/// textures are at most `max_side` on a side, and the scale it is drawn at. A window that fits is
|
||||
/// unchanged (scale 1); a larger one shrinks uniformly, keeping its aspect ratio, so the image
|
||||
/// the compositor stretches back over the window is not distorted. Never zero on a side.
|
||||
pub fn clamp_surface_extent(width: u32, height: u32, max_side: u32) -> (u32, u32, f32) {
|
||||
let max_side = max_side.max(1);
|
||||
let (width, height) = (width.max(1), height.max(1));
|
||||
let longest = width.max(height);
|
||||
if longest <= max_side {
|
||||
return (width, height, 1.0);
|
||||
}
|
||||
let scale = f64::from(max_side) / f64::from(longest);
|
||||
let fit = |side: u32| -> u32 {
|
||||
let scaled = (f64::from(side) * scale).floor() as u32;
|
||||
scaled.clamp(1, max_side)
|
||||
};
|
||||
let scale_f32 = scale as f32;
|
||||
(fit(width), fit(height), scale_f32)
|
||||
}
|
||||
Vendored
+3
-1
@@ -269,7 +269,9 @@ impl WgpuSetupCreateNew {
|
||||
required_limits: wgpu::Limits {
|
||||
// When using a depth buffer, we have to be able to create a texture
|
||||
// large enough for the entire surface, and we want to support 4k+ displays.
|
||||
max_texture_dimension_2d: 8192,
|
||||
// SolidCraft: ask for all the adapter offers (16384 on most desktop GPUs),
|
||||
// never less than 8192, so a window spanning large monitors still fits.
|
||||
max_texture_dimension_2d: adapter.limits().max_texture_dimension_2d.max(8192),
|
||||
..base_limits
|
||||
},
|
||||
// SolidCraft: without it only the sample counts WebGPU guarantees (1x, 4x)
|
||||
|
||||
Vendored
+43
-11
@@ -17,6 +17,11 @@ struct SurfaceState {
|
||||
alpha_mode: wgpu::CompositeAlphaMode,
|
||||
width: u32,
|
||||
height: u32,
|
||||
/// SolidCraft: the window's size in pixels, which `width`/`height` may be clamped down from.
|
||||
requested: (u32, u32),
|
||||
/// SolidCraft: `width / requested.0` (1 unless clamped); egui is drawn at this scale so the
|
||||
/// whole window still fits in the smaller surface.
|
||||
scale: f32,
|
||||
resizing: bool,
|
||||
needs_reconfigure: bool,
|
||||
needs_recreate: bool,
|
||||
@@ -117,16 +122,21 @@ impl Painter {
|
||||
let width = surface_state.width;
|
||||
let height = surface_state.height;
|
||||
|
||||
// SolidCraft: an unsupported surface is logged, not a panic.
|
||||
let Some(default_config) = surface_state
|
||||
.surface
|
||||
.get_default_config(&render_state.adapter, width, height)
|
||||
else {
|
||||
log::error!("The surface isn't supported by this adapter");
|
||||
return;
|
||||
};
|
||||
let mut surf_config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: render_state.target_format,
|
||||
present_mode,
|
||||
alpha_mode: surface_state.alpha_mode,
|
||||
view_formats: vec![render_state.target_format],
|
||||
..surface_state
|
||||
.surface
|
||||
.get_default_config(&render_state.adapter, width, height)
|
||||
.expect("The surface isn't supported by this adapter")
|
||||
..default_config
|
||||
};
|
||||
|
||||
if let Some(desired_maximum_frame_latency) = desired_maximum_frame_latency {
|
||||
@@ -158,8 +168,8 @@ impl Painter {
|
||||
self.install_surface(
|
||||
surface,
|
||||
viewport_id,
|
||||
old_state.width,
|
||||
old_state.height,
|
||||
old_state.requested.0,
|
||||
old_state.requested.1,
|
||||
old_state.resizing,
|
||||
);
|
||||
Ok(())
|
||||
@@ -296,6 +306,8 @@ impl Painter {
|
||||
surface,
|
||||
width,
|
||||
height,
|
||||
requested: (width, height),
|
||||
scale: 1.0,
|
||||
alpha_mode,
|
||||
resizing,
|
||||
needs_reconfigure: false,
|
||||
@@ -332,14 +344,32 @@ impl Painter {
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let width = width_in_pixels.get();
|
||||
let height = height_in_pixels.get();
|
||||
// SolidCraft: no unwraps, and a window larger than the device's largest texture (a
|
||||
// transient size while a window moves between monitors of different scale, or a window
|
||||
// spanning several monitors) gets a surface clamped to it instead of a validation panic.
|
||||
let (Some(render_state), Some(surface_state)) = (
|
||||
self.render_state.as_ref(),
|
||||
self.surfaces.get_mut(&viewport_id),
|
||||
) else {
|
||||
log::warn!("Resize before the surface and render state exist; ignored");
|
||||
return;
|
||||
};
|
||||
|
||||
let render_state = self.render_state.as_ref().unwrap();
|
||||
let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
|
||||
let max_side = render_state.device.limits().max_texture_dimension_2d;
|
||||
let requested = (width_in_pixels.get(), height_in_pixels.get());
|
||||
let (width, height, scale) = crate::clamp_surface_extent(requested.0, requested.1, max_side);
|
||||
if scale < 1.0 {
|
||||
log::warn!(
|
||||
"Window of {}x{} pixels is larger than the largest texture ({max_side}); drawing at {width}x{height}",
|
||||
requested.0,
|
||||
requested.1
|
||||
);
|
||||
}
|
||||
|
||||
surface_state.width = width;
|
||||
surface_state.height = height;
|
||||
surface_state.requested = requested;
|
||||
surface_state.scale = scale;
|
||||
|
||||
Self::configure_surface(surface_state, render_state, &self.config.surface);
|
||||
|
||||
@@ -560,9 +590,10 @@ impl Painter {
|
||||
});
|
||||
|
||||
// Upload all resources for the GPU.
|
||||
// SolidCraft: a clamped surface draws the whole window at a smaller scale.
|
||||
let screen_descriptor = renderer::ScreenDescriptor {
|
||||
size_in_pixels: [surface_state.width, surface_state.height],
|
||||
pixels_per_point,
|
||||
pixels_per_point: pixels_per_point * surface_state.scale,
|
||||
};
|
||||
|
||||
let user_cmd_bufs = {
|
||||
@@ -800,3 +831,4 @@ impl Painter {
|
||||
// TODO(emilk): something here?
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user