diff --git a/Cargo.lock b/Cargo.lock index 5e4dc897..a83dca11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1248,6 +1248,11 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dpi" +version = "0.1.1" +source = "git+https://github.com/rust-windowing/winit?rev=39c4009c41e5977d779cf8a365792c8153ca77c5#39c4009c41e5977d779cf8a365792c8153ca77c5" + [[package]] name = "dpi" version = "0.1.2" @@ -2647,7 +2652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6744ab645ebe79f9c428455c7cb9dfa4275fa3b50a25408e08f927da8dc618c" dependencies = [ "crossbeam-channel", - "dpi", + "dpi 0.1.2", "keyboard-types", "objc2 0.6.4", "objc2-app-kit 0.3.2", @@ -5516,8 +5521,7 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +source = "git+https://github.com/rust-windowing/winit?rev=39c4009c41e5977d779cf8a365792c8153ca77c5#39c4009c41e5977d779cf8a365792c8153ca77c5" dependencies = [ "ahash", "android-activity", @@ -5531,7 +5535,7 @@ dependencies = [ "core-foundation", "core-graphics", "cursor-icon", - "dpi", + "dpi 0.1.1", "js-sys", "libc", "memmap2", diff --git a/Cargo.toml b/Cargo.toml index c2126ade..0b621ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,3 +97,13 @@ opt-level = 3 lto = "thin" codegen-units = 16 debug = 0 + +# Windows 11: winit 0.30.13 resizes a window wrongly while it is dragged across monitors with +# different display scales (rust-windowing/winit#4041, #4600). It flips between the two scales, +# grows by the scale ratio (3600×2250 → 9048×5757 between 250 % and 100 %) and outgrows the GPU +# surface. Pinned to the one commit of rust-windowing/winit#4694, the 0.30.x backport of the fix +# winit master has had since 488c036a (#4341): during a live drag it applies the rect Windows +# suggests (+21/−34 lines in src/platform_impl/windows/event_loop.rs on top of v0.30.13). Remove +# once eframe depends on a winit release with the fix. +[patch.crates-io] +winit = { git = "https://github.com/rust-windowing/winit", rev = "39c4009c41e5977d779cf8a365792c8153ca77c5" } diff --git a/ROADMAP.md b/ROADMAP.md index 171c1294..dfcd0bf6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-12** · Windows: dragging the window across monitors with different display scales keeps its size instead of growing until it crashes (winit pinned to its 0.30.x backport of the fix). The GPU surface may be as large as the adapter's largest texture (16384 px on DirectX 12, Vulkan and Metal hardware) instead of 8192, and a wgpu error is logged instead of closing the app. No readiness percentage change. - **2026-10-12** · Recovered documents keep a checkpoint until saved, closed or reverted, including a second crash before autosave. Restarted document IDs and explicit recovery folders preserve other snapshots; failed republishing retains the original. No readiness percentage change. - **2026-10-12** · Book numbering and style updates prepare every chapter before saving. Only closed native chapters are changed; imported IDML remains a read-only style/PDF source. If a later disk write fails, the error names the completed chapters; the batch is not a whole-book transaction. No readiness percentage change. - **2026-10-12** · Desktop write callbacks and CLI raster exports use the atomic writer too; open readers retain the original bytes, while private Unix modes and symlink targets survive replacement. Permanent tests cover both actual callers. No readiness percentage change. diff --git a/apps/designcraft/src/gpu_limits.rs b/apps/designcraft/src/gpu_limits.rs new file mode 100644 index 00000000..5615e565 --- /dev/null +++ b/apps/designcraft/src/gpu_limits.rs @@ -0,0 +1,84 @@ +//! What the graphics device may do with a window that outgrows it, on every native platform. +//! +//! The window's surface is a texture, so its sides can't exceed the device's largest texture side, +//! and eframe asks for exactly 8192 whatever the GPU can do. A window across monitors that add up +//! to more (3440 + 3840 + 1080 pixels side by side), or one a mixed-DPI drag on Windows scaled up +//! (9048×5757 between 250 % and 100 % monitors), then failed `Surface::configure`, and wgpu's +//! default error handler panics: the app closed with the documents in it. DirectX 12, Vulkan and +//! Metal hardware offer 16384. + +use eframe::{ + NativeOptions, + egui_wgpu::WgpuSetup, + wgpu::{Device, Limits}, +}; +use std::sync::{Arc, Mutex, PoisonError}; + +/// `required` with the largest texture side the adapter's `supported` limits offer. Taking the +/// adapter's own value also never asks a smaller (downlevel) adapter for more than it has. +pub fn surface_texture_limits(mut required: Limits, supported: &Limits) -> Limits { + required.max_texture_dimension_2d = supported.max_texture_dimension_2d; + required +} + +/// Makes the device eframe creates ask for the adapter's largest texture side +/// ([`surface_texture_limits`]), keeping eframe's other device requirements. +pub fn request_adapter_texture_size(options: &mut NativeOptions) { + if let WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup { + let device_descriptor = Arc::clone(&create.device_descriptor); + create.device_descriptor = Arc::new(move |adapter| { + let mut descriptor = device_descriptor(adapter); + descriptor.required_limits = surface_texture_limits(descriptor.required_limits, &adapter.limits()); + descriptor + }); + } +} + +/// Logs wgpu errors no error scope caught instead of panicking. A window larger than even the +/// adapter's texture side then keeps its last surface (wgpu validates a configuration before +/// replacing the old one) and draws wrongly until it shrinks, instead of closing the app. +pub fn log_uncaptured_errors(device: &Device) { + // A surface that can't be configured fails again on every frame: log each message once in a row. + let last = Mutex::new(String::new()); + device.on_uncaptured_error(Arc::new(move |error| { + let message = error.to_string(); + let mut last = last.lock().unwrap_or_else(PoisonError::into_inner); + if *last != message { + log::error!("graphics: {message}"); + *last = message; + } + })); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn surface_texture_size_is_the_adapters_own() { + // A mixed-DPI drag's 9048×5757 needs more than eframe's 8192; a downlevel adapter must not + // be asked for more than it has. + for base in [Limits::default(), Limits::downlevel_webgl2_defaults()] { + let eframe_request = Limits { max_texture_dimension_2d: 8192, ..base.clone() }; + for side in [4096, 8192, 16384, 32768] { + let supported = Limits { max_texture_dimension_2d: side, ..base.clone() }; + let limits = surface_texture_limits(eframe_request.clone(), &supported); + assert_eq!(limits.max_texture_dimension_2d, side); + // The drag's 9048×5757 surface fits from 16384 up. + assert_eq!(side >= 16384, 9048 <= limits.max_texture_dimension_2d); + // Everything else eframe asks for is unchanged. + assert_eq!(Limits { max_texture_dimension_2d: 8192, ..limits.clone() }, eframe_request); + assert!(limits.check_limits(&supported)); + } + } + } + + #[test] + fn native_options_keep_creating_their_own_device() { + // The wrapper only replaces the device descriptor: eframe still creates the instance, + // adapter and device, and main.rs can still choose the backends. + let mut options = NativeOptions::default(); + request_adapter_texture_size(&mut options); + assert!(matches!(options.wgpu_options.wgpu_setup, WgpuSetup::CreateNew(_))); + } +} diff --git a/apps/designcraft/src/main.rs b/apps/designcraft/src/main.rs index e3ff86cf..8f75573c 100644 --- a/apps/designcraft/src/main.rs +++ b/apps/designcraft/src/main.rs @@ -13,6 +13,7 @@ mod control_server; mod gpu; +mod gpu_limits; mod logging; #[cfg(target_os = "macos")] mod native_menu; @@ -258,6 +259,9 @@ fn main() -> eframe::Result { if let Some(icon) = app_icon() { options.viewport = options.viewport.with_icon(icon); } + // The adapter's largest texture side instead of eframe's 8192, so a window across several + // monitors keeps its surface (see `gpu_limits`). + gpu_limits::request_adapter_texture_size(&mut options); // The graphics backend, decided before the window exists: a driver that faults takes the // process down before any Rust code can catch it (see `gpu`). let gpu = gpu::Startup::begin(if no_prefs() { None } else { profile::config_dir().map(|d| d.join(gpu::FILE)) }, env!("CARGO_PKG_VERSION")); @@ -282,6 +286,7 @@ fn main() -> eframe::Result { Box::new(move |cc| { let mut gpu = gpu; if let Some(rs) = &cc.wgpu_render_state { + gpu_limits::log_uncaptured_errors(&rs.device); // What a bug report needs to know about the graphics driver. let info = rs.adapter.get_info(); log::info!("graphics: {} on {:?} ({:?}; driver {} {})", info.name, info.backend, info.device_type, info.driver, info.driver_info); diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index fb92c587..9d6af612 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -1,6 +1,6 @@ # Hardware and platform parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (Windows mixed-DPI drag and windows wider than 8192 px don't crash) · **Target:** Adobe InDesign 2026 (21.6) What InDesign does with hardware, per platform, against DesignCraft. Layout is not a GPU-hungry workload, so this dimension carries little weight, but a start that fails on a @@ -16,9 +16,9 @@ graphics driver loses the user entirely. Overall numbers: [target-app-parity.md] | Start-up on any GPU | robust | Metal device lost on Intel Macs (#334) | AMD OpenGL crash (#167), launch failures (#273) | some reports (#192, #220) | | | Backend fallback | n/a | `gpu.json` skips a backend whose start never showed a frame (#297) | same | same | | | Animated / scrubby zoom | ✓ (GPU) | ✓ (CPU tiles, damage regions) | ✓ | ✓ | ✓ | -| HiDPI / Retina | ✓ | ✓ | ✓ | ✓ | ✓ | +| HiDPI / Retina | ✓ | ✓ | ✓ (drags across monitors with different scales keep the window's size: winit pinned to the rust-windowing/winit#4694 backport) | ✓ | ✓ | | UI scaling | ✓ | ✓ (menus and dialogs usable at larger scales, #281) | ✓ | ✓ | ✓ | -| Multi-monitor, floating document windows | ✓ | ~ one window; New Window for the same document | ~ | ~ | ✗ | +| Multi-monitor, floating document windows | ✓ | ~ one window; New Window for the same document; the window may span monitors up to the GPU's largest texture (16384 px on DirectX 12, Vulkan and Metal hardware), and beyond it draws wrongly instead of closing | ~ | ~ | ✗ | | Printing | system print with PPDs, PostScript, separations | PDF to CUPS (`craft-print`) | Windows PDF raster spooler (`craft-print`) | PDF to CUPS (`craft-print`) | browser download | | Colour-managed display (monitor profile) | ✓ | ✗ (working CMYK profile simulated; monitor profile not applied) | ✗ | ✗ | ✗ | | Touch / pen | Touch workspace (Windows), gestures | trackpad pinch/scroll | ~ | ~ | ~ | @@ -53,6 +53,7 @@ the developers don't use daily. | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Windows: dragging across monitors with different display scales doesn't grow the window and crash (winit pinned to the rust-windowing/winit#4694 backport); a window wider or taller than 8192 px keeps its surface, and a wgpu error logs instead of closing the app | | 2026-10-10 | major | Created from the code, InDesign 2026's GPU Performance features and the issue tracker | Windows submission now uses shared craft-print. It selects matching or default driver paper,