diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c817716d..9d539c31 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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() { diff --git a/crates/ui-egui/src/surface_clamp_tests.rs b/crates/ui-egui/src/surface_clamp_tests.rs new file mode 100644 index 00000000..84b4aaa8 --- /dev/null +++ b/crates/ui-egui/src/surface_clamp_tests.rs @@ -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); + } +} diff --git a/vendor/README.md b/vendor/README.md index 88638dff..0d30476d 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -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. diff --git a/vendor/egui-wgpu/src/lib.rs b/vendor/egui-wgpu/src/lib.rs index b07e4528..b69c65ae 100644 --- a/vendor/egui-wgpu/src/lib.rs +++ b/vendor/egui-wgpu/src/lib.rs @@ -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) +} diff --git a/vendor/egui-wgpu/src/setup.rs b/vendor/egui-wgpu/src/setup.rs index ef2fa48b..f3a881ca 100644 --- a/vendor/egui-wgpu/src/setup.rs +++ b/vendor/egui-wgpu/src/setup.rs @@ -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) diff --git a/vendor/egui-wgpu/src/winit.rs b/vendor/egui-wgpu/src/winit.rs index 7c61f2f1..117f7c49 100644 --- a/vendor/egui-wgpu/src/winit.rs +++ b/vendor/egui-wgpu/src/winit.rs @@ -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? } } +