Bugfix: macOS Monterey white-flash loop then silent exit #225

Closed
opened 2026-09-07 16:16:05 +01:00 by gronod · 0 comments
Owner

Project: ICCery
Component: Tauri v2 window / WKWebView / Stage 5 WebGL
Affects: macOS 12 (Monterey), especially Intel GPUs
Severity: High (app unusable on a documented support target)
Type: Bugfix
Related report: App window flashes white several times (redraw) then exits. No Crash Reporter dialog for ICCery.app.


1. Summary

On macOS Monterey the packaged app opens a native window, paints white several times, then disappears. Apple Crash Reporter does not attach to ICCery.app.

This is the WKWebView Web Content / GPU helper dying and being respawned. Each respawn shows WKWebView’s default white backing store. After a few cycles the main process tears the window down. That is why there is no ICCery crash report.

ICCery currently:

  • Shows the main window immediately (visible defaults to true).
  • Does not set a window or webview background colour. Theme is dark (--bg-card#1a1a22, gamut scene 0x0e0e14).
  • Creates a THREE.WebGLRenderer({ antialias: true, alpha: true }) and starts requestAnimationFrame from initGamutViewer() during DOMContentLoaded, even when Stage 5 is hidden.
  • Declares bundle.macOS.minimumSystemVersion = "10.15" while README says 11.0+ and the stack is Tauri 2 + WebGL.
  • Logs frontend messages via log_frontend_message, but does not handle RunEvent webview / web-content termination.

This ticket specifies the five fixes required to stop the flash-loop, survive a lost WebGL context, tell the truth about supported OS versions, and leave a log when WebKit dies.


2. Environment / evidence

Item Value
App ICCery 0.8.4 (com.gronod.iccery)
Stack Tauri 2, wry/WKWebView, vanilla JS, Three.js
Frontend dist src/ (no bundler base path issue)
Window config src-tauri/tauri.conf.jsonapp.windows[0]
Rust entry src-tauri/src/lib.rs (iccery_lib)
Capabilities src-tauri/capabilities/default.json (core:default, dialog:default)
Existing macOS crates objc2, objc2-app-kit, objc2-foundation
Logger tauri-plugin-logiccery log dir + stdout + webview; JS src/js/logger.js

Repro symptoms:

  1. Launch on macOS 12.x.
  2. Window appears, flashes white 2–N times.
  3. Process exits.
  4. ~/Library/Logs/DiagnosticReports may contain com.apple.WebKit.WebContent or com.apple.WebKit.GPU, not ICCery.
  5. Terminal launch (ICCery.app/Contents/MacOS/ICCery) may print web content process terminated.

3. Root cause (working model)

  1. White flash. WKWebView on macOS draws an opaque white layer until HTML/CSS composite. Dark UI makes this obvious. Hidden-then-shown is the standard mitigation; backgroundColor / drawsBackground=NO removes the remaining flash. See wry#1662 and tauri#1564.
  2. Repeated flashes. Web Content process crash → WKWebView rebuild → white paint → crash again. Typical trigger on Monterey WebKit (Safari 15 / WebKit 613): WebGL2/WebGL context creation, antialias + high devicePixelRatio, continuous rAF on a hidden canvas.
  3. Silent exit. Helper process death does not produce an ICCery crash report. lib.rs RunEvent match ignores webview destruction / content process death.
  4. Eager WebGL. src/js/app.js calls safeInit('Gamut Viewer', initGamutViewer) on every launch. initGamutViewer() in src/js/gamut_viewer.js constructs WebGLRenderer and calls animate() immediately. Stage 5 visibility is only checked for the R key handler.

4. Goals

After this work:

  • Main window is not shown until the first frontend paint signal.
  • Native window + WKWebView backing colour match the dark theme (#1a1a22 / #0e0e14).
  • WKWebView does not draw its default white background on macOS.
  • Three.js / WebGL is created only when Stage 5 is first shown; context loss is handled; missing WebGL does not take down the app.
  • minimumSystemVersion and README state a support policy that matches Tauri 2 + WebGL.
  • Web Content / webview termination is written to the ICCery log file.

Out of scope: rewriting the gamut viewer, bumping Tauri major, shipping a bundled WebKit.


5. Fix 1 — Do not show the window until first paint

5.1 Config

src-tauri/tauri.conf.jsonapp.windows[0]:

{
  "label": "main",
  "title": "ICCery",
  "width": 1280,
  "height": 800,
  "minWidth": 1100,
  "minHeight": 700,
  "visible": false,
  "backgroundColor": "#1A1A22"
}

Notes:

  • label: "main" must stay aligned with capabilities/default.json "windows": ["main"].
  • backgroundColor uses the theme fallback already in index.html (var(--bg-card, #1a1a22)).
  • Confirm against the Tauri 2 schema in use (https://schema.tauri.app/config/2). If backgroundColor is rejected by the pinned CLI, keep visible: false and implement colour only in Fix 2.

5.2 Show path (preferred: Rust command)

Do not rely on @tauri-apps/api window permissions unless we add them. The frontend already uses window.__TAURI__.core.invoke.

Add command show_main_window in src-tauri/src/commands.rs and register it in lib.rs generate_handler!.

#[tauri::command]
pub fn show_main_window(app: tauri::AppHandle) -> Result<(), String> {
    if let Some(win) = app.get_webview_window("main") {
        win.show().map_err(|e| e.to_string())?;
        let _ = win.set_focus();
        log::info!("Main window shown after frontend ready");
    } else {
        log::warn!("show_main_window: window 'main' not found");
    }
    Ok(())
}

lib.rs already imports tauri::Manager (needed for get_webview_window).

5.3 Frontend signal

In src/js/app.js, at the end of the DOMContentLoaded handler (after all safeInit calls):

requestAnimationFrame(() => {
  requestAnimationFrame(async () => {
    try {
      await invoke('show_main_window');
    } catch (e) {
      console.warn('[ICCery] show_main_window failed:', e);
    }
  });
});

Double-rAF waits for layout + first paint of the dark CSS. Fallback timeout of 1500 ms must also call show_main_window so a JS exception cannot leave a permanently hidden window.

5.4 Safety

  • If tauri dev hot-reloads, show() on an already-visible window must be idempotent (it is).
  • Do not call show() from Rust setup() — that races the white webview.

5.5 Acceptance

On Monterey and later, first visible frame is the dark UI (or the configured #1A1A22), not a white rectangle, when WebKit is healthy.


6. Fix 2 — Kill WKWebView’s white backing on macOS

backgroundColor in config is not enough on macOS: WKWebView still paints white until wry drawsBackground / underPageBackgroundColor are set.

6.1 Where

Add a helper module src-tauri/src/macos_webview.rs (cfg-gated) and call it from lib.rs .setup() after the window exists, and again from show_main_window in case the webview was recreated.

The crate already depends on objc2 / objc2-app-kit / objc2-foundation for printing. Prefer the same stack. If the pinned wry already exposes WebviewWindow::set_background_color / platform with_webview, use that first and keep the objc path as fallback.

6.2 Required behaviour (macOS only)

  1. NSWindow.setBackgroundColor → sRGB 26/255, 26/255, 34/255, 1 (#1A1A22).
  2. WKWebView: set drawsBackground to NO via KVC (same private key wry uses for transparency), or the public wry/Tauri API if available in the locked Tauri 2 patch.
  3. If running on macOS 12+: set underPageBackgroundColor on the WKWebView to the same colour (covers overscroll / unpainted page).

Sketch (adapt to the exact objc2 versions already in Cargo.toml):

#[cfg(target_os = "macos")]
pub fn paint_dark_webview(window: &tauri::WebviewWindow) {
    // 1) NSWindow background
    // 2) window.with_webview(|wv| { set drawsBackground / underPageBackgroundColor })
}

Do not set the window transparent: true. That changes hit-testing and titlebar compositing. We only want a dark opaque backing.

6.3 Other platforms

No-op. Windows/Linux flash is a different compositor; do not change those code paths in this ticket.

6.4 Acceptance

On Monterey, with Fix 1 applied, a slow first paint must not flash white. A brief dark frame is acceptable.


7. Fix 3 — Defer and harden WebGL (Stage 5 only)

7.1 Stop creating the renderer on launch

src/js/app.js today:

safeInit('Gamut Viewer', initGamutViewer);

Change to lazy init:

  • Remove initGamutViewer() from the DOMContentLoaded batch.
  • Call it the first time Stage 5 becomes visible (wizardState.navigateToStage(5) / stepper click / hash restore).
  • Keep a module-level gamutViewerReady flag so it runs once.

Implementation options (pick one, prefer A):

A. Export ensureGamutViewer() from gamut_viewer.js and call it from state.js when targetStep === 5 and the stage element is not .hidden.
B. Observe #stage-5 with IntersectionObserver / MutationObserver on the hidden class.

Do not start WebGL while Stage 5 is in the DOM but display:none / .hidden. Hidden containers report clientWidth === 0; the code already falls back to 500×400 and will still create a GPU context.

7.2 Feature detect before new WebGLRenderer

At the top of renderer creation in initGamutViewer():

function webglAvailable() {
  try {
    const c = document.createElement('canvas');
    return !!(c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl'));
  } catch {
    return false;
  }
}

If false: leave a visible message in #gamutViewerContainer (“3D gamut viewer requires WebGL; the rest of ICCery still works”), logger.warn(...), return. Never throw into safeInit as a fatal path.

7.3 Safer renderer flags on old WebKit

Monterey Intel: drop work that blows the GPU process.

const lowPower = isLikelyConstrainedGpu(); // see below
renderer = new THREE.WebGLRenderer({
  antialias: !lowPower,
  alpha: false,          // opaque; matches scene.background
  powerPreference: lowPower ? 'low-power' : 'default',
  failIfMajorPerformanceCaveat: false,
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, lowPower ? 1 : 2));

isLikelyConstrainedGpu() heuristic (log the result):

  • navigator.platform contains MacIntel (Rosetta or Intel).
  • Optional: parse sw_vers via a tiny get_os_info command if we add one in Fix 4; otherwise UA / navigator.userAgentData is enough.

alpha: true on a dark opaque scene is unnecessary and has caused compositor flashes with CSS overlays.

7.4 Context-loss handlers

After appendChild(renderer.domElement):

renderer.domElement.addEventListener('webglcontextlost', (e) => {
  e.preventDefault();
  logger.error('WebGL context lost', 'GamutViewer');
  stopAnimate();
});
renderer.domElement.addEventListener('webglcontextrestored', () => {
  logger.warn('WebGL context restored — rebuilding viewer', 'GamutViewer');
  // dispose + re-init, or set a "Reload 3D view" button
});

animate() must check a running flag so a lost context does not spin rAF forever.

function animate() {
  if (!animationRunning) return;
  requestAnimationFrame(animate);
  // ...
}

On Stage 5 hide, set animationRunning = false. On show, restart. Continuous rAF on a hidden WebGL canvas is a known Monterey killer.

7.5 Dispose on failure

If new THREE.WebGLRenderer throws (some WebKit builds throw instead of returning null):

  • Catch (already wrapped, but the message is only console.warn).
  • logger.error with stack.
  • Do not leave a half-attached canvas.

7.6 Acceptance

  • Launching ICCery on Monterey does not create a WebGL context until the user opens Stage 5.
  • If WebGL is missing or the context is lost, Stages 1–4 and settings remain usable.
  • Opening Stage 5 twice does not leak a second renderer.

8. Fix 4 — Honest minimum OS + docs

8.1 Bundle plist

src-tauri/tauri.conf.json:

"macOS": {
  "minimumSystemVersion": "12.0",
  ...
}

Rationale:

  • Current value 10.15 is false for Tauri 2 + this UI.
  • README already claims 11.0+.
  • WebGL + WKWebView quality below 12 is not something we will test.
  • Setting 12.0 makes Gatekeeper / Installer reject 10.15/11 rather than flash-loop.

If product still wants Big Sur installs: set 11.0 and mark WebGL as unsupported there. Do not keep 10.15.

8.2 README / ROADMAP

Replace the macOS support bullet with an explicit matrix:

macOS Status
13+ (Ventura and newer), Apple Silicon Supported
13+, Intel Supported
12.7.x Monterey, Apple Silicon Supported, WebGL best-effort
12.0–12.6 Monterey, Intel Best-effort; WebGL deferred; known WKWebView GPU process crashes
11 Big Sur Not supported (installer should refuse once min version is 12.0)
10.15 Catalina Not supported

Add a short “macOS troubleshooting” note:

  • Launch from Terminal to see web content process terminated.
  • Check ~/Library/Logs/DiagnosticReports for WebKit.WebContent / WebKit.GPU.
  • ICCery log file location (already used by tauri-plugin-log): app log dir / iccery.log.
  • Custom ColorSync display profiles can crash toolkit UIs on Monterey; Safe Mode / default display profile is a valid support question.

8.3 Optional runtime banner

get_app_info (already invoked from About) should include os + arch. If macOS major < 13 and Intel, show a one-time notice in the existing wizard notification strip: 3D gamut view may be unavailable; profiling stages still work.

8.4 Acceptance

A fresh DMG on 10.15/11 does not install (or shows a clear OS requirement). Docs no longer promise Catalina.


9. Fix 5 — Log Web Content termination

Users cannot file useful bugs when Crash Reporter is silent. ICCery already has a file logger.

9.1 RunEvent in lib.rs

Extend the existing .run(|app_handle, event| { match event { ... }}).

Handle at least:

tauri::RunEvent::WindowEvent { label, event, .. } => {
    match event {
        tauri::WindowEvent::Destroyed => {
            log::error!("Window destroyed: {label}");
        }
        tauri::WindowEvent::CloseRequested { .. } => { /* existing kill_all */ }
        other => {
            log::debug!("WindowEvent on {label}: {other:?}");
        }
    }
}

Tauri 2 also emits webview lifecycle through RunEvent. Match whatever the locked tauri 2.x crate exposes, in particular any variant whose debug string contains web content process terminated (logged today only at DEBUG inside tauri_runtime_wry).

If the public RunEvent enum does not include content-process death, add a macOS-only observer:

  • WKWebView / NSNotification WebContentProcessDidTerminate if reachable via with_webview.
  • Or raise tauri-plugin-log max level for tauri_runtime_wry to info in production so that line lands in iccery.log.

Minimum viable: set the plugin to capture tauri_runtime_wry at Info and write a dedicated log::error! when we detect window destruction that was not preceded by CloseRequested / ExitRequested.

9.2 Correlate frontend

On visibilitychange / pagehide, logger.warn('Frontend pagehide/visibility', 'WebView').

On webglcontextlost, already logged in Fix 3.

9.3 Support snippet

Document in README:

Log file: ~/Library/Logs/com.gronod.iccery/iccery.log

(Confirm exact app_log_dir on macOS for identifier com.gronod.iccery; prune logic already keeps 5 rotated files.)

9.4 Acceptance

Killing the Web Content process (or a Monterey GPU death) leaves an ERROR/WARN line in iccery.log with timestamp, even when DiagnosticReports has nothing named ICCery.


10. Implementation order

Do not land Fix 3 last. Eager WebGL is the crash; flash cosmetics are Fix 1–2.

Step Fix Risk Why this order
1 Fix 3 (defer + harden WebGL) Medium Stops the crash loop
2 Fix 5 (log termination) Low Need logs to verify 3
3 Fix 1 (hidden window + show) Low Cosmetic + avoids first white frame
4 Fix 2 (WKWebView backing) Medium (objc) Remaining flash
5 Fix 4 (min OS + docs) Low Policy; do after behaviour is stable

Each step should be a separate commit so Monterey testers can bisect.


11. Files to touch

File Changes
src-tauri/tauri.conf.json visible: false, label, backgroundColor, minimumSystemVersion
src-tauri/src/lib.rs register command; RunEvent logging; call macOS paint helper
src-tauri/src/commands.rs show_main_window; optional get_os_info
src-tauri/src/macos_webview.rs new, cfg macos
src-tauri/capabilities/default.json only if we show the window from JS APIs (core:window:allow-show). Not required for the invoke-command approach
src/js/app.js remove eager initGamutViewer; double-rAF show_main_window
src/js/gamut_viewer.js lazy ensure, feature detect, context loss, pause rAF
src/js/state.js call ensureGamutViewer() on navigate to stage 5
src/js/logger.js no API change expected
README.md support matrix + log path + Monterey note
ROADMAP.md optional one-liner under packaging

12. Test plan

12.1 Must pass before merge

  1. macOS 13+ Apple Silicon, release DMG: launch, no white frame, About shows version, Stage 5 WebGL renders, close cleanly.
  2. macOS 13+ Intel: same; if WebGL works, antialias may be on.
  3. macOS 12.7 Intel (the reported box):
    • Launch from Terminal.
    • Window appears once, dark, no multi-flash.
    • Stages 1–4 usable.
    • Open Stage 5: either viewer works or the fallback message appears; app must not quit.
    • iccery.log contains Main window shown after frontend ready.
    • Force-fail WebGL (temporary throw after renderer create) → fallback UI, no exit.
  4. Window hide/show: minimise and restore — no white flashbang (Fix 2).
  5. Hot reload / tauri dev: window still appears; show_main_window is idempotent.
  6. Linux + Windows smoke: visible: false + command still shows the window. No objc code compiled.

12.2 Log expectations

On a Web Content death (if still possible):

ERROR Window destroyed: main
ERROR WebGL context lost   (if GPU path)

On healthy start:

INFO ICCery initialized.
INFO Main window shown after frontend ready

12.3 What not to expect

  • A new Apple Crash Reporter dialog for helper-process death. That is an OS limitation; Fix 5 is the substitute.
  • Perfect WebGL on every Monterey Intel iGPU. Fallback is success.

13. Suggested issue metadata

  • Title: Monterey: WKWebView white-flash loop + silent exit (defer WebGL, dark backing, log Web Content death)
  • Labels: bug, macos, tauri, webgl
  • Blocks: production claim of “macOS 11+ universal binary” until Fix 4 ships
  • Does not block: Windows / Linux releases

14. Reporter template (paste into comments)

ICCery version:
macOS version (sw_vers):
Chip (Intel / Apple Silicon):
Launch method (Finder / Terminal):
Flashes before exit (count):
~/Library/Logs/DiagnosticReports names:
iccery.log excerpt:
Stage reached (if any):
Custom display ICC profile? (yes/no):
**Project:** ICCery **Component:** Tauri v2 window / WKWebView / Stage 5 WebGL **Affects:** macOS 12 (Monterey), especially Intel GPUs **Severity:** High (app unusable on a documented support target) **Type:** Bugfix **Related report:** App window flashes white several times (redraw) then exits. No Crash Reporter dialog for `ICCery.app`. --- ## 1. Summary On macOS Monterey the packaged app opens a native window, paints **white several times**, then disappears. Apple Crash Reporter does not attach to `ICCery.app`. This is the WKWebView **Web Content / GPU helper** dying and being respawned. Each respawn shows WKWebView’s default **white** backing store. After a few cycles the main process tears the window down. That is why there is no ICCery crash report. ICCery currently: - Shows the main window immediately (`visible` defaults to `true`). - Does not set a window or webview background colour. Theme is dark (`--bg-card` ≈ `#1a1a22`, gamut scene `0x0e0e14`). - Creates a `THREE.WebGLRenderer({ antialias: true, alpha: true })` and starts `requestAnimationFrame` from `initGamutViewer()` during `DOMContentLoaded`, **even when Stage 5 is hidden**. - Declares `bundle.macOS.minimumSystemVersion = "10.15"` while README says 11.0+ and the stack is Tauri 2 + WebGL. - Logs frontend messages via `log_frontend_message`, but does **not** handle `RunEvent` webview / web-content termination. This ticket specifies the five fixes required to stop the flash-loop, survive a lost WebGL context, tell the truth about supported OS versions, and leave a log when WebKit dies. --- ## 2. Environment / evidence | Item | Value | |---|---| | App | ICCery `0.8.4` (`com.gronod.iccery`) | | Stack | Tauri 2, wry/WKWebView, vanilla JS, Three.js | | Frontend dist | `src/` (no bundler `base` path issue) | | Window config | `src-tauri/tauri.conf.json` → `app.windows[0]` | | Rust entry | `src-tauri/src/lib.rs` (`iccery_lib`) | | Capabilities | `src-tauri/capabilities/default.json` (`core:default`, `dialog:default`) | | Existing macOS crates | `objc2`, `objc2-app-kit`, `objc2-foundation` | | Logger | `tauri-plugin-log` → `iccery` log dir + stdout + webview; JS `src/js/logger.js` | Repro symptoms: 1. Launch on macOS 12.x. 2. Window appears, flashes white 2–N times. 3. Process exits. 4. `~/Library/Logs/DiagnosticReports` may contain `com.apple.WebKit.WebContent` or `com.apple.WebKit.GPU`, not `ICCery`. 5. Terminal launch (`ICCery.app/Contents/MacOS/ICCery`) may print `web content process terminated`. --- ## 3. Root cause (working model) 1. **White flash.** WKWebView on macOS draws an opaque white layer until HTML/CSS composite. Dark UI makes this obvious. Hidden-then-shown is the standard mitigation; `backgroundColor` / `drawsBackground=NO` removes the remaining flash. See wry#1662 and tauri#1564. 2. **Repeated flashes.** Web Content process crash → WKWebView rebuild → white paint → crash again. Typical trigger on Monterey WebKit (Safari 15 / WebKit 613): WebGL2/WebGL context creation, antialias + high `devicePixelRatio`, continuous rAF on a hidden canvas. 3. **Silent exit.** Helper process death does not produce an `ICCery` crash report. `lib.rs` `RunEvent` match ignores webview destruction / content process death. 4. **Eager WebGL.** `src/js/app.js` calls `safeInit('Gamut Viewer', initGamutViewer)` on every launch. `initGamutViewer()` in `src/js/gamut_viewer.js` constructs `WebGLRenderer` and calls `animate()` immediately. Stage 5 visibility is only checked for the `R` key handler. --- ## 4. Goals After this work: - [ ] Main window is not shown until the first frontend paint signal. - [ ] Native window + WKWebView backing colour match the dark theme (`#1a1a22` / `#0e0e14`). - [ ] WKWebView does not draw its default white background on macOS. - [ ] Three.js / WebGL is created only when Stage 5 is first shown; context loss is handled; missing WebGL does not take down the app. - [ ] `minimumSystemVersion` and README state a support policy that matches Tauri 2 + WebGL. - [ ] Web Content / webview termination is written to the ICCery log file. Out of scope: rewriting the gamut viewer, bumping Tauri major, shipping a bundled WebKit. --- ## 5. Fix 1 — Do not show the window until first paint ### 5.1 Config `src-tauri/tauri.conf.json` → `app.windows[0]`: ```json { "label": "main", "title": "ICCery", "width": 1280, "height": 800, "minWidth": 1100, "minHeight": 700, "visible": false, "backgroundColor": "#1A1A22" } ``` Notes: - `label: "main"` must stay aligned with `capabilities/default.json` `"windows": ["main"]`. - `backgroundColor` uses the theme fallback already in `index.html` (`var(--bg-card, #1a1a22)`). - Confirm against the Tauri 2 schema in use (`https://schema.tauri.app/config/2`). If `backgroundColor` is rejected by the pinned CLI, keep `visible: false` and implement colour only in Fix 2. ### 5.2 Show path (preferred: Rust command) Do **not** rely on `@tauri-apps/api` window permissions unless we add them. The frontend already uses `window.__TAURI__.core.invoke`. Add command `show_main_window` in `src-tauri/src/commands.rs` and register it in `lib.rs` `generate_handler!`. ```rust #[tauri::command] pub fn show_main_window(app: tauri::AppHandle) -> Result<(), String> { if let Some(win) = app.get_webview_window("main") { win.show().map_err(|e| e.to_string())?; let _ = win.set_focus(); log::info!("Main window shown after frontend ready"); } else { log::warn!("show_main_window: window 'main' not found"); } Ok(()) } ``` `lib.rs` already imports `tauri::Manager` (needed for `get_webview_window`). ### 5.3 Frontend signal In `src/js/app.js`, at the **end** of the `DOMContentLoaded` handler (after all `safeInit` calls): ```js requestAnimationFrame(() => { requestAnimationFrame(async () => { try { await invoke('show_main_window'); } catch (e) { console.warn('[ICCery] show_main_window failed:', e); } }); }); ``` Double-rAF waits for layout + first paint of the dark CSS. Fallback timeout of 1500 ms must also call `show_main_window` so a JS exception cannot leave a permanently hidden window. ### 5.4 Safety - If `tauri dev` hot-reloads, `show()` on an already-visible window must be idempotent (it is). - Do not call `show()` from Rust `setup()` — that races the white webview. ### 5.5 Acceptance On Monterey and later, first visible frame is the dark UI (or the configured `#1A1A22`), not a white rectangle, when WebKit is healthy. --- ## 6. Fix 2 — Kill WKWebView’s white backing on macOS `backgroundColor` in config is not enough on macOS: WKWebView still paints white until wry `drawsBackground` / `underPageBackgroundColor` are set. ### 6.1 Where Add a helper module `src-tauri/src/macos_webview.rs` (cfg-gated) and call it from `lib.rs` `.setup()` after the window exists, **and** again from `show_main_window` in case the webview was recreated. The crate already depends on `objc2` / `objc2-app-kit` / `objc2-foundation` for printing. Prefer the same stack. If the pinned wry already exposes `WebviewWindow::set_background_color` / platform `with_webview`, use that first and keep the objc path as fallback. ### 6.2 Required behaviour (macOS only) 1. `NSWindow.setBackgroundColor` → sRGB `26/255, 26/255, 34/255, 1` (`#1A1A22`). 2. WKWebView: set `drawsBackground` to `NO` via KVC (same private key wry uses for transparency), **or** the public wry/Tauri API if available in the locked Tauri 2 patch. 3. If running on macOS 12+: set `underPageBackgroundColor` on the `WKWebView` to the same colour (covers overscroll / unpainted page). Sketch (adapt to the exact objc2 versions already in `Cargo.toml`): ```rust #[cfg(target_os = "macos")] pub fn paint_dark_webview(window: &tauri::WebviewWindow) { // 1) NSWindow background // 2) window.with_webview(|wv| { set drawsBackground / underPageBackgroundColor }) } ``` Do **not** set the window `transparent: true`. That changes hit-testing and titlebar compositing. We only want a dark opaque backing. ### 6.3 Other platforms No-op. Windows/Linux flash is a different compositor; do not change those code paths in this ticket. ### 6.4 Acceptance On Monterey, with Fix 1 applied, a slow first paint must not flash white. A brief dark frame is acceptable. --- ## 7. Fix 3 — Defer and harden WebGL (Stage 5 only) ### 7.1 Stop creating the renderer on launch `src/js/app.js` today: ```js safeInit('Gamut Viewer', initGamutViewer); ``` Change to **lazy** init: - Remove `initGamutViewer()` from the `DOMContentLoaded` batch. - Call it the first time Stage 5 becomes visible (`wizardState.navigateToStage(5)` / stepper click / hash restore). - Keep a module-level `gamutViewerReady` flag so it runs once. Implementation options (pick one, prefer A): **A.** Export `ensureGamutViewer()` from `gamut_viewer.js` and call it from `state.js` when `targetStep === 5` and the stage element is not `.hidden`. **B.** Observe `#stage-5` with `IntersectionObserver` / `MutationObserver` on the `hidden` class. Do not start WebGL while Stage 5 is in the DOM but `display:none` / `.hidden`. Hidden containers report `clientWidth === 0`; the code already falls back to 500×400 and will still create a GPU context. ### 7.2 Feature detect before `new WebGLRenderer` At the top of renderer creation in `initGamutViewer()`: ```js function webglAvailable() { try { const c = document.createElement('canvas'); return !!(c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl')); } catch { return false; } } ``` If false: leave a visible message in `#gamutViewerContainer` (“3D gamut viewer requires WebGL; the rest of ICCery still works”), `logger.warn(...)`, `return`. Never throw into `safeInit` as a fatal path. ### 7.3 Safer renderer flags on old WebKit Monterey Intel: drop work that blows the GPU process. ```js const lowPower = isLikelyConstrainedGpu(); // see below renderer = new THREE.WebGLRenderer({ antialias: !lowPower, alpha: false, // opaque; matches scene.background powerPreference: lowPower ? 'low-power' : 'default', failIfMajorPerformanceCaveat: false, }); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, lowPower ? 1 : 2)); ``` `isLikelyConstrainedGpu()` heuristic (log the result): - `navigator.platform` contains `MacIntel` (Rosetta or Intel). - Optional: parse `sw_vers` via a tiny `get_os_info` command if we add one in Fix 4; otherwise UA / `navigator.userAgentData` is enough. `alpha: true` on a dark opaque scene is unnecessary and has caused compositor flashes with CSS overlays. ### 7.4 Context-loss handlers After `appendChild(renderer.domElement)`: ```js renderer.domElement.addEventListener('webglcontextlost', (e) => { e.preventDefault(); logger.error('WebGL context lost', 'GamutViewer'); stopAnimate(); }); renderer.domElement.addEventListener('webglcontextrestored', () => { logger.warn('WebGL context restored — rebuilding viewer', 'GamutViewer'); // dispose + re-init, or set a "Reload 3D view" button }); ``` `animate()` must check a `running` flag so a lost context does not spin rAF forever. ```js function animate() { if (!animationRunning) return; requestAnimationFrame(animate); // ... } ``` On Stage 5 hide, set `animationRunning = false`. On show, restart. Continuous rAF on a hidden WebGL canvas is a known Monterey killer. ### 7.5 Dispose on failure If `new THREE.WebGLRenderer` throws (some WebKit builds throw instead of returning null): - Catch (already wrapped, but the message is only `console.warn`). - `logger.error` with stack. - Do not leave a half-attached canvas. ### 7.6 Acceptance - Launching ICCery on Monterey does **not** create a WebGL context until the user opens Stage 5. - If WebGL is missing or the context is lost, Stages 1–4 and settings remain usable. - Opening Stage 5 twice does not leak a second renderer. --- ## 8. Fix 4 — Honest minimum OS + docs ### 8.1 Bundle plist `src-tauri/tauri.conf.json`: ```json "macOS": { "minimumSystemVersion": "12.0", ... } ``` Rationale: - Current value `10.15` is false for Tauri 2 + this UI. - README already claims 11.0+. - WebGL + WKWebView quality below 12 is not something we will test. - Setting `12.0` makes Gatekeeper / Installer reject 10.15/11 rather than flash-loop. If product still wants Big Sur installs: set `11.0` and mark WebGL as unsupported there. Do **not** keep `10.15`. ### 8.2 README / ROADMAP Replace the macOS support bullet with an explicit matrix: | macOS | Status | |---|---| | 13+ (Ventura and newer), Apple Silicon | Supported | | 13+, Intel | Supported | | 12.7.x Monterey, Apple Silicon | Supported, WebGL best-effort | | 12.0–12.6 Monterey, Intel | Best-effort; WebGL deferred; known WKWebView GPU process crashes | | 11 Big Sur | Not supported (installer should refuse once min version is 12.0) | | 10.15 Catalina | Not supported | Add a short “macOS troubleshooting” note: - Launch from Terminal to see `web content process terminated`. - Check `~/Library/Logs/DiagnosticReports` for `WebKit.WebContent` / `WebKit.GPU`. - ICCery log file location (already used by `tauri-plugin-log`): app log dir / `iccery.log`. - Custom ColorSync display profiles can crash toolkit UIs on Monterey; Safe Mode / default display profile is a valid support question. ### 8.3 Optional runtime banner `get_app_info` (already invoked from About) should include `os` + `arch`. If macOS major < 13 and Intel, show a one-time notice in the existing wizard notification strip: 3D gamut view may be unavailable; profiling stages still work. ### 8.4 Acceptance A fresh DMG on 10.15/11 does not install (or shows a clear OS requirement). Docs no longer promise Catalina. --- ## 9. Fix 5 — Log Web Content termination Users cannot file useful bugs when Crash Reporter is silent. ICCery already has a file logger. ### 9.1 `RunEvent` in `lib.rs` Extend the existing `.run(|app_handle, event| { match event { ... }})`. Handle at least: ```rust tauri::RunEvent::WindowEvent { label, event, .. } => { match event { tauri::WindowEvent::Destroyed => { log::error!("Window destroyed: {label}"); } tauri::WindowEvent::CloseRequested { .. } => { /* existing kill_all */ } other => { log::debug!("WindowEvent on {label}: {other:?}"); } } } ``` Tauri 2 also emits webview lifecycle through `RunEvent`. Match whatever the locked `tauri` 2.x crate exposes, in particular any variant whose debug string contains `web content process terminated` (logged today only at `DEBUG` inside `tauri_runtime_wry`). If the public `RunEvent` enum does not include content-process death, add a macOS-only observer: - `WKWebView` / `NSNotification` `WebContentProcessDidTerminate` if reachable via `with_webview`. - Or raise `tauri-plugin-log` max level for `tauri_runtime_wry` to `info` in production so that line lands in `iccery.log`. Minimum viable: set the plugin to capture `tauri_runtime_wry` at `Info` and write a dedicated `log::error!` when we detect window destruction that was **not** preceded by `CloseRequested` / `ExitRequested`. ### 9.2 Correlate frontend On `visibilitychange` / `pagehide`, `logger.warn('Frontend pagehide/visibility', 'WebView')`. On `webglcontextlost`, already logged in Fix 3. ### 9.3 Support snippet Document in README: ```text Log file: ~/Library/Logs/com.gronod.iccery/iccery.log ``` (Confirm exact `app_log_dir` on macOS for identifier `com.gronod.iccery`; prune logic already keeps 5 rotated files.) ### 9.4 Acceptance Killing the Web Content process (or a Monterey GPU death) leaves an `ERROR`/`WARN` line in `iccery.log` with timestamp, even when DiagnosticReports has nothing named ICCery. --- ## 10. Implementation order Do not land Fix 3 last. Eager WebGL is the crash; flash cosmetics are Fix 1–2. | Step | Fix | Risk | Why this order | |---|---|---|---| | 1 | Fix 3 (defer + harden WebGL) | Medium | Stops the crash loop | | 2 | Fix 5 (log termination) | Low | Need logs to verify 3 | | 3 | Fix 1 (hidden window + show) | Low | Cosmetic + avoids first white frame | | 4 | Fix 2 (WKWebView backing) | Medium (objc) | Remaining flash | | 5 | Fix 4 (min OS + docs) | Low | Policy; do after behaviour is stable | Each step should be a separate commit so Monterey testers can bisect. --- ## 11. Files to touch | File | Changes | |---|---| | `src-tauri/tauri.conf.json` | `visible: false`, `label`, `backgroundColor`, `minimumSystemVersion` | | `src-tauri/src/lib.rs` | register command; `RunEvent` logging; call macOS paint helper | | `src-tauri/src/commands.rs` | `show_main_window`; optional `get_os_info` | | `src-tauri/src/macos_webview.rs` | **new**, cfg macos | | `src-tauri/capabilities/default.json` | only if we show the window from JS APIs (`core:window:allow-show`). Not required for the invoke-command approach | | `src/js/app.js` | remove eager `initGamutViewer`; double-rAF `show_main_window` | | `src/js/gamut_viewer.js` | lazy ensure, feature detect, context loss, pause rAF | | `src/js/state.js` | call `ensureGamutViewer()` on navigate to stage 5 | | `src/js/logger.js` | no API change expected | | `README.md` | support matrix + log path + Monterey note | | `ROADMAP.md` | optional one-liner under packaging | --- ## 12. Test plan ### 12.1 Must pass before merge 1. **macOS 13+ Apple Silicon, release DMG:** launch, no white frame, About shows version, Stage 5 WebGL renders, close cleanly. 2. **macOS 13+ Intel:** same; if WebGL works, antialias may be on. 3. **macOS 12.7 Intel (the reported box):** - Launch from Terminal. - Window appears once, dark, no multi-flash. - Stages 1–4 usable. - Open Stage 5: either viewer works or the fallback message appears; app must not quit. - `iccery.log` contains `Main window shown after frontend ready`. - Force-fail WebGL (temporary `throw` after renderer create) → fallback UI, no exit. 4. **Window hide/show:** minimise and restore — no white flashbang (Fix 2). 5. **Hot reload / `tauri dev`:** window still appears; `show_main_window` is idempotent. 6. **Linux + Windows smoke:** `visible: false` + command still shows the window. No objc code compiled. ### 12.2 Log expectations On a Web Content death (if still possible): ``` ERROR Window destroyed: main ERROR WebGL context lost (if GPU path) ``` On healthy start: ``` INFO ICCery initialized. INFO Main window shown after frontend ready ``` ### 12.3 What not to expect - A new Apple Crash Reporter dialog for helper-process death. That is an OS limitation; Fix 5 is the substitute. - Perfect WebGL on every Monterey Intel iGPU. Fallback is success. --- ## 13. Suggested issue metadata - **Title:** Monterey: WKWebView white-flash loop + silent exit (defer WebGL, dark backing, log Web Content death) - **Labels:** `bug`, `macos`, `tauri`, `webgl` - **Blocks:** production claim of “macOS 11+ universal binary” until Fix 4 ships - **Does not block:** Windows / Linux releases --- ## 14. Reporter template (paste into comments) ``` ICCery version: macOS version (sw_vers): Chip (Intel / Apple Silicon): Launch method (Finder / Terminal): Flashes before exit (count): ~/Library/Logs/DiagnosticReports names: iccery.log excerpt: Stage reached (if any): Custom display ICC profile? (yes/no): ```
gronod added the Kind/Bug
Reviewed
Confirmed
1
Priority
High
2
labels 2026-09-07 16:16:05 +01:00
Sign in to join this conversation.