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.
~/Library/Logs/DiagnosticReports may contain com.apple.WebKit.WebContent or com.apple.WebKit.GPU, not ICCery.
Terminal launch (ICCery.app/Contents/MacOS/ICCery) may print web content process terminated.
3. Root cause (working model)
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.
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.
Silent exit. Helper process death does not produce an ICCery crash report. lib.rsRunEvent match ignores webview destruction / content process death.
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
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.rsgenerate_handler!.
#[tauri::command]pubfnshow_main_window(app: tauri::AppHandle)-> Result<(),String>{ifletSome(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):
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.
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.
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):
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():
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.
constlowPower=isLikelyConstrainedGpu();// see below
renderer=newTHREE.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.
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,..}=>{matchevent{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 / NSNotificationWebContentProcessDidTerminate 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').
(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.
**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):
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
visibledefaults totrue).--bg-card≈#1a1a22, gamut scene0x0e0e14).THREE.WebGLRenderer({ antialias: true, alpha: true })and startsrequestAnimationFramefrominitGamutViewer()duringDOMContentLoaded, even when Stage 5 is hidden.bundle.macOS.minimumSystemVersion = "10.15"while README says 11.0+ and the stack is Tauri 2 + WebGL.log_frontend_message, but does not handleRunEventwebview / 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
0.8.4(com.gronod.iccery)src/(no bundlerbasepath issue)src-tauri/tauri.conf.json→app.windows[0]src-tauri/src/lib.rs(iccery_lib)src-tauri/capabilities/default.json(core:default,dialog:default)objc2,objc2-app-kit,objc2-foundationtauri-plugin-log→iccerylog dir + stdout + webview; JSsrc/js/logger.jsRepro symptoms:
~/Library/Logs/DiagnosticReportsmay containcom.apple.WebKit.WebContentorcom.apple.WebKit.GPU, notICCery.ICCery.app/Contents/MacOS/ICCery) may printweb content process terminated.3. Root cause (working model)
backgroundColor/drawsBackground=NOremoves the remaining flash. See wry#1662 and tauri#1564.devicePixelRatio, continuous rAF on a hidden canvas.ICCerycrash report.lib.rsRunEventmatch ignores webview destruction / content process death.src/js/app.jscallssafeInit('Gamut Viewer', initGamutViewer)on every launch.initGamutViewer()insrc/js/gamut_viewer.jsconstructsWebGLRendererand callsanimate()immediately. Stage 5 visibility is only checked for theRkey handler.4. Goals
After this work:
#1a1a22/#0e0e14).minimumSystemVersionand README state a support policy that matches Tauri 2 + WebGL.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]:Notes:
label: "main"must stay aligned withcapabilities/default.json"windows": ["main"].backgroundColoruses the theme fallback already inindex.html(var(--bg-card, #1a1a22)).https://schema.tauri.app/config/2). IfbackgroundColoris rejected by the pinned CLI, keepvisible: falseand implement colour only in Fix 2.5.2 Show path (preferred: Rust command)
Do not rely on
@tauri-apps/apiwindow permissions unless we add them. The frontend already useswindow.__TAURI__.core.invoke.Add command
show_main_windowinsrc-tauri/src/commands.rsand register it inlib.rsgenerate_handler!.lib.rsalready importstauri::Manager(needed forget_webview_window).5.3 Frontend signal
In
src/js/app.js, at the end of theDOMContentLoadedhandler (after allsafeInitcalls):Double-rAF waits for layout + first paint of the dark CSS. Fallback timeout of 1500 ms must also call
show_main_windowso a JS exception cannot leave a permanently hidden window.5.4 Safety
tauri devhot-reloads,show()on an already-visible window must be idempotent (it is).show()from Rustsetup()— 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
backgroundColorin config is not enough on macOS: WKWebView still paints white until wrydrawsBackground/underPageBackgroundColorare set.6.1 Where
Add a helper module
src-tauri/src/macos_webview.rs(cfg-gated) and call it fromlib.rs.setup()after the window exists, and again fromshow_main_windowin case the webview was recreated.The crate already depends on
objc2/objc2-app-kit/objc2-foundationfor printing. Prefer the same stack. If the pinned wry already exposesWebviewWindow::set_background_color/ platformwith_webview, use that first and keep the objc path as fallback.6.2 Required behaviour (macOS only)
NSWindow.setBackgroundColor→ sRGB26/255, 26/255, 34/255, 1(#1A1A22).drawsBackgroundtoNOvia KVC (same private key wry uses for transparency), or the public wry/Tauri API if available in the locked Tauri 2 patch.underPageBackgroundColoron theWKWebViewto the same colour (covers overscroll / unpainted page).Sketch (adapt to the exact objc2 versions already in
Cargo.toml):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.jstoday:Change to lazy init:
initGamutViewer()from theDOMContentLoadedbatch.wizardState.navigateToStage(5)/ stepper click / hash restore).gamutViewerReadyflag so it runs once.Implementation options (pick one, prefer A):
A. Export
ensureGamutViewer()fromgamut_viewer.jsand call it fromstate.jswhentargetStep === 5and the stage element is not.hidden.B. Observe
#stage-5withIntersectionObserver/MutationObserveron thehiddenclass.Do not start WebGL while Stage 5 is in the DOM but
display:none/.hidden. Hidden containers reportclientWidth === 0; the code already falls back to 500×400 and will still create a GPU context.7.2 Feature detect before
new WebGLRendererAt the top of renderer creation in
initGamutViewer():If false: leave a visible message in
#gamutViewerContainer(“3D gamut viewer requires WebGL; the rest of ICCery still works”),logger.warn(...),return. Never throw intosafeInitas a fatal path.7.3 Safer renderer flags on old WebKit
Monterey Intel: drop work that blows the GPU process.
isLikelyConstrainedGpu()heuristic (log the result):navigator.platformcontainsMacIntel(Rosetta or Intel).sw_versvia a tinyget_os_infocommand if we add one in Fix 4; otherwise UA /navigator.userAgentDatais enough.alpha: trueon a dark opaque scene is unnecessary and has caused compositor flashes with CSS overlays.7.4 Context-loss handlers
After
appendChild(renderer.domElement):animate()must check arunningflag so a lost context does not spin rAF forever.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.WebGLRendererthrows (some WebKit builds throw instead of returning null):console.warn).logger.errorwith stack.7.6 Acceptance
8. Fix 4 — Honest minimum OS + docs
8.1 Bundle plist
src-tauri/tauri.conf.json:Rationale:
10.15is false for Tauri 2 + this UI.12.0makes Gatekeeper / Installer reject 10.15/11 rather than flash-loop.If product still wants Big Sur installs: set
11.0and mark WebGL as unsupported there. Do not keep10.15.8.2 README / ROADMAP
Replace the macOS support bullet with an explicit matrix:
Add a short “macOS troubleshooting” note:
web content process terminated.~/Library/Logs/DiagnosticReportsforWebKit.WebContent/WebKit.GPU.tauri-plugin-log): app log dir /iccery.log.8.3 Optional runtime banner
get_app_info(already invoked from About) should includeos+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
RunEventinlib.rsExtend the existing
.run(|app_handle, event| { match event { ... }}).Handle at least:
Tauri 2 also emits webview lifecycle through
RunEvent. Match whatever the lockedtauri2.x crate exposes, in particular any variant whose debug string containsweb content process terminated(logged today only atDEBUGinsidetauri_runtime_wry).If the public
RunEventenum does not include content-process death, add a macOS-only observer:WKWebView/NSNotificationWebContentProcessDidTerminateif reachable viawith_webview.tauri-plugin-logmax level fortauri_runtime_wrytoinfoin production so that line lands iniccery.log.Minimum viable: set the plugin to capture
tauri_runtime_wryatInfoand write a dedicatedlog::error!when we detect window destruction that was not preceded byCloseRequested/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:
(Confirm exact
app_log_diron macOS for identifiercom.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/WARNline iniccery.logwith 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.
Each step should be a separate commit so Monterey testers can bisect.
11. Files to touch
src-tauri/tauri.conf.jsonvisible: false,label,backgroundColor,minimumSystemVersionsrc-tauri/src/lib.rsRunEventlogging; call macOS paint helpersrc-tauri/src/commands.rsshow_main_window; optionalget_os_infosrc-tauri/src/macos_webview.rssrc-tauri/capabilities/default.jsoncore:window:allow-show). Not required for the invoke-command approachsrc/js/app.jsinitGamutViewer; double-rAFshow_main_windowsrc/js/gamut_viewer.jssrc/js/state.jsensureGamutViewer()on navigate to stage 5src/js/logger.jsREADME.mdROADMAP.md12. Test plan
12.1 Must pass before merge
iccery.logcontainsMain window shown after frontend ready.throwafter renderer create) → fallback UI, no exit.tauri dev: window still appears;show_main_windowis idempotent.visible: false+ command still shows the window. No objc code compiled.12.2 Log expectations
On a Web Content death (if still possible):
On healthy start:
12.3 What not to expect
13. Suggested issue metadata
bug,macos,tauri,webgl14. Reporter template (paste into comments)