diff --git a/README.md b/README.md index 0ec61c0..b1cca17 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,12 @@ Remove-Item Env:WGPU_BACKEND # restore the default for later An explicit `gl` override can bring the driver crash back on affected systems. The macOS, Linux and web backend defaults are unchanged. +**If the app is slow on an older Windows PC:** when DirectX 12 finds no graphics card with a +driver for it (for example Intel 4th-generation graphics on Windows 11), it only offers Microsoft's +software renderer, which draws every frame on the processor. In that case WordCraft uses OpenGL +instead, without setting anything. The log file records the choice (lines starting with +`graphics:`). `WGPU_BACKEND` still overrides it, e.g. `$env:WGPU_BACKEND = "gl"` or `"dx12"`. + ### macOS | Build | File | Notes | diff --git a/ROADMAP.md b/ROADMAP.md index 37c3a7c..e391b38 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -130,7 +130,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | What landed | |---|---| -| 2026-10-10 | Full re-measure against Word for Mac 16.113.4 and progress docs to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `roadmap.md`, `architecture.md`, format/layout/UI/hardware/localization parity; `docs/parity.md` → `docs/parity-checklist.md`). Landed the same day (~80 PRs): equations with OMML and an Equation tab (#191); Word 97-2003 `.doc` import (#145); right-to-left and Persian text (#207); Zotero integration (#189); Read Aloud player (#190); text boxes and floating objects editable (#46) and placed like Word (#136); floating tables (#137); track-changes fixes (#125, #244); typing parity with Word (#204); hidden text (#173); save prompts and AutoSave rules (#151); LaTeX import/export (#11); keytips and mini-toolbar (#43); Spanish, Ukrainian and Brazilian Portuguese interfaces; system theme (#249); Linux file dialogs no longer freeze (#246); Chinese UI font on Windows (#248); every installed font weight (#239); cell text direction (#245); Paste Special (#235), Advanced Find (#234), Column Selection (#237), Style Inspector (#236), custom table styles (#256), View gridlines (#243), zoom buttons (#232), inertial touchpad scrolling (#252), File Info fields kept (#268), ¶ in mixed-direction paragraphs (#278), Serbian interface (#250), table Height/Width boxes, AutoFit Contents and Table Properties (#44), AutoSave switch greyed out with the reason where it can't save (.md/.txt/.html, unsaved, browser) (#196, #176); v0.4.0 released | +| 2026-10-10 | Full re-measure against Word for Mac 16.113.4 and progress docs to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `roadmap.md`, `architecture.md`, format/layout/UI/hardware/localization parity; `docs/parity.md` → `docs/parity-checklist.md`). Landed the same day (~80 PRs): equations with OMML and an Equation tab (#191); Word 97-2003 `.doc` import (#145); right-to-left and Persian text (#207); Zotero integration (#189); Read Aloud player (#190); text boxes and floating objects editable (#46) and placed like Word (#136); floating tables (#137); track-changes fixes (#125, #244); typing parity with Word (#204); hidden text (#173); save prompts and AutoSave rules (#151); LaTeX import/export (#11); keytips and mini-toolbar (#43); Spanish, Ukrainian and Brazilian Portuguese interfaces; system theme (#249); Linux file dialogs no longer freeze (#246); Chinese UI font on Windows (#248); every installed font weight (#239); cell text direction (#245); Paste Special (#235), Advanced Find (#234), Column Selection (#237), Style Inspector (#236), custom table styles (#256), View gridlines (#243), zoom buttons (#232), inertial touchpad scrolling (#252), File Info fields kept (#268), ¶ in mixed-direction paragraphs (#278), Serbian interface (#250), table Height/Width boxes, AutoFit Contents and Table Properties (#44), AutoSave switch greyed out with the reason where it can't save (.md/.txt/.html, unsaved, browser) (#196, #176), OpenGL fallback on Windows PCs without a DirectX 12 driver (#316); v0.4.0 released | | 2026-10-09 | Interface languages follow the system or Options (#12; zh-hans, zh-hant, ja); TOC page numbers as an updatable field (#52); rotating log file (#17); window geometry remembered (#38) | | 2026-10-08 | v0.2.0 and v0.3.0 released; Flatpak and AppImage auto-update (#22) | | 2026-10-07 | v0.1.0 released; contributor credits in About (#6) | @@ -141,6 +141,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-10 | trivial | Progress log: Windows OpenGL fallback without a DirectX 12 driver (#316) | | 2026-10-10 | minor | Password-protected .docx (#281): files encrypted with a password (agile encryption) open and save; File › Info › Encrypt with Password | | 2026-10-10 | minor | Relanded clipboard pane, table styles editor, Manage Styles, Asian typography (kinsoku), Group/Ungroup and Shape Effects (#264 #263 #270 #271 #267 #280): catalog 390/431 (90.5%) | | 2026-10-10 | minor | Merged main: catalog 384/430 (89.3%), 431 commands; Paste Special, Advanced Find, Column Selection, Style Inspector, custom table styles, gridlines, zoom buttons, touchpad scrolling, File Info, Serbian landed; mainstream 54→55% | diff --git a/apps/wordcraft/src/graphics.rs b/apps/wordcraft/src/graphics.rs index ccd0c45..85aa433 100644 --- a/apps/wordcraft/src/graphics.rs +++ b/apps/wordcraft/src/graphics.rs @@ -1,33 +1,122 @@ //! Windows graphics instance configuration, applied before wgpu can initialize any driver. -use eframe::{NativeOptions, egui_wgpu::WgpuSetup, wgpu::Backends}; +use eframe::{ + NativeOptions, + egui_wgpu::WgpuSetup, + wgpu::{Backends, DeviceType}, +}; -/// Restrict the wgpu instance to DirectX 12 unless `backend_override` (normally -/// `Backends::from_env()`, i.e. `WGPU_BACKEND`) names other backends. -pub fn configure(options: &mut NativeOptions, backend_override: Option) { +/// What the start-up probe learned about one DirectX 12 adapter. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Dx12Adapter { + /// `Cpu` is WARP (Microsoft's software rasteriser): it works, but every frame is drawn on the + /// processor, which makes the app lag on older machines (#316). + pub device_type: DeviceType, + /// Whether the adapter offers the limits eframe will ask for when it creates the device. + pub meets_limits: bool, +} + +/// The backends to initialize on Windows. +/// +/// An explicit `backend_override` (normally `Backends::from_env()`, i.e. `WGPU_BACKEND`) always +/// wins. Otherwise DirectX 12 only, as long as `dx12_adapters` (what a DirectX 12-only probe found) +/// contains a usable hardware GPU. Without one, OpenGL is the way to the GPU: graphics chips whose +/// driver has no DirectX 12 support (Intel 4th generation on Windows 11, #316) still have an +/// OpenGL driver, and GL can't reach the AMD driver crash described in [`configure_backends`], +/// because an AMD GPU with a driver shows up as a DirectX 12 adapter and keeps us on DirectX 12. +/// When DirectX 12 offers only WARP, it stays enabled next to GL as a last resort for machines +/// without an OpenGL driver either (virtual machines): wgpu prefers any GPU over a software +/// adapter, so it's only picked when GL has nothing better. +pub fn choose_backends(backend_override: Option, dx12_adapters: &[Dx12Adapter]) -> Backends { + if let Some(backends) = backend_override { + return backends; + } + let hardware = |a: &&Dx12Adapter| a.device_type != DeviceType::Cpu; + if dx12_adapters.iter().filter(hardware).any(|a| a.meets_limits) { + Backends::DX12 + } else if !dx12_adapters.is_empty() && !dx12_adapters.iter().any(|a| hardware(&a)) { + Backends::DX12 | Backends::GL + } else { + // No DirectX 12 adapter at all, or only GPUs that can't run the app: GL alone, so a + // DirectX 12 GPU that would fail device creation isn't picked over the OpenGL one. + Backends::GL + } +} + +/// Restricts the wgpu instance eframe creates to `backends`. +pub fn configure_backends(options: &mut NativeOptions, backends: Backends) { if let WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup { // eframe's default includes GL. Creating that backend can crash inside AMD's // atio6axx.dll before adapter selection or any Rust error handling runs, so the // window never appears. Choosing an adapter afterwards is too late: only leaving GL // out of the instance avoids it. This matches the verified WGPU_BACKEND=dx12 // workaround without mutating process environment variables. - create.instance_descriptor.backends = backend_override.unwrap_or(Backends::DX12); + create.instance_descriptor.backends = backends; } } +/// Chooses the backends ([`choose_backends`]) and applies them to `options`. Without a +/// `backend_override` this first probes DirectX 12 with a short-lived instance of its own, which +/// is dropped before eframe creates the real one. +#[cfg(target_os = "windows")] +pub fn configure(options: &mut NativeOptions, backend_override: Option) { + let backends = match backend_override { + Some(backends) => { + log::info!("graphics: WGPU_BACKEND override, backends {backends:?}"); + backends + } + None => { + let adapters = probe_dx12(options); + let backends = choose_backends(None, &adapters); + log::info!("graphics: {} DirectX 12 adapter(s) {adapters:?}, backends {backends:?}", adapters.len()); + backends + } + }; + configure_backends(options, backends); +} + +/// Lists the DirectX 12 adapters, checking each against the limits eframe's device descriptor +/// will require. Empty when this build has no DirectX 12 backend. +#[cfg(target_os = "windows")] +fn probe_dx12(options: &NativeOptions) -> Vec { + use eframe::wgpu::{Instance, InstanceDescriptor}; + // `Instance::new` panics when the build has no backend for the platform. + if !Instance::enabled_backend_features().contains(Backends::DX12) { + return Vec::new(); + } + let WgpuSetup::CreateNew(create) = &options.wgpu_options.wgpu_setup else { + return Vec::new(); + }; + let instance = Instance::new(InstanceDescriptor { backends: Backends::DX12, ..InstanceDescriptor::new_without_display_handle() }); + pollster::block_on(instance.enumerate_adapters(Backends::DX12)) + .iter() + .map(|adapter| { + let info = adapter.get_info(); + let required = (create.device_descriptor)(adapter).required_limits; + let meets_limits = required.check_limits(&adapter.limits()); + log::info!("graphics: DirectX 12 adapter {:?} ({:?}, driver {:?}), limits ok: {meets_limits}", info.name, info.device_type, info.driver); + Dx12Adapter { device_type: info.device_type, meets_limits } + }) + .collect() +} + #[cfg(test)] mod tests { use super::*; - fn configured_backends(backend_override: Option) -> Backends { + fn configured_backends(backends: Backends) -> Backends { let mut options = NativeOptions::default(); - configure(&mut options, backend_override); + configure_backends(&mut options, backends); let WgpuSetup::CreateNew(create) = options.wgpu_options.wgpu_setup else { panic!("default native options must create a wgpu instance"); }; create.instance_descriptor.backends } + const fn adapter(device_type: DeviceType, meets_limits: bool) -> Dx12Adapter { + Dx12Adapter { device_type, meets_limits } + } + #[test] fn windows_default_initializes_only_dx12() { // eframe's own default (PRIMARY | GL) would initialize OpenGL as well. @@ -35,13 +124,32 @@ mod tests { panic!("default native options must create a wgpu instance"); }; assert!(eframe_default.instance_descriptor.backends.contains(Backends::GL)); - assert_eq!(configured_backends(None), Backends::DX12); + let gpu = [adapter(DeviceType::IntegratedGpu, true)]; + assert_eq!(configured_backends(choose_backends(None, &gpu)), Backends::DX12); } #[test] fn explicit_backend_override_is_preserved() { + let warp_only = [adapter(DeviceType::Cpu, true)]; for backends in [Backends::VULKAN, Backends::GL, Backends::DX12, Backends::VULKAN | Backends::DX12, Backends::empty()] { - assert_eq!(configured_backends(Some(backends)), backends); + // Whatever the probe would have found, the override wins (the probe doesn't run then). + assert_eq!(choose_backends(Some(backends), &[]), backends); + assert_eq!(choose_backends(Some(backends), &warp_only), backends); + assert_eq!(configured_backends(backends), backends); } } + + #[test] + fn gl_when_dx12_has_no_usable_gpu() { + // #316: no DirectX 12 driver, so DirectX 12 offers only WARP (or nothing) and lags. + let warp_only = [adapter(DeviceType::Cpu, true)]; + assert_eq!(choose_backends(None, &warp_only), Backends::GL | Backends::DX12); + assert_eq!(choose_backends(None, &[]), Backends::GL); + // A GPU that can't create eframe's device: GL alone, so it isn't picked over the GL one. + let weak_gpu = [adapter(DeviceType::IntegratedGpu, false), adapter(DeviceType::Cpu, true)]; + assert_eq!(choose_backends(None, &weak_gpu), Backends::GL); + // Any usable GPU keeps DirectX 12 only, next to WARP or a weaker GPU. + let mixed = [adapter(DeviceType::Cpu, true), adapter(DeviceType::IntegratedGpu, false), adapter(DeviceType::DiscreteGpu, true)]; + assert_eq!(choose_backends(None, &mixed), Backends::DX12); + } } diff --git a/apps/wordcraft/src/main.rs b/apps/wordcraft/src/main.rs index 15e9b4a..d825752 100644 --- a/apps/wordcraft/src/main.rs +++ b/apps/wordcraft/src/main.rs @@ -206,7 +206,8 @@ fn main() -> eframe::Result { if let Some(icon) = app_icon() { options.viewport = options.viewport.with_icon(icon); } - // Before eframe creates the wgpu instance: default Windows to DirectX 12 only (see graphics.rs). + // Before eframe creates the wgpu instance: default Windows to DirectX 12 only, or OpenGL when + // DirectX 12 has no GPU driver (see graphics.rs). #[cfg(target_os = "windows")] graphics::configure(&mut options, eframe::wgpu::Backends::from_env()); let restored = saved_window(); diff --git a/docs/gaps.md b/docs/gaps.md index 5f2a56c..fa86a4a 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -13,7 +13,7 @@ first (`gh pr list`), and update this file and the parity doc when a gap closes. |---|---|---|---|---|---|---| | 1 | **DOCX never tested on real-world files.** No corpus of files from Word, Google Docs, LibreOffice and Pages has been opened, rendered and round-tripped and compared with Word page by page | B | No corpus in the repo; `crates/docx/tests` are synthetic fixtures; "DOCX fidelity corpus: not started" since 2026-10-06 | Everyone: the files people receive are the files that break | 30–50 | [file-format-parity.md](file-format-parity.md) | | 2 | **Charts, SmartArt, OLE objects, ink and group shapes are dropped on open and lost on save, silently** | B | No `c:chart`, `dgm`, `wpg`, `w:object` handling in `crates/docx` | Business and academic documents lose content without warning | 10–15 to preserve and show fallback pictures; 45–70 to render and edit charts and SmartArt | [file-format-parity.md](file-format-parity.md) | -| 3 | **Crashes and freezes in the field**: startup crash on Intel UHD Graphics (#170), freezes (#77, #59 Thai fonts, #31 font selector), no writes without admin on Windows 11 (#218), broken shape tool (#142), images can't be moved (#82) | B | 62 open issues, ~15 of them crashes, hangs or data-path failures | Anyone hit loses work or trust; the never-crash standard covers panics, not hangs or GPU driver faults | 20–30 | [target-app-parity.md](target-app-parity.md) (Stability) | +| 3 | **Crashes and freezes in the field**: startup crash on Intel UHD Graphics (#170) (lag on GPUs without a DirectX 12 driver fixed, #316), freezes (#77, #59 Thai fonts, #31 font selector), no writes without admin on Windows 11 (#218), broken shape tool (#142), images can't be moved (#82) | B | 62 open issues, ~15 of them crashes, hangs or data-path failures | Anyone hit loses work or trust; the never-crash standard covers panics, not hangs or GPU driver faults | 20–30 | [target-app-parity.md](target-app-parity.md) (Stability) | | 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; columns don't balance; long footnotes don't continue; legacy compatibility options ignored; no page-by-page comparison harness | B | [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) | | 5 | **Formatting revisions, move tracking and content controls** are lost (`w:rPrChange`/`w:pPrChange` absent; `w:sdt` unwrapped) | B | Issue #41; source grep | Legal and editorial review workflows; templates with form controls | 12–18 | [file-format-parity.md](file-format-parity.md) | | 6 | **No native printing** on desktop: File › Print opens the PDF in the system viewer, which prints (#286); no print dialog of our own (printer, copies, ranges); web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) | @@ -64,6 +64,7 @@ The same gaps grouped the way the parity documents are, for agents working in on | Date | Change | Summary | |---|---|---| +| 2026-10-10 | trivial | #3: Windows lag on GPUs without a DirectX 12 driver fixed with an OpenGL fallback (#316) | | 2026-10-10 | trivial | #6: desktop File › Print opens the system PDF viewer (#286) | | 2026-10-10 | trivial | Table Properties dialog landed (#44); removed from the dialog-depth list | | 2026-10-10 | trivial | Merged main: landed features removed from the feature-gap list | diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md index f9a3934..ebcfb96 100644 --- a/docs/hardware-parity.md +++ b/docs/hardware-parity.md @@ -11,7 +11,7 @@ pens, microphones (Dictate) and speakers (Read Aloud), and displays. | Hardware | Word | WordCraft macOS | Windows | Linux / BSD | Web | Parity | Hours | |---|---|---|---|---|---|---|---| | Printers: native print dialog, printer choice, copies, duplex, page ranges, tray, scaling | ✅ (`WordPDE.plugin` print-dialog extension on Mac) | 🟡 PDF opens in Preview to print (#286) | 🟡 PDF opens in the default viewer (#286) | 🟡 PDF opens in the default viewer (#286) | ✅ browser print dialog (#209) | 35% | 5–8 | -| GPU rendering, HiDPI/Retina, pixel-aligned text | ✅ | ✅ wgpu + vello_cpu rasteriser, whole-pixel text (#110, #165) | ✅ DirectX 12 default (#50); startup crash on Intel UHD (#170) | ✅ (soft text on KDE at some zooms, #140) | ✅ | 80% | 3–5 (bugs) | +| GPU rendering, HiDPI/Retina, pixel-aligned text | ✅ | ✅ wgpu + vello_cpu rasteriser, whole-pixel text (#110, #165) | ✅ DirectX 12 default (#50), OpenGL when DirectX 12 has no GPU driver (#316); startup crash on Intel UHD (#170) | ✅ (soft text on KDE at some zooms, #140) | ✅ | 80% | 3–5 (bugs) | | Multiple monitors, per-monitor DPI, remembered window position | ✅ | ✅ (#38, #76) | ✅ (winit DPI fix pending, #155) | ✅ | — | 85% | 1 | | Pen / stylus: ink, pressure, eraser, ink to shape/math | ✅ (Draw tab, `InkRender.bundle`) | ❌ | ❌ | ❌ | ❌ | 0% | (Draw tab, 15–25) | | Microphone: Dictate | ✅ (cloud speech service) | ❌ | ❌ | ❌ | ❌ | 0% | 8–15 + owner (speech model choice) | @@ -23,6 +23,7 @@ pens, microphones (Dictate) and speakers (Read Aloud), and displays. | Date | Change | Summary | |---|---|---| +| 2026-10-10 | trivial | Windows falls back to OpenGL when DirectX 12 offers only the software adapter (#316) | | 2026-10-10 | trivial | Desktop File › Print opens the PDF in the system viewer (#286) | | 2026-10-10 | trivial | Inertial touchpad scrolling landed (#252) | | 2026-10-10 | major | First version |