fix: recover preview from WebGL context loss on Linux/Wayland#19
Conversation
On Manjaro / CachyOS (Linux/Wayland with Mesa/EGL) the preview Pixi app can lose its WebGL context during heavy GPU usage from the exporter — most likely the `VideoEncoder`'s `prefer-hardware` path or the Linux-only CPU readback in `frameRenderer.ts.readbackVideoCanvas`. The wallpaper is rendered to a 2D canvas (`frameRenderer.ts:212`, "Background renders separately, not in PixiJS") and survives the context loss, but the video sprite lives inside the lost Pixi GL context and never reappears. That is the "only background remains" symptom in issue #8. Fix: attach a `webglcontextlost` listener to the preview Pixi canvas. `preventDefault()` opts in to Chromium's restoration attempt, and bumping a `pixiGeneration` state tears the broken app down via the existing `useEffect` cleanup so the next mount rebuilds it from scratch with a fresh WebGL context. The recovery is logged for diagnostics ("[VideoPlayback] WebGL context lost, recreating Pixi app"), which will now reach the terminal thanks to the console-message forwarding added to `electron/main.ts` in PR #11. Extracted the handler into `createWebGLContextLostHandler` for unit testability and added a small focused test that covers the two invariants the bug hinges on: the event's `preventDefault` is called and the regenerate callback fires. Tested: - npx tsc --noEmit - npm run lint - npm test (244 passed, +3 new) - npm run build-vite
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 24 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
ChangesWebGL Context Loss Recovery
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Add ROADMAP.md at repo root for top-level discoverability. - Stability queue pulled from open issues / PRs on getopenscreen/openscreen (#8, #21, #22, #19, #18, #24). - AI direction framed as opt-in / off by default, with per-feature tags. - Extend .github/workflows/discord.yaml with a roadmap-notify job that posts an embed to the #🗺️・roadmap Discord channel whenever a PR changes ROADMAP.md (or docs/roadmap.md). Mirrors the existing PR -> #pr-reviews sync pattern. Activated by setting the DISCORD_ROADMAP_WEBHOOK_URL repo secret; silently skipped otherwise so CI never breaks.
- Add ROADMAP.md at repo root for top-level discoverability. - Stability queue pulled from open issues / PRs on getopenscreen/openscreen (#8, #21, #22, #19, #18, #24). - AI direction framed as opt-in / off by default, with per-feature tags. - Link the new roadmap from the README Community section. - Extend .github/workflows/discord.yaml with a roadmap-notify job that posts an embed to the #🗺️・roadmap Discord channel whenever a PR changes ROADMAP.md (or docs/roadmap.md). Mirrors the existing PR -> #pr-reviews sync pattern. Activated by setting the DISCORD_ROADMAP_WEBHOOK_URL repo secret; silently skipped otherwise so CI never breaks.
- Add ROADMAP.md at repo root for top-level discoverability. - Stability queue pulled from open issues / PRs on getopenscreen/openscreen (#8, #21, #22, #19, #18, #24). - AI direction framed as opt-in / off by default, with per-feature tags. - Link the new roadmap from the README Community section. - Extend .github/workflows/discord.yaml with a roadmap-notify job that posts an embed to the #🗺️・roadmap Discord channel whenever a PR changes ROADMAP.md (or docs/roadmap.md). Mirrors the existing PR -> #pr-reviews sync pattern. Activated by setting the DISCORD_ROADMAP_WEBHOOK_URL repo secret; silently skipped otherwise so CI never breaks.
Bugs from the spine test pass: #4 - clips display as 60s instead of the real video length. Root cause: insertClipAt falls back to PLACEHOLDER_DURATION_SEC (60s) when asset.durationSec is undefined, and the loadedmetadata auto-correct (handleLoadedMetadata) didn't fire reliably (in the browser-shim the preview path is unreliable; in the real Electron window the preview <video> remounts only when the activeSource swaps, so a freshly-added asset's metadata never reaches the auto-correct). Fix: probe the file directly via a throwaway <video> (src/lib/ai-edition/timeline/duration.ts, 5 unit tests) and insert the clip at the real duration synchronously. Persist the probed duration back onto the asset so subsequent inserts don't re-probe. Falls back to PLACEHOLDER on probe failure. #17 - dragging a skip's left/right chevron appeared to move the whole skip instead of just one edge. Root cause: clipSegments() was called once with the source skipRanges, then the cut segment was patched with dragPreview.startSec/endSec - but the surrounding keep segments weren't regenerated, so their flex widths didn't shrink to make room and the cut's visual position drifted. Fix: build a virtual skipRanges with the dragPreview'd bounds and re-run clipSegments() end-to-end so keep+cut stay consistent. #18 - clicking the trash button on a skip didn't remove it. Root cause: startClipReorder calls setPointerCapture on the clip block, so the subsequent pointerup is captured on the block (not the trash button) and the click event fires on the block (not the button). Fix: stop pointerdown propagation on the trash button so the clip block never captures the pointer. #5/#19 - clips were overlapping by 1px (the join-border overlap from T09). User: this is NOT wanted for clips - only for skip strips inside a clip. Fix: remove the JS left/width shift on hasJoinedPrev. CSS .joinedPrev/.joinedNext still zero the adjacent border-radius + border-width so corners blend. #6 - the reorder marker wasn't visible. Root cause: z-index 11, but the moving clip has z-index 20, and the marker's soft box-shadow was only 8px so it disappeared against dark backgrounds. Fix: bump width to 4px, z-index to 30 (above the moving clip), and stronger box-shadow. Verified: tsc clean, biome clean, 412 tests pass.
…he correct boundary Two follow-up bugs from the spine test pass: #5/#19 — clips were STILL merging visually when adjacent. I had already removed the JS left/width shift on hasJoinedPrev in f9d62e1, but the .joinedPrev / .joinedNext CSS classes were still in the rendered classList, and the CSS for them was still zeroing the border-radius + border-width on the joined side. User feedback (round 2): there is NO reason to merge clips visually, ever — clips keep their own border + border-radius on every side, even when adjacent (and even more so when there are gaps between them, which the user pointed out can happen). Dropped the hasJoinedPrev / hasJoinedNext calculations, the .joinedPrev / .joinedNext classList additions, the CLIP_JOIN_THRESHOLD_PX constant, and the CSS rules. Only the keep-vs-cut segment rendering (which IS the right place to overlap freely) keeps its proportional layout. #6 — the reorder marker was at the wrong boundary. My port kept axcut's exact formula: boundarySec = remainingClips[insertIndex]. timelineStartSec. In axcut that's correct because clips can have gaps, so the moving clip's new START = the next non-moving clip's original START. In our model clips are resequenced to be contiguous, so the moving clip's new START = the END of the preceding clip (not the START of the next). With clips [A(0-10), B(15-25), C(30-40)] and B moving to slot 1, axcut puts the marker at 30 (C.start); ours should put it at 10 (A.end == new B.start). Fix: use remainingClips[insertIndex-1].timelineEndSec as the boundary. Also simplifies — insertIndex >= remaining.length no longer needs a special-case since remaining.length-1's timelineEndSec equals virtualDurationSec. Verified: tsc clean, biome clean, 412 tests pass.
Bugs from the spine test pass: #4 - clips display as 60s instead of the real video length. Root cause: insertClipAt falls back to PLACEHOLDER_DURATION_SEC (60s) when asset.durationSec is undefined, and the loadedmetadata auto-correct (handleLoadedMetadata) didn't fire reliably (in the browser-shim the preview path is unreliable; in the real Electron window the preview <video> remounts only when the activeSource swaps, so a freshly-added asset's metadata never reaches the auto-correct). Fix: probe the file directly via a throwaway <video> (src/lib/ai-edition/timeline/duration.ts, 5 unit tests) and insert the clip at the real duration synchronously. Persist the probed duration back onto the asset so subsequent inserts don't re-probe. Falls back to PLACEHOLDER on probe failure. #17 - dragging a skip's left/right chevron appeared to move the whole skip instead of just one edge. Root cause: clipSegments() was called once with the source skipRanges, then the cut segment was patched with dragPreview.startSec/endSec - but the surrounding keep segments weren't regenerated, so their flex widths didn't shrink to make room and the cut's visual position drifted. Fix: build a virtual skipRanges with the dragPreview'd bounds and re-run clipSegments() end-to-end so keep+cut stay consistent. #18 - clicking the trash button on a skip didn't remove it. Root cause: startClipReorder calls setPointerCapture on the clip block, so the subsequent pointerup is captured on the block (not the trash button) and the click event fires on the block (not the button). Fix: stop pointerdown propagation on the trash button so the clip block never captures the pointer. #5/#19 - clips were overlapping by 1px (the join-border overlap from T09). User: this is NOT wanted for clips - only for skip strips inside a clip. Fix: remove the JS left/width shift on hasJoinedPrev. CSS .joinedPrev/.joinedNext still zero the adjacent border-radius + border-width so corners blend. #6 - the reorder marker wasn't visible. Root cause: z-index 11, but the moving clip has z-index 20, and the marker's soft box-shadow was only 8px so it disappeared against dark backgrounds. Fix: bump width to 4px, z-index to 30 (above the moving clip), and stronger box-shadow. Verified: tsc clean, biome clean, 412 tests pass.
…he correct boundary Two follow-up bugs from the spine test pass: #5/#19 — clips were STILL merging visually when adjacent. I had already removed the JS left/width shift on hasJoinedPrev in f9d62e1, but the .joinedPrev / .joinedNext CSS classes were still in the rendered classList, and the CSS for them was still zeroing the border-radius + border-width on the joined side. User feedback (round 2): there is NO reason to merge clips visually, ever — clips keep their own border + border-radius on every side, even when adjacent (and even more so when there are gaps between them, which the user pointed out can happen). Dropped the hasJoinedPrev / hasJoinedNext calculations, the .joinedPrev / .joinedNext classList additions, the CLIP_JOIN_THRESHOLD_PX constant, and the CSS rules. Only the keep-vs-cut segment rendering (which IS the right place to overlap freely) keeps its proportional layout. #6 — the reorder marker was at the wrong boundary. My port kept axcut's exact formula: boundarySec = remainingClips[insertIndex]. timelineStartSec. In axcut that's correct because clips can have gaps, so the moving clip's new START = the next non-moving clip's original START. In our model clips are resequenced to be contiguous, so the moving clip's new START = the END of the preceding clip (not the START of the next). With clips [A(0-10), B(15-25), C(30-40)] and B moving to slot 1, axcut puts the marker at 30 (C.start); ours should put it at 10 (A.end == new B.start). Fix: use remainingClips[insertIndex-1].timelineEndSec as the boundary. Also simplifies — insertIndex >= remaining.length no longer needs a special-case since remaining.length-1's timelineEndSec equals virtualDurationSec. Verified: tsc clean, biome clean, 412 tests pass.
Adds an in-app File/Edit/View menu bar to the editor titlebar and auto-hides the native OS menu bar on Windows/Linux (electron/windows.ts), for a consistent custom-styled menu across platforms. Maintainer cleanup on top of the original PR getopenscreen#87: - Dropped the WebGL context-loss fix that was duplicated from getopenscreen#19 (this branch had merged getopenscreen#19's branch in); that fix now lands solely via getopenscreen#19. - Dropped the unrelated package.json `allowScripts` block and the @electron/windows-sign / postject / cross-dirname lockfile additions. - Extracted the menu into a testable EditorMenuBar component built from a pure model, with unit + interaction tests (EditorMenuBar.test.tsx). - Removed the unnecessary `as never` i18n casts and dead `|| "…"` fallbacks; the keys already exist in common/dialogs, so rawT() is called directly (matches the existing showInFolder usage). - Made shortcut hints platform-aware (⌘ on macOS, Ctrl elsewhere), matching the accelerators wired in the native menu (electron/main.ts) and the editor keydown handler. - Routed Quit through a new `app-quit` IPC (app.quit()) instead of window.close(), matching the native menu's role:"quit". Co-authored-by: Gede Cahya <49280905+gede-cahya@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
This is the canonical fix for the preview WebGL context-loss recovery. #87 had merged this branch in and shipped the same |
Adds an in-app File/Edit/View menu bar to the editor titlebar and auto-hides the native OS menu bar on Windows/Linux (electron/windows.ts), for a consistent custom-styled menu across platforms. Maintainer cleanup on top of the original PR #87: - Dropped the WebGL context-loss fix that was duplicated from #19 (this branch had merged #19's branch in); that fix now lands solely via #19. - Dropped the unrelated package.json `allowScripts` block and the @electron/windows-sign / postject / cross-dirname lockfile additions. - Extracted the menu into a testable EditorMenuBar component built from a pure model, with unit + interaction tests (EditorMenuBar.test.tsx). - Removed the unnecessary `as never` i18n casts and dead `|| "…"` fallbacks; the keys already exist in common/dialogs, so rawT() is called directly (matches the existing showInFolder usage). - Made shortcut hints platform-aware (⌘ on macOS, Ctrl elsewhere), matching the accelerators wired in the native menu (electron/main.ts) and the editor keydown handler. - Routed Quit through a new `app-quit` IPC (app.quit()) instead of window.close(), matching the native menu's role:"quit". Co-authored-by: Gede Cahya <49280905+gede-cahya@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
webglcontextlostlistener to the preview Pixi canvas inVideoPlayback.tsx.event.preventDefault()(to opt in to Chromium's restoration attempt) and bumps apixiGenerationstate so the existinguseEffectcleanup tears the broken app down. The next mount rebuilds it from scratch with a fresh WebGL context.frameRenderer.ts:212) survives the context loss, so this fix makes the "only background remains" symptom in issue [Bug]: Video disappears from editor after exporting (only background remains) #8 self-healing.createWebGLContextLostHandlerfor unit testability and addedVideoPlayback.test.tswith three focused tests (preventDefault is called, regenerate fires, generation is logged).webglcontextlostandwebglcontextrestoredevents so the recovery is visible in the terminal — combined with the console-message forwarding from PR [codex] Add export preview diagnostics #11, this gives a complete trace of any future loss/restoration on Linux.Why
The bug only reproduces on Manjaro / CachyOS (Arch Linux + KDE Wayland, Mesa/EGL) and only after MP4 export —
VideoEncoderuses theprefer-hardwarepath on Linux (videoExporter.ts:681), and the FrameRenderer also takes a Linux-specific CPU-readback branch (videoExporter.ts:397andframeRenderer.ts:914).grep webglcontextlostagainst the codebase before this PR returned 0 hits: the preview never registered a recovery listener, so once the GL context died the video sprite stayed gone. The wallpaper kept rendering becauseframeRenderer.ts:212deliberately composites it on a plain 2D canvas outside of PixiJS, which is why the symptom is "wallpaper stays, video disappears" instead of a fully black preview.Trade-offs
prefer-softwareon Linux. Software H.264 is 3–10× slower on this workload and would have been a real UX regression for Manjaro users for a problem we had not yet confirmed. This fix keeps the encoder preferences untouched and makes the bug self-heal at the renderer instead.Test plan
npx tsc --noEmit— cleannpm run lint— cleannpm test— 244 passed (3 new inVideoPlayback.test.ts)npm run build-vite— cleanFollow-ups
["prefer-software", "prefer-hardware"](the same order Windows uses atvideoExporter.ts:679).webglcontextlostlistener to the FrameRenderer's Pixi canvas inframeRenderer.tsas well. It only matters during export, where a lost context currently shows as a single green/empty frame, so it is much lower priority.Related
Summary by CodeRabbit
Bug Fixes
Tests