Skip to content

Fix #306: end the stream when the console suspends - #318

Open
quadseven wants to merge 3 commits into
XITRIX:masterfrom
quadseven:fix/306-sleep-resume-crash
Open

Fix #306: end the stream when the console suspends#318
quadseven wants to merge 3 commits into
XITRIX:masterfrom
quadseven:fix/306-sleep-resume-crash

Conversation

@quadseven

@quadseven quadseven commented Jul 27, 2026

Copy link
Copy Markdown

Size: XS

Why

#306 is a hung console after waking from sleep mid stream. Orange screen, unresponsive, hard power off to recover.

The hang lives in the window where the OS tears the graphics and network services down underneath a process that keeps running. This ends the stream at that point instead of trying to carry decoder and GPU state through it, so there is no stale state left to get wrong.

This matches Moonlight on Android, where Game.onStop() calls stopConnection() then finish(): sleeping the phone mid stream ends the session and returns you to the host list. Suggested in review by @nyanpasu64, and after building and testing both approaches on hardware I agree it is the better one to ship.

What changed

On losing applet focus, drop input and call the existing terminate(false).

terminateApp is false, so only the stream ends. Whatever is running on the host keeps running and can be resumed by reconnecting.

Acceptance criteria

  • Sleeping mid stream ends the stream and shows the host list, with no hang
  • Reconnecting after waking works
  • HOME mid stream behaves the same way
  • Nothing is left running on the host that should not be

Test plan

Tested on hardware, Switch on Atmosphere, streaming a game from a PC:

  • Three sleep and resume cycles: stream ends, host list appears, reconnect works, no hang.
  • HOME mid stream: same behaviour.

I also built and tested the alternative, which keeps the session alive across the suspend and repairs the GPU frame mappings on the first resumed frame. That also survived three sleeps with no hang, so both are reliable. It is on my fork as archive/306-renderer-invalidation if it is ever wanted.

Out of scope

  • Preserving the stream across a suspend. That is the alternative above, and it is a bigger change.
  • Any behaviour outside applet focus loss.

@quadseven

Copy link
Copy Markdown
Author

Hardware result, on a V1 console with Atmosphere, running in title takeover mode (AppletType_Application, so appletSetFocusHandlingMode(NoSuspend) is actually in effect).

The hang is gone. Sleep during an active stream, then wake, previously gave a dead screen needing a 15 second power hold and a fresh payload injection. With this change the console wakes, the display comes back after a brief white flash while the display stack reinitialises, and the app stays responsive and exits cleanly.

The overlay confirmed the path runs, in order: StreamingView: window focus lost -> MoonlightSession: rendering suspended -> AppletFocusState_InFocus -> StreamingView: window focus gained -> MoonlightSession: rendering resumed.

Also checked the case this could plausibly have regressed: pressing HOME mid stream, waiting, and returning. The stream stays up and resumes, so the behaviour NoSuspend exists to provide is preserved. updateFrameMapping() reported five cached nvmap handles during that stream, which is the decoder surface pool this change invalidates.

One thing this does not fix, which was previously invisible because the console bricked first: after a real sleep the stream does not recover. Wifi is off while asleep, so the connection dies, and AVFrameQueue::pop() returns bufferFrame when the queue is empty, so the renderer keeps redrawing the last decoded frame indefinitely while input goes to a dead socket. The app is healthy and quits cleanly, it just never notices the session is dead.

That looks like a separate issue in the connection lifecycle rather than something to fold in here, so I have left this PR scoped to the hang. I am instrumenting a build to find out whether connection_terminated fires at all across a sleep and what the reconnect in MoonlightSession::connection_terminated does, and will open a separate PR if there is a clean fix.

@quadseven

Copy link
Copy Markdown
Author

Correction to my previous comment, plus the direct evidence for the root cause.

I was wrong that the stream does not recover. It does. With a file log attached, a real sleep and wake gives:

23:13:07.165 StreamingView: window focus lost
23:13:07.175 MoonlightSession: rendering suspended (active=true terminated=false)
23:13:27.221 Audio Receive: recvUdpSocket() failed: 115
23:13:27.221 MoonlightSession: Connection terminated with code: 115
23:13:27.221 MoonlightSession: Reconnection attempt
23:13:27.224 StreamingView: window focus gained
23:13:27.224 MoonlightSession: rendering resumed
23:13:27.795 MoonlightSession: Reconnected

The reconnect in connection_terminated() completes in about 570ms. What I described earlier as a frozen stream was me quitting during that window on a build with no logging. There is no second bug here, and nothing further is needed. Apologies for the noise.

The evidence for the stale mapping. The same log confirms the decoder surfaces really are reallocated across a sleep, which is what makes the invalidation in this PR load bearing rather than merely harmless.

nvmap handles cached by updateFrameMapping() before the sleep:

1236, 1244, 1252, 1276, 1292, 1300

On resume:

23:13:27.261 DKVideoRenderer: dropped 6 hardware frame mapping(s)

and the handles the renderer then maps after reconnecting:

1372, 1380, 1388, 1396, 1404, 1420

A completely disjoint set. Every mapping cached before the sleep was pointing at decoder memory that no longer existed in that form. Without the invalidation, updateFrameMapping() would have matched the stale cache entries by handle and kept sampling them, which is the GPU fault this PR is about.

The instrumentation used to produce this is a local diagnostic build, not part of this PR.

@nyanpasu64

Copy link
Copy Markdown

What happens if you remove appletSetFocusHandlingMode(AppletFocusHandlingMode_NoSuspend)? Please have your human test and reply rather than letting Claude write a response.

@quadseven

Copy link
Copy Markdown
Author

I tested it out. I built three versions: master with that line deleted, master untouched as a control, and this PR's branch unmodified. I used the same commit for the first two, so the only difference is just that one line.

With the line deleted, sleep and resume still crashes. I got a solid orange screen, the console hung, and I had to hold down power to get out of it. Pressing Home mid-stream also kills the stream, and doing it a second time panicked Atmosphere with Title ID: 0100000000000034, std::abort (0xFFE). That title ID is Atmosphere's own fatal module (stratosphere/fatal/fatal.json).

With the untouched master on the same commit: sleep and resume drops the stream, but the app stays up and relaunches fine, and pressing Home works 3/3 time. This PR's branch survives sleep and resume 3/3 with the controls still working afterwards.

So it doesn't fix the crash, and it basically turns a recoverable stream drop into a hung console.

One caveat: stock master degraded gracefully for me rather than hard-crashing the way #306 describes. So, this is really just measuring the effect of removing the call, not a reproduction of the original report.

Also, the call isn't even in this repo. It's in the borealis submodule at library/lib/platforms/switch/switch_platform.cpp:112, and xfangfang's borealis (the one wiliwili uses) has it in the exact same place. It seems like a framework-level thing rather than something this PR could just drop.

@nyanpasu64

Copy link
Copy Markdown

Why not pause the stream and return to the games menu upon console sleep? This should avoid all "stream surviving sleep mode" issues, and is consistent with behavior on Android (though a bit less convenient than fully surviving sleep, but even then it's a "good enough" solution even if a better fix gets added in the future).

Moonlight on Android ends the session when the app stops: Game.onStop() calls stopConnection() then finish(), so sleeping the phone mid stream drops you back to the host list. This does the same on Switch, where losing applet focus means the console sleeping or the HOME menu taking over.

On focus loss it drops input and calls the existing terminate(false), so the stream ends and the view falls back to the host list. terminateApp is false, so only the stream stops; whatever is running on the host keeps running and can be resumed by reconnecting.

Issue XITRIX#306 is a hung console after waking, and it lives in the window where the OS tears the graphics and network services down underneath a process that keeps running. Ending the stream removes the state rather than trying to repair it: there is nothing stale left to get wrong.

Tested on hardware, three sleep cycles plus HOME: the stream ends, the host list appears, reconnecting works, no hang.

Co-Authored-By: Claude <noreply@anthropic.com>
@quadseven
quadseven force-pushed the fix/306-sleep-resume-crash branch from 6f5fa71 to 6bd4dfe Compare August 1, 2026 17:47
@quadseven quadseven changed the title Fix #306: stop the stream touching the GPU while the app is off screen Fix #306: end the stream when the console suspends Aug 1, 2026
@quadseven

Copy link
Copy Markdown
Author

Swapped this PR over to your suggestion.

I built both and tested them on hardware first, three sleeps each plus home. Parity version ends the stream, lands on the host list, reconnects fine. The old approach kept the stream alive with controls working. Neither hangs, so reliability was a wash and it came down to which is less to go wrong later.

This one reuses the existing terminate(false) and carries nothing across the suspend, so there's no state left to get stale. I checked moonlight-android first and Game.onStop() does call stopConnection() then finish(), so it really is the same behaviour.

Old approach is on my fork as archive/306-renderer-invalidation if it's ever wanted.

@nyanpasu64

This comment was marked as outdated.

@nyanpasu64

Copy link
Copy Markdown

Note that I am running a beta version of libnx with incompatible changes, and I'm unsure if that's a factor.

Tried copying the file over the installed filename. Now it sometimes exits properly and sometimes crashes upon wake. I've managed to reproduce the crash via home screen, which should be easier to debug than sleep (since the latter disconnects gdb).

@nyanpasu64

Copy link
Copy Markdown
Thread 2 "libnx Thread_0x1466920fb0" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 147.1170]
0x0000001464574124 in MoonlightSession::stop (this=0x30, terminate_app=0) at /home/nyanpasu64/code/Moonlight-Switch/app/src/streaming/MoonlightSession.cpp:373
373         if (m_stop_requested)
=> 0x0000001464574124 <_ZN16MoonlightSession4stopEi+4>: 39456800        ldrb    w0, [x0, #346]
   0x0000001464574128 <_ZN16MoonlightSession4stopEi+8>: 370003e0        tbnz    w0, #0, 0x14645741a4 <_ZN16MoonlightSession4stopEi+132>
(gdb) set width 0
(gdb) bt
#0  0x0000001464574124 in MoonlightSession::stop (this=0x30, terminate_app=0) at /home/nyanpasu64/code/Moonlight-Switch/app/src/streaming/MoonlightSession.cpp:373
#1  0x000000146458489c in StreamingView::terminate (this=0x7315321a60, terminateApp=<optimized out>) at /home/nyanpasu64/code/Moonlight-Switch/app/src/streaming_view.cpp:452
#2  0x00000014646c8cf4 in std::function<void(bool)>::operator() (this=0x616add7cb0, __args#0=<optimized out>) at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/std_function.h:581
#3  brls::Event<bool>::fire (this=0x1465489750 <brls::Application::windowFocusChangedEvent>, args#0=false) at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/include/borealis/core/event.hpp:76
#4  0x0000001464f77b0c in appletCallHook (hookType=<optimized out>) at /home/nyanpasu64/code/libnx/nx/source/services/applet.c:526
#5  appletProcessMessage (msg=<optimized out>) at /home/nyanpasu64/code/libnx/nx/source/services/applet.c:3080
#6  0x0000001464f77d24 in appletMainLoop () at /home/nyanpasu64/code/libnx/nx/source/services/applet.c:3124
#7  0x000000146465d324 in brls::Application::internalMainLoop () at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/lib/core/application.cpp:206
#8  0x000000146465dc3c in std::__invoke_impl<bool, bool (*&)()> (__f=@0x616add7f20: 0x146465d280 <brls::Application::internalMainLoop()>) at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/invoke.h:63
#9  std::__invoke_r<bool, bool (*&)()> (__fn=@0x616add7f20: 0x146465d280 <brls::Application::internalMainLoop()>) at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/invoke.h:116
#10 std::_Function_handler<bool(), bool (*)()>::_M_invoke (__functor=...) at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/std_function.h:295
#11 std::function<bool()>::operator() (this=0x616add7f20) at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/std_function.h:581
#12 brls::Platform::runLoop (this=<optimized out>, runLoopImpl=...) at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/include/borealis/core/platform.hpp:176
#13 brls::Application::mainLoop () at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/lib/core/application.cpp:196
#14 0x00000014644d42c4 in main (argc=1, argv=0x7313104038) at /home/nyanpasu64/code/Moonlight-Switch/app/src/main.cpp:180

The bug is that you call StreamingView::terminate before MoonlightSession* session is initialized. Though this shouldn't be possible (modulo race conditions) since StreamingView initializes session in its constructor.

If I breakpoint MoonlightSession::MoonlightSession, hit the home button then, then continue execution, then StreamingView::terminate is called, and we get a crash in:

Thread 5 "libnx Thread_0x7696377900" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 149.1325]
GameStreamClient::server_data (this=0x34e718bf8 <Singleton<GameStreamClient>::instance()::instance>, address=...) at /home/nyanpasu64/code/Moonlight-Switch/app/src/streaming/GameStreamClient.hpp:57
57              return m_server_data[address];
(gdb) set width 0
(gdb) bt
#0  GameStreamClient::server_data (this=0x34e718bf8 <Singleton<GameStreamClient>::instance()::instance>, address=...) at /home/nyanpasu64/code/Moonlight-Switch/app/src/streaming/GameStreamClient.hpp:57
#1  operator() (__closure=0x7698139620) at /home/nyanpasu64/code/Moonlight-Switch/app/src/streaming/MoonlightSession.cpp:348
#2  0x000000034d90931c in std::function<void()>::operator() (this=0x2fb0f27ef0) at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/std_function.h:581
#3  brls::ThreadPool::threadEntry (this=0x76963774a0, i=<optimized out>) at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/lib/core/thread_pool.cpp:175
#4  0x000000034e2735bc in execute_native_thread_routine ()
#5  0x000000034e213584 in __thread_entry (__arg=0x2f67424e80) at /home/nyanpasu64/code/libnx/nx/source/runtime/newlib.c:160
#6  0x000000034e201728 in _EntryWrap (args=0x2fb0f27fd0) at /home/nyanpasu64/code/libnx/nx/source/kernel/thread.c:57

It appears the change made is wildly concurrency-unsafe. Out of curiosity can you push your old code to a different branch for me to test?

Reported with a gdb backtrace by nyanpasu64: SIGSEGV in MoonlightSession::stop with this=0x30, reached from appletCallHook -> Event<bool>::fire -> StreamingView::terminate.

The cause is in borealis' Event::fire:

    for (Callback cb : this->callbacks)
        cb(args...);

It iterates the callback list. terminate() calls dismiss(), which pops this view and runs its destructor, and the destructor unsubscribes. Doing that from inside the callback mutates the list fire() is walking, so the iteration continues over a destroyed object. That is the dangling this.

The callback now only records the intent and draw() acts on it, where the view is known alive and nothing is iterating the event. When the focus loss was a console sleep this runs on the first frame after waking, which is also the first moment the graphics service is back, so the teardown no longer happens while the OS is mid suspend.

Also null guards session in draw().

This does not address the separate async-start race in the second backtrace, where a MoonlightSession start callback runs on a thread pool thread while the view is going away. That one predates this change.

Co-Authored-By: Claude <noreply@anthropic.com>
@quadseven

Copy link
Copy Markdown
Author

Nice catch, thanks for checking it out.

The bug was that I was tearing the view down from inside the focus callback. borealis loops over its callback list when it fires the event, and my code was destroying the view halfway through that loop, so it kept going over something that no longer existed. Hence the garbage pointer.

Fixed by not doing anything destructive in the callback. It just sets a flag now and the next frame does the actual teardown, where nothing is mid-loop. When it’s a sleep that ends up happening on the first frame after waking, which is also nicer because it’s not tearing things down while the console is still going under.

Retested here, pressed home a bunch of times and did several sleeps, no crashes. I’m on release libnx though, not your beta, so it’d be good to know if it holds up on yours.

And yes, old renderer version is up as archive/306-renderer-invalidation on my fork. It’s straight off current master so it should build as-is.

@nyanpasu64

Copy link
Copy Markdown

I did notice one annoyance: I'd get a wall of "Input queue reached maximum size limit" after going to the home menu and returning to the app. Redirecting stdout to the PC using:

    registerDeepLinkHandler();

    nxlinkStdio();

followed by:

make Moonlight.nro -j$(nproc) && nxlink -a 192.168.0.67 Moonlight.nro --server

produced the following output:
log.plain.txt

Annoyingly there's 16 different occurrences of "Input queue reached maximum size limit" in the code. Picking a random one with streaming inputs, LiSendControllerMotionEvent seems to check inputEnabled, though not all paths seem to.

  • Perhaps onWindowFocusChanged() should set inputEnabled = false? An alternative is for the input paths to check for !pendingSuspendTerminate as well.
  • Was there a reason to alter the check to if (!session || session->is_terminated())? The commit message says "Also null guards session in draw()", yet StreamingView::session is initialized in its constructor and never cleared (besides being deleted at the end of the destructor, but the pointer is never cleared as it's inaccessible).
  • I found StreamingView::onFocusGained() gets called repeatedly when we open and close the +- menu, resulting in multiple subscriptions to calling onWindowFocusChanged. gdb shows this function called like 5 times after I repeatedly open and close the menu. I'm not sure if this is the right place to create a subscription, but you have to prevent duplicate subscriptions.
  • I think your PR approach is... incomplete. Sleep is not the only time Moonlight can drop a connection, attempt to reconnect, then fail completely. If I connect to my server, then pause the app for 10 seconds in Switch gdb, moonlight does the same thing, says "Reconnected", then the OS hangs.
    • It's easier to reproduce by disconnecting your PC from the network (eg. KDE's Networks Ethernet disconnect button). Waiting 5 or so seconds (and optionally reconnecting your PC) results in being kicked back to the home menu, or "Reconnected" followed by a hang, or a gray screen (with working log overlay) but trying to disconnect produces an orange screen.

(Also out of curiosity how much of your analysis was by hand, and how much of the code authorship was by hand, rather than LLM-written artifacts? I'm doing my debugging largely on my own.)

The focus subscription was created in onFocusGained, which runs every time the view regains focus, including when an overlay closes. The destructor removes one subscription, so every extra focus cycle left a callback registered on a global event pointing at freed memory once the view went away. Reported with a backtrace showing the dangling this.

Deferring the teardown to draw() did not fix that. The leaked callbacks still ran; they just wrote a flag into a destroyed object instead of calling terminate on it.

Subscribe once, in the constructor, immediately after session exists. Log focusSubscriptionCount on both sides: one streaming view exists at a time, so it is 1 while streaming and 0 otherwise, and anything else is this bug announcing itself instead of staying silent. brls::Event keeps its callback list private and offers no count of its own.

Also disable input before dropping it on focus loss, which is where the wall of input queue limit messages came from, and drop the null check on session, which is assigned in the constructor and never cleared.

Co-Authored-By: Claude <noreply@anthropic.com>
@nyanpasu64

nyanpasu64 commented Aug 5, 2026

Copy link
Copy Markdown

Thoughts:

  • I do think the functionality of this PR is necessary, if not sufficient to prevent crashing. On stock, merely opening the gallery while a stream is decoding in the background is enough to result in a full system hang.
  • Looking in my Tracy fork I observed that the app calls StreamingView::terminate as soon as you go to the home screen, but the contents of the screen take a while to redraw when you reopen the app.
  • To avoid burning CPU during sleep mode, I wonder if it's possible to enable AppletFocusHandlingMode_SuspendHomeSleepNotify once termination completes, then return to AppletFocusHandlingMode_NoSuspend when we regain focus. I don't know if we actually burn more power (from not entering deep sleep?) than on the home screen, if this fixes it, and if it breaks functionality.
  • I still get crashes whenever Moonlight exits the screen from a disconnect rather than user action; I can return to hbmenu (since Sphaira would sometimes crash on return), but reentering Moonlight crashes in main > appletInitializeGamePlayRecording > tmemCreate > memset. I don't think it's from a race condition between video decoding and shutdown; I created a private fork which halts the video decode thread after 5 seconds, but even after video halts I still get a reentry crash if I interrupt the network and no crash if I exit from the UI.
    • This is not caused by the call stack leading up to StreamingView::terminate (called on the main thread). On this PR, returning to the home menu calls this function along the same path as a network disconnect, but does not lead to a crash on app restart.
  • In Tracy, every crash is preceded by a number of repeated calls to ClInternalConnectionTerminated from multiple threads. The first call is followed by a single call to terminationCallbackThreadFunc > connection_terminated, and both execute before terminate runs. Is this related to the state corruption or incorrect order of events that primes a reentry crash?
    • When I close a stream without crashing, whether by returning to home menu or by exiting from the overlay, first (main thread) terminate begins execution, then before it returns the controlReceive thread prints "ENet wait interrupted", "Control stream connection failed: 4" followed by ClInternalConnectionTerminated. There are no further calls to ClInternalConnectionTerminated, and no calls to terminationCallbackThreadFunc > connection_terminated.

All in all the crashing and non-crashing paths have noticeable differences, but I don't know what's responsible for the remaining crash, and haven't started selectively executing or stubbing parts of the code to narrow down the culprit.

  • ClInternalConnectionTerminated exits if ConnectionInterrupted is set, and if not, runs connection_terminated on a background thread (before disabling itself to avoid re-running connection_terminated). On a user-initiated shutdown, terminate sets ConnectionInterrupted before background threads call ClInternalConnectionTerminated, so connection_terminated is never called.
    • I would blame connection_terminated, but it does nothing now that I've removed the reconnection attempt.
    • I honestly think the disorderly shutdown with multiple ClInternalConnectionTerminated is suspect, but it's coordinated by upstream Moonlight code so it's probably not ideal to change the process to fix a crash in the fork. I'm still going to hack it to hell and back for debugging...

Caveats:

  • I'm running my testing with the Audren backend stubbed out to reduce confounders. I don't know if this will affect behavior.
  • I know I got an "orange screen" crash before, but cannot reproduce. Perhaps it requires disconnecting Ethernet and not merely Tracy? To reproduce this while keeping Tracy logs, I may have to run Sunshine and Tracy on separate machines...
  • Alternatively is Tracy preventing the "nasty" crashes? I don't know.
  • I don't recall if the system hangs happened before or after I commented out connection_terminated > "MoonlightSession: Reconnection attempt".

@nyanpasu64

Copy link
Copy Markdown

master...nyanpasu64:Moonlight-Switch:fork#files_bucket
XITRIX/moonlight-common-c@borealis...nyanpasu64:moonlight-common-c:fork#files_bucket

it works. more or less.

  • when a reconnect fails, what order does brls::Logger::info("MoonlightSession: Reconnection failed"); m_active_session->m_status = Status::Terminate; and MoonlightSession::connection_terminated() { if (error_code != 0 && m_active_session->m_status <= Status::Active) { m_active_session->scheduleRestart(); run in?
    • this is immaterial because I specifically wrote my code to treat Terminate, TerminateAndRestart, and Restarting the same.
    • (we should never reach terminated on TerminateAndRestart. we should only schedule terminated after mainThreadRestart() sets m_active_session->m_status = Status::Restarting;. but shrug.)
  • why do i get a loading spinner and "Could not connect to server" in 1 second from home screen, but a 2 minute wait for reconnection attempts?
    • future problem
  • got a non-reproducible crash on app exit. could not replicate it under gdb so won't attempt to fix for the time being.
  • this is a debug fork and will leak memory until you connect a tracy GUI to pull logs out. possibly cmake -DTRACY_ENABLE=OFF will turn it off. i have not tested.
  • i would ask where the LLM came up with the idea for invalidateHardwareResources() but i see it copied frameMappings.clear(); from surrounding functions. do we need to if (vctx != nullptr) (you didn't add it)? i don't know.

@quadseven

quadseven commented Aug 6, 2026

Copy link
Copy Markdown
Author

Still working on this. Yes, the code is mostly LLM written. I'm not a Switch dev, I hacked this thing for the first time a few weeks ago so I could stream Sunshine off my PC instead of buying a Steam Deck, it started hanging whenever I slept it mid stream, and I threw an LLM at it and started testing on hardware. The testing is mine and I'm not going to argue about code I don't fully understand.

After you mentioned the repeated ClInternalConnectionTerminated, I put spans with thread ids around the focus change and the termination callback on my fork, written to the SD card as each one completes so a hang still leaves them behind. It crashed again last night and caught this:

session.connection_terminated   thread 861   ran 100.20 ms
applet.focus_gained             thread 759   started 7.77 ms into it
                                             92.43 ms still left to run

So they overlap, on different threads, rather than just landing near each other. connection_terminated is still inside its reconnect attempt, error code 115, when the applet regains focus on the main thread and the resume path starts running underneath it.

Correcting something I had in this comment earlier: I said the log stopped mid word about 40ms later, partway through "Stopping control stream", and read that as roughly where it died. That isn't right. borealis only fflush()es per line under __MINGW32__, so on Switch log.log is fully buffered and where it stops is a stdio boundary, not where the process stopped. I've set _IOLBF on it so it means something from now on.

The spans are the trustworthy part, since each is written and flushed as it completes. And they say connection_terminated closed normally at 100.20 ms, so it returned and whatever killed the process ran after it. No crash report and no fatal report on the card either. There are two ERPT records four seconds later but they're routine noise, the same pair this console writes every few minutes with AbortFlag false.

That sent me reading moonlight-common-c rather than our code, and the part I hadn't understood is that LiStopConnection ends up called from two threads. connection_terminated calls it on the AsyncTerm thread to reconnect, and StreamingView::terminate calls it on the main thread through MoonlightSession::stop. stop() only guards on m_stop_requested, and the reconnect path deliberately doesn't set that flag, so nothing excludes the two. The stage ladder in LiStopConnection has no lock.

Calling LiStopConnection from inside the callback on its own looks fine, the AsyncTerm thread in Connection.c exists so that doesn't deadlock. Restarting afterwards is the part I can't find support for: Limelight.h says LiStartConnection isn't thread safe, and it resets ConnectionInterrupted and alreadyTerminated, which stopControlStream and the ENet loop read to know when to stop.

moonlight-qt looks like it deliberately does the opposite. Its clConnectionTerminated doesn't call LiStopConnection at all, it pushes SDL_QUIT and returns, the main thread unwinds, and LiStopConnection runs once from there with a semaphore held until it returns so a restart can't overlap a teardown. Which I think is what you've already arrived at with mainThreadRestart.

So sleep probably isn't the trigger, it's the termination callback and the resume being live at the same time, which would be why pulling the network gets you there too.

Leaving this PR open for XITRIX to decide on, it's their repo and their call. If you want to open yours alongside it, please do, better they have both to pick from.

If it's useful having someone with hardware who'll run whatever build you want and send back traces like that, I'm around.

@nyanpasu64

Copy link
Copy Markdown

Never reproduced the crash on gdb, but managed to recconstruct the call stack from atmosphere/crash_reports/...log.

    Stack Trace:
        ReturnAddress[00]:       0000002201581060 (Moonlight + 0x263060)
        ReturnAddress[01]:       000000220155c998 (Moonlight + 0x23e998)
        ReturnAddress[02]:       00000022015620b4 (Moonlight + 0x2440b4)
        ReturnAddress[03]:       0000002201444304 (Moonlight + 0x126304)
        ReturnAddress[04]:       000000220156207c (Moonlight + 0x24407c)
        ReturnAddress[05]:       00000022015894b4 (Moonlight + 0x26b4b4)
        ReturnAddress[06]:       0000002201558998 (Moonlight + 0x23a998)
        ReturnAddress[07]:       000000220155d04c (Moonlight + 0x23f04c)
        ReturnAddress[08]:       000000220155d93c (Moonlight + 0x23f93c)
        ReturnAddress[09]:       00000022013ca224 (Moonlight + 0xac224)
        ReturnAddress[10]:       00000022013b87a0 (Moonlight + 0x9a7a0)
/opt/devkitpro/devkitA64/bin/aarch64-none-elf-addr2line -e Moonlight.elf -fpC 0x9a7a0 0xac224 0x23f93c 0x23f04c 0x23a998 0x26b4b4 0x24407c 0x126304 0x2440b4 0x23e998 0x263060
exit at ??:?
main at /home/nyanpasu64/code/Moonlight-Switch/app/src/main.cpp:188 (discriminator 2)
std::_Function_base::~_Function_base() at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/std_function.h:248
brls::Application::internalMainLoop() at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/lib/core/application.cpp:261
std::_Deque_iterator<brls::View*, brls::View*&, brls::View**>::operator++() at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/stl_deque.h:195
brls::AppletFrame::~AppletFrame() at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/include/borealis/views/applet_frame.hpp:47
brls::Box::~Box() at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/lib/core/box.cpp:764(discriminator 8)
MainTabs::~MainTabs() at /home/nyanpasu64/code/Moonlight-Switch/app/include/main_tabs_view.hpp:16 (discriminator 1)
brls::Box::~Box() at /home/nyanpasu64/code/Moonlight-Switch/extern/borealis/library/lib/core/box.cpp:764(discriminator 8)
std::char_traits<char>::copy(char*, char const*, unsigned long) at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/char_traits.h:432
bool std::operator==<char, std::char_traits<char>, std::allocator<char> >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, char const*) at /opt/devkitpro/devkitA64/aarch64-none-elf/include/c++/16.1.0/bits/basic_string.h:4121

Don't have time to look into it now; my current problem is that the frame pacing added to master increases input latency to the point mouse movement is difficult to control, and I need to figure out how it works and how to wrangle it into a useful state.

@Sinnohman

Copy link
Copy Markdown

Tested this fix on FW 21.1.0 + Atmosphere 1.11.2 (emuMMC), as a custom NRO build. Previously: sleeping the console mid-stream caused a hard orange-screen hang on wake (had to hard power off). With this PR applied: auto-sleep, HOME-menu sleep, and hold-HOME popup sleep all end the stream cleanly and return to the app selection screen relaunching reconnects instantly. Zero crashes across a full day of testing.

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.

3 participants