Skip to content

fix(gl): repaint thread vs AppKit cursor hide race (#1947) - #1973

Merged
dfattal merged 2 commits into
mainfrom
fix/gl-repaint-appkit-1947
Oct 12, 2026
Merged

dfattal merged 2 commits into
mainfrom
fix/gl-repaint-appkit-1947

Conversation

@dfattal

@dfattal dfattal commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1947.

Root cause

The crash is not an AppKit race. It is one CGL context in use on two threads at once.

  • cube_handle_gl_macos renders with [g_glView openGLContext] and passes g_glView through XR_DXR_cocoa_window_binding.
  • For an NSOpenGLView, comp_gl_window_macos_setup_external adopted the view's existing openGLContext. That is the same context the app renders with, so the compositor had no private context.
  • The Decouple weave rate from app render rate: re-weave the last atlas at panel rate with fresh eye positions #868 repaint thread makes that context current and replays the weave on it (gl_crop_and_process_dp → glBlitFramebuffer). It is serialised against the app only by c->mutex and app_frame_in_progress, and both cover xrEndFrame alone. The app's own GL (RenderScene, between xrBeginFrame and xrEndFrame) runs outside both.
  • The result is that one GLDContextRec is driven from two threads. The crash reports show the faulting repaint thread in GLRResourceList::addResource while, at the same moment, the main thread is in RenderScene → GLDContextRec::flushContext. When the main thread loses the race instead, it aborts with Metal validation in glDrawElements.

Hiding the OS cursor was only a timing trigger: it shifts the app's frame cadence just enough for the repaint gate to open. With DXR_WEAVE_REPAINT_FORCE=1 (a repaint every refresh), the old code crashes 10/10 launches with the cursor extension OFF.

The adapter in displayxr-common is not involved: NSCursor hide/unhide already runs on the main thread. So there is no displayxr-common PR.

Fix

  • No repaint on an app-owned context. No lock the runtime takes can cover the app's own GL calls, so the GL compositor does not start the repaint thread when its context is the app's own. comp_gl_window_macos_context_is_app_owned() reports that case, and a one-time WARN says so: #868/#1947: weave repaint OFF — …. The app's frames weave and present exactly as before. The only loss is the between-frames replay for this one configuration (an app that binds an NSOpenGLView).
  • Private contexts keep the repaint. These are the own-window, plain-NSView and offscreen-texture paths. The macOS repaint arm now also takes CGLLockContext, the lock AppKit takes around -[NSOpenGLContext update] on the main thread.
  • cube_handle_gl_macos: depth cursor default-on again. The feat(cursor): the runtime draws the depth cursor — Vulkan + GL (ADR-046 Amendment 2 §B) #1946 opt-in is undone; DISPLAYXR_CURSOR_DEPTH=0 opts out, the same as the Metal and Vulkan cubes. The issue reference is removed from the app.
  • ADR-046. The "known issue" bullet is rewritten as the resolved root cause.

Evidence

All runs are on this Mac (M-series), sim-display, SIM_DISPLAY_OUTPUT=sbs, 10 s per launch. An early exit counts as a crash. Each crash left a .ips report.

configuration before (repaint on the app context) after
cursor active, runtime draws (DISPLAYXR_CURSOR_DEPTH_UV=0.5,0.5) 1 / 15 (SIGSEGV in repaint thread), then 0 / 30 0 / 32
same, DXR_WEAVE_REPAINT_FORCE=1 10 / 10 0 / 10
extension off (DISPLAYXR_CURSOR_DEPTH=0), DXR_WEAVE_REPAINT_FORCE=1 10 / 10 0 / 10
app draws the crosshair (DISPLAYXR_CURSOR_DEPTH_DRAW=app) — 0 / 10
custom sprite (DISPLAYXR_CURSOR_DEPTH_SPRITE=custom) — 0 / 6
cursor never active (UV 0.1,0.1) — 0 / 8
extension off — 0 / 8
cube_hosted_legacy_gl (private context, repaint still ON) — 0 / 8; with FORCE 0 / 6, repaints firing (count=273)

Notes on the evidence:

  • How "before" was measured. For the forced and 30-run columns, the same build had the new gate bypassed by a temporary env check that is not committed.
  • Crash signatures across the 20 forced before-crashes:
    • 16 × SIGSEGV on gl_repaint_thread in GLRResourceList hash emplace, the same as the issue's stack 1.
    • 3 × the same fault reached from RenderScene.
    • 1 × SIGABRT.
  • Unforced rate. Without FORCE, the repaint gate opened in only 1 of the 30 before-runs. That is why the natural rate is low and timing-dependent.
  • The cursor still draws. The GL atlas capture after the fix shows the runtime-drawn arrow on the cube in both tiles, with disparity.

Not tested

  • A real Leia/vendor panel on macOS GL. There is no macOS GL weaver here besides sim-display.
  • The NSOpenGLView + DXR_WEAVE_REPAINT loss in practice: a stalled GL handle app now holds its last presented frame instead of re-weaving. sim-display has no tracked phase, so this cannot be seen here.
  • The Windows / Linux GL paths. They are unchanged (the code is __APPLE__-only) and were not run.
  • The plain-NSView external path (private context attached to the app's view) under the new CGL lock. No test app takes it.

🤖 Generated with Claude Code

dfattal and others added 2 commits October 11, 2026 19:09
A handle app that binds an NSOpenGLView hands the runtime that view, and
comp_gl_window_macos_setup_external adopted the view's openGLContext - the
very CGL context the app renders with. The #868 repaint thread then replayed
the weave on it while the app thread was drawing (RenderScene runs between
xrBeginFrame and xrEndFrame, outside c->mutex and outside
app_frame_in_progress), so one GLDContextRec was driven from two threads:
SIGSEGV in GLRResourceList::addResource under the repaint's
glBlitFramebuffer, or a Metal-validation abort in the app's draw.

Hiding the OS cursor was only a timing trigger. With
DXR_WEAVE_REPAINT_FORCE=1 the old code crashes 10/10 launches with the
cursor extension off as well.

No lock the runtime takes can cover the app's own GL calls, so the repaint
thread is not started when the compositor's context is app-owned (one WARN
says so). Private contexts (own window, plain NSView, offscreen texture)
keep the repaint, now under CGLLockContext so it cannot interleave with
AppKit's -[NSOpenGLContext update] on the main thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Undo the #1946 opt-in (DISPLAYXR_CURSOR_DEPTH=1) now that the crash it
worked around is fixed in the GL compositor: like the Metal and Vulkan
cubes, the cursor is on by default and DISPLAYXR_CURSOR_DEPTH=0 opts out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dfattal
dfattal requested a review from a team as a code owner October 12, 2026 02:10
@dfattal
dfattal merged commit 8271f30 into main Oct 12, 2026
42 checks passed
@dfattal
dfattal deleted the fix/gl-repaint-appkit-1947 branch October 12, 2026 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GL compositor repaint thread vs AppKit cursor hide: ~1/5 crash in Apple's GL driver (cube_handle_gl_macos)

1 participant