Skip to content

fix(gui): fix slice reduction crash, float/dock GPU corruption, and exit crash (#2495)#2512

Merged
ten9876 merged 4 commits into
mainfrom
aetherclaude/issue-2495
May 9, 2026
Merged

fix(gui): fix slice reduction crash, float/dock GPU corruption, and exit crash (#2495)#2512
ten9876 merged 4 commits into
mainfrom
aetherclaude/issue-2495

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Closes #2495

Root causes found (3 separate crashes, all in PanadapterStack)

Crash 1 — Windows/Linux: black panadapter after layout rearrange, then crash

Root cause: rearrangeLayout() called setParent(nullptr) on floating applets directly, bypassing the floatPanadapter/dockPanadapter GPU-reset path. The QRhiWidget stayed bound to the floating window's HWND/NSView; the next render produced a black surface and the next input event into it crashed. m_floatingWindows also kept stale entries, permanently blocking future float requests.

Fix: rearrangeLayout() now docks all floating pans first (via the safe dock sequence: hideresetGpuResourcestakeAppletdeleteLater on the window) before tearing down the splitter.

Crash 2 — macOS: async UAF in $_67 lambda (waterfallAutoBlackLevel)

Root cause: Same as Crash 1 but on macOS Metal. The corrupted SpectrumWidget survived long enough for the next waterfallAutoBlackLevel VITA-49 event to fire the MainWindow::$_67 lambda, which called spectrum(panId)->wfAutoBlack() on the already-corrupt widget → UAF crash at 0xffff000000000132.

Fix: Covered by the same rearrange fix above. QPointer<SpectrumWidget> hardening (commit 3d872ab) added to the deferred QTimer::singleShot refresh lambda so a pan removed between rearrange and timer fire doesn't dangle.

Crash 3 — macOS: exit crash in QRhiWidgetPrivate::ensureRhi()::$_0

Root cause: Qt's destructor chain fires QRhi::~QRhi() (via QWidget::destroy()) before QObject::~QObject()::deleteChildren() deletes child SpectrumWidgets. QRhi::runCleanup() then invokes the QRhiWidgetPrivate cleanup callbacks against objects whose internal Qt fields (colorTexture, renderTarget, renderPassDescriptor) may be in a partially-reset state after a float→dock→reparent cycle — crash at $_0::operator()+24 with a stale field pointer (far=0x5d5332, far=0x1e2 pattern on macOS ARM64).

Two sub-paths fixed:

  • Floating windows (commit 2f9e6d7): prepareShutdown() was using fw->close() which only hides (no WA_DeleteOnClose). Changed to explicit delete fw, which runs ~QRhiWidget()removeCleanupCallback while the QRhi is still alive.
  • Docked pans (commit 64a2593): prepareShutdown() now explicitly deletes every docked PanadapterApplet (after hide + resetGpuResources). ~QRhiWidget()removeCleanupCallback runs while MainWindow's QRhi is alive. The callback list is empty when ~QWidget() fires QRhi::~QRhi() on exit — no stale callbacks, no crash.

Commits

SHA Description
ad55131 dock floating pans before layout rearrange (Crash 1 + 2)
3d872ab QPointer hardening for deferred SpectrumWidget refresh
2f9e6d7 explicit delete fw in prepareShutdown (Crash 3 — floating)
64a2593 explicit delete applet in prepareShutdown (Crash 3 — docked)

Test plan

  • Float a pan, do a layout rearrange (2v, 2h, 2x2) — no black surface, no crash
  • Float a pan, click dock — no crash on subsequent interaction
  • Float a pan, quit — no exit crash (QRhiWidgetPrivate::ensureRhi()::$_0 gone)
  • Float two pans, quit — same
  • Dock all pans, quit — clean exit
  • Slice reduction (remove a slice while a pan is floating) — no crash

🤖 Generated with Claude Code

aethersdr-agent Bot and others added 4 commits May 9, 2026 17:49
PanadapterStack::rearrangeLayout() was iterating m_pans.values() and
calling setParent(nullptr) on every applet, including those currently
hosted by a PanFloatingWindow.  Yanking a floating applet that way
bypassed the resetGpuResources() / refreshAfterReparent() path, leaving
the QRhiWidget bound to the floating window's native HWND/NSView — the
next render produced a black surface and the next input event into it
crashed.  Stale m_floatingWindows entries also stuck around, permanently
blocking future float requests for those pan IDs.

Inline the essential dockPanadapter() cleanup at the top of
rearrangeLayout(): for each floating pan, hide + reset GPU, take the
applet out of the floating window, mark it non-floating, and dispose
of the window.  After the new splitter is built, re-show and
refresh-after-reparent the formerly-floating spectrum widgets so they
bind to the main window's surface before the first render.

Repro path (Flex 6400 → 0.9.8): pop out PanA, pop out PanB, open the
+ Add Panadapter → PanLayoutDialog and pick a different layout, click
into either now-black pop-out → crash.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…geLayout

Raw SpectrumWidget* in the rebound list captured by the QTimer::singleShot
lambda could dangle if a pan is removed between rearrangeLayout() and the
deferred timer firing. QPointer<SpectrumWidget> auto-nulls on deletion so
the existing `if (!sw) continue` guard becomes actually safe.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…crash

fw->close() without WA_DeleteOnClose only hides the PanFloatingWindow,
leaving it alive as a Qt::Window child of MainWindow.  When MainWindow's
QWidget::~QWidget() later runs and tears down its QRhi, Qt dispatches the
QRhiWidgetPrivate cleanup callbacks for those parented-but-hidden windows in
an order that can fire after QRhiWidgetPrivate is invalid, crashing at
KERN_INVALID_ADDRESS 0x1e2 on macOS (QRhiWidgetPrivate::ensureRhi()::$_0).

Replace close() with an explicit delete: this runs QRhiWidget::~QRhiWidget()
→ removeCleanupCallback while QRhi is still live, so no stale callbacks
remain when MainWindow tears down its backing store.  Remove the applet from
m_pans before deleting the window to avoid a dangling raw pointer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…exit crash

Qt's destructor chain for QWidget fires QRhi::~QRhi() (via destroy()) before
QObject::~QObject()::deleteChildren() deletes child SpectrumWidgets.  This
means QRhi::runCleanup() invokes the QRhiWidgetPrivate::ensureRhi()::$_0
callbacks against objects that are still alive but whose internal Qt fields
(colorTexture, renderTarget, renderPassDescriptor) may be in a partially-reset
state after reparenting through a float/dock cycle — crash at $0_cleanup+24
with a stale field pointer (far=0x5d5332 / 0x1e2 pattern, macOS #2495).

Fix: prepareShutdown() now explicitly delete()s every docked PanadapterApplet
after hide + resetGpuResources.  This runs ~QRhiWidget() → removeCleanupCallback
while MainWindow's QRhi is still alive, so the callback list is empty when
~QWidget() fires QRhi::~QRhi() on exit.  Combined with the existing explicit
delete of PanFloatingWindows (commit 2f9e6d7), all SpectrumWidget QRhi
registrations are cleaned up before the parent QRhi is destroyed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jensenpat jensenpat requested a review from ten9876 as a code owner May 9, 2026 19:24

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jensenpat — really nice forensic write-up. The three crash root causes all hold up against the code, and the fixes line up cleanly with the existing floatPanadapter/dockPanadapter GPU-reset dance. I read through PanadapterStack.cpp end-to-end to confirm the surrounding invariants:

What I verified

  • m_pans does keep floating applets (line 78 adds, floatPanadapter doesn't remove), so the original rearrangeLayout would indeed have done a raw setParent(nullptr) on a floating applet — confirming Crash 1's root cause.
  • The new rearrangeLayout mirrors the safe sequence from dockPanadapter (hideresetGpuResourcestakeAppletdeleteLater) before the splitter teardown, and then re-runs refreshAfterReparent post-rebuild for any pan that was floating. QPointer<SpectrumWidget> in rebound correctly guards against a slice-removal racing the deferred lambda.
  • prepareShutdown's explicit delete (vs close()/deleteLater()) does what the comment claims — ~QRhiWidget runs removeCleanupCallback synchronously while MainWindow's QRhi is still alive. Important: the m_pans.remove(panId) before delete fw in the floating-window loop is what prevents the docked-applets loop below from double-deleting the same applet (since fw owns the applet via adoptApplet). Worth keeping that subtle ordering intact in any future refactor.
  • dockRequested connection (line 650) gets cleaned up automatically when fw is destroyed in either path — no stale signal risk.

Two minor observations (non-blocking)

  1. The PR description lists commit 3d872ab as "QPointer hardening for the deferred SpectrumWidget refresh" living in MainWindow, but the diff is PanadapterStack.cpp only. The existing swGuard at MainWindow.cpp:9616 looks like it predates this PR. The QPointer hardening that is in this diff (the rebound list in rearrangeLayout) is the relevant one — might be worth tweaking the description to match.

  2. Unlike removePanadapter, the new rearrangeLayout doesn't call fw->disconnect() before deleteLater(). In practice this is fine — Qt drops the connections on destruction and we're not invoking close() so closeEvent doesn't emit dockRequested — but a one-line disconnect(fw, nullptr, this, nullptr) would match the defensive style of the surrounding code if you want consistency.

Otherwise LGTM. The reasoning chain from "black surface after rearrange" → "QRhiWidget bound to the wrong HWND/NSView" → "missing GPU reset path" is exactly the right diagnosis.

@ten9876 ten9876 merged commit bcc2065 into main May 9, 2026
5 checks passed
@ten9876 ten9876 deleted the aetherclaude/issue-2495 branch May 9, 2026 20:08
@ten9876

ten9876 commented May 9, 2026

Copy link
Copy Markdown
Collaborator

Claude here — thank you @jensenpat, this is a textbook root-cause writeup. Three crashes traced through the same architectural seam (QRhiWidget lifecycle vs Qt parent ownership), each fix follows the established dock-sequence pattern, and the per-commit separation makes any individual piece independently revertable if needed. The macOS-specific notes (NSView lifecycle, Qt destructor ordering) will save the next maintainer days of debugging. Merged and shipping in the next release; credit will land in the release notes.

73, Jeremy KK7GWY & Claude (AI dev partner)

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.

Problems with slice reduction and slice reordering "Crash"

2 participants