Skip to content

[Fix] Exit Stream Navigation#248

Open
mpotrykus wants to merge 12 commits intoTheElixZammuto:masterfrom
mpotrykus:fix-exit-stream-navigation
Open

[Fix] Exit Stream Navigation#248
mpotrykus wants to merge 12 commits intoTheElixZammuto:masterfrom
mpotrykus:fix-exit-stream-navigation

Conversation

@mpotrykus
Copy link
Copy Markdown
Contributor

@mpotrykus mpotrykus commented Jan 11, 2026

Swallows any errors and safely returns. If there are issues, safely return to HostSelectorPage.

---------UPDATE---------

This PR is now a 2-part fix.

  1. On exiting a stream, it will send you back to the AppPage. On the chance of a null host, the page load event now checks for host, if none is found for any reason, you are directed back to the HostSelectorPage.

  2. The threads were remarshalled when starting a stream, in order to free the UI thread. This fixes a couple things:

    1. Status updates now happen on the UI for init stages.
    2. The UI is now free to handle any errors that happen during initialization. Before, the process could hang long after the error was received until cleanup work was done from the library.
    3. I also changed the error response dialog to a direct message that your average user can understand. There is still the option to toggle over to logs for a more in-depth look.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added real-time streaming progress indicators showing current operation stages
    • Enhanced error reporting with quick access to diagnostic logs
  • Bug Fixes

    • Improved connection failure handling with better error messages
    • Wake-on-LAN now includes automatic host connectivity verification
    • Better stream exit cleanup and navigation handling
  • UI/UX

    • Centered status displays and stage information during streaming startup

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces a centralized stream exit mechanism (ExitStreamPage), refactors error and status handling in the streaming startup pipeline, adds loading state tracking to the renderer, enhances logging with formatted stage names, improves host wake polling, and adjusts UI components to display progress information.

Changes

Cohort / File(s) Summary
Streaming Exit & Navigation
Streaming/moonlight_xbox_dxMain.h, Streaming/moonlight_xbox_dxMain.cpp, Pages/StreamPage.xaml, Pages/StreamPage.xaml.cpp, Pages/StreamPage.xaml.h
Introduces centralized ExitStreamPage() method for cleanup and navigation. Replaces inline navigation handlers with event-driven logic (OnFailed, OnCompleted, OnStatusUpdate). Adds StepText UI element and m_stepText property to display current loading stage. Simplifies Page_Loaded initialization by inlining DISPATCH_UI. Updates error/completion handlers to manage progress visibility and error dialogs.
Configuration & App Resolution
State/StreamConfiguration.h, Pages/AppPage.xaml.cpp
Adds appName property to StreamConfiguration. Introduces GetAppById helper to resolve application name from host configuration. Populates StreamConfiguration with host-derived settings (FPS, audio, codec, HDR, stats, graphs) during Connect flow.
Error Handling & Logging Enhancements
State/MoonlightClient.cpp, Utils.cpp
Expands failure handling for gs_start_app and LiStartConnection to invoke OnFailed callbacks. Enhances logging with formatted stage names via LiGetFormattedStageName. Standardizes log messages to include stage context. Adds mutex locking in GetLogLines for thread-safe access.
Rendering & Loading State
Streaming/VideoRenderer.h, Streaming/VideoRenderer.cpp
Adds m_loadingSuccessful atomic and IsLoadingSuccessful/IsLoadingComplete accessors. Moves streaming startup to threadpool dispatch (DISPATCH_THREADPOOL). Updates loading state flags on success/failure. Adjusts rendering wait condition to account for both completion and success flags.
Host Wake Polling
Pages/HostSelectorPage.xaml.cpp
Replaces post-WoL success confirmation with aggressive polling mechanism. Spawns background task to repeatedly query host connectivity for up to 60 seconds, tracking consecutive successes. Resets WolPolling flag via dispatcher upon success or timeout.
Dispatch Macros & Helpers
pch.h
Refactors DISPATCH_UI macro to accept single LAMBDA parameter with Normal priority. Introduces DISPATCH_THREADPOOL macro for threadpool execution. Adds LiGetFormattedStageName helper function to retrieve and capitalize stage names.
Project & Build Configuration
moonlight-xbox-dx.sln, moonlight-xbox-dx.vcxproj, Package.appxmanifest, App.xaml.cpp
Updates Visual Studio solution format from version 17 to 18 with new project GUIDs and configuration mappings. Applies XML declaration/formatting adjustments. Minor documentation comment alteration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Improve Xbox One performance #239: Modifies streaming render loop, VideoRenderer loading state, StreamConfiguration initialization, and client event handlers—directly overlaps with core changes to loading state tracking and configuration propagation.
  • Fix stream load display #246: Modifies stream startup UI lifecycle and completion handling in StreamPage and moonlight_xbox_dxMain—shares overlapping changes to loading indicators and OnCompleted handler wiring.
  • Fix 60 & 120fps frame pacing issues and eliminate screen tearing at 120hz #222: Modifies render loop termination and page navigation logic in moonlight_xbox_dxMain—related through the new ExitStreamPage centralization and render-loop exit paths.

Suggested reviewers

  • TheElixZammuto

Poem

🐰 Through streams of light the rabbits hop,
With exits clean and errors stop!
Loading states now gleam and show,
Stage names whisper as connections grow.
A threadpool skip—what's not to love? ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[Fix] Exit Stream Navigation' directly describes the main change: centralizing and improving the safe exit flow from streaming back to the host selector/app page with proper error handling and fallback logic.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can enable review details to help with troubleshooting, context usage and more.

Enable the reviews.review_details setting to include review details such as the model used, the time taken for each step and more in the review comments.

@mpotrykus mpotrykus changed the title fix exit stream navigation Fix exit stream navigation Jan 11, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @Streaming/moonlight_xbox_dxMain.cpp:
- Around line 547-558: The loop reading rootFrame->Content after calling
rootFrame->GoBack() can race because Content updates only after navigation
completes; update the logic in the loop that uses rootFrame, CanGoBack, GoBack,
Content and AppPage: either inspect rootFrame->BackStack entries (check each
entry's SourcePageType for AppPage) to determine if an AppPage exists before
calling GoBack, or convert the loop to trigger GoBack and wait for the
Frame::Navigated event (or a short completion wait tied to Navigated) before
re-reading rootFrame->Content and continuing; ensure you stop iterating when
CanGoBack is false or when an AppPage is found and keep maxSteps control.
🧹 Nitpick comments (1)
Streaming/moonlight_xbox_dxMain.cpp (1)

533-575: Consider adding minimal error logging for debuggability.

The broad catch(...) blocks silently swallow all exceptions, which aligns with the PR's goal of safe returns. However, this makes debugging difficult when issues occur.

Adding a single log line in the outer catch would preserve the safe-return behavior while providing visibility:

📝 Suggested logging addition
 void moonlight_xbox_dxMain::ExitStreamPage() {
 	try {
 		// ... existing code ...
-	} catch (...) {
+	} catch (...) {
+		Utils::Logf("ExitStreamPage: unexpected exception during navigation\n");
 	}
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3993f9a and 000406d.

📒 Files selected for processing (2)
  • Streaming/moonlight_xbox_dxMain.cpp
  • Streaming/moonlight_xbox_dxMain.h
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: mpotrykus
Repo: TheElixZammuto/moonlight-xbox PR: 246
File: Pages/StreamPage.xaml.cpp:77-91
Timestamp: 2026-01-10T18:24:38.508Z
Learning: In the moonlight-xbox project's StreamPage::Page_Loaded, the inner DISPATCH_UI macro (lines 83-91 in StreamPage.xaml.cpp) is necessary to ensure the loading screen displays correctly. Even though the outer RunAsync executes on the UI thread, wrapping the heavy initialization (CreateDeviceDependentResources, CreateWindowSizeDependentResources, StartRenderLoop) in a separate DISPATCH_UI breaks the work into two message queue items, allowing the UI to update and show the loading screen between the moonlight_xbox_dxMain construction and the resource initialization.
📚 Learning: 2026-01-10T18:24:38.508Z
Learnt from: mpotrykus
Repo: TheElixZammuto/moonlight-xbox PR: 246
File: Pages/StreamPage.xaml.cpp:77-91
Timestamp: 2026-01-10T18:24:38.508Z
Learning: In the moonlight-xbox project's StreamPage::Page_Loaded, the inner DISPATCH_UI macro (lines 83-91 in StreamPage.xaml.cpp) is necessary to ensure the loading screen displays correctly. Even though the outer RunAsync executes on the UI thread, wrapping the heavy initialization (CreateDeviceDependentResources, CreateWindowSizeDependentResources, StartRenderLoop) in a separate DISPATCH_UI breaks the work into two message queue items, allowing the UI to update and show the loading screen between the moonlight_xbox_dxMain construction and the resource initialization.

Applied to files:

  • Streaming/moonlight_xbox_dxMain.h
  • Streaming/moonlight_xbox_dxMain.cpp
📚 Learning: 2025-12-14T22:36:59.945Z
Learnt from: andygrundman
Repo: TheElixZammuto/moonlight-xbox PR: 239
File: Streaming/moonlight_xbox_dxMain.cpp:150-159
Timestamp: 2025-12-14T22:36:59.945Z
Learning: In Streaming/moonlight_xbox_dxMain.cpp, initialize lastFramePts to 0 because the first decoded frame's PTS is never 0, enabling safe detection of frame changes in the render loop. Include a clarifying comment about this assumption. If this assumption could change in the future, add a one-time initialization using the first non-zero PTS to guard against regressions.

Applied to files:

  • Streaming/moonlight_xbox_dxMain.cpp
🧬 Code graph analysis (2)
Streaming/moonlight_xbox_dxMain.h (1)
Streaming/moonlight_xbox_dxMain.cpp (2)
  • ExitStreamPage (532-576)
  • ExitStreamPage (532-532)
Streaming/moonlight_xbox_dxMain.cpp (1)
Pages/AppPage.xaml.cpp (1)
  • AppPage (27-34)
🪛 Cppcheck (2.19.0)
Streaming/moonlight_xbox_dxMain.cpp

[error] 569-569: syntax error

(syntaxError)

🔇 Additional comments (4)
Streaming/moonlight_xbox_dxMain.h (1)

33-33: LGTM!

The new ExitStreamPage() declaration is appropriately placed alongside related navigation methods (Disconnect, CloseApp).

Streaming/moonlight_xbox_dxMain.cpp (3)

7-8: LGTM!

Required includes for the navigation logic in ExitStreamPage().


194-196: LGTM!

Correct use of DISPATCH_UI to dispatch the navigation call to the UI thread from the background render loop worker. This properly replaces the previous inline navigation logic.


569-569: Static analysis hint is likely a false positive.

Cppcheck reports a syntax error here, but HostSelectorPage::typeid is valid C++/CX syntax for obtaining type information. Cppcheck doesn't fully support C++/CX language extensions.

Verify this compiles successfully in your build environment.

Comment on lines +547 to +558
for (unsigned int step = 0; step < maxSteps && rootFrame->CanGoBack; ++step) {
auto current = dynamic_cast<AppPage ^>(rootFrame->Content);
if (current != nullptr) {
reachedAppPage = true;
break;
}
try {
rootFrame->GoBack();
} catch (...) {
break;
}
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

UWP Windows::UI::Xaml::Controls::Frame GoBack synchronous or asynchronous

💡 Result:

Frame.GoBack() is a synchronous method (returns void) — there is no built‑in GoBackAsync API. Callers can invoke GoBack() directly; if you need to await completion of the navigation, listen for the Frame.Navigated (or NavigationService) events and/or use a TaskCompletionSource to produce an awaitable Task. [1][2]

Sources:
[1] Frame class (Windows.UI.Xaml.Controls) — Microsoft Docs.
[2] Back navigation for UWP apps — Microsoft Docs.


🌐 Web query:

UWP Frame GoBack Content property updated immediately or after navigation complete

💡 Result:

Short answer: Frame.Content is updated only when the navigation completes — after the Frame raises its Navigated event (not instantly at the moment you call GoBack()). See the Frame/navigation lifecycle docs which show the frame fires Navigating → (page OnNavigatingFrom/OnNavigatingTo) → Navigated when the new content is ready. [1][2]

Sources:

  • Microsoft: Peer-to-peer navigation / navigation lifecycle (Frame.Navigate, Navigated, Navigating). [1]
  • Tutorial/teaching note describing event order (Navigate → Navigating → page OnNavigatingFrom → Navigated → page OnNavigatingTo). [2]

Potential race condition with asynchronous navigation.

Frame::GoBack() is synchronous (returns immediately), but Frame.Content is only updated after the navigation completes (when the Navigated event fires). Reading rootFrame->Content on the next loop iteration may not reflect the new page yet, causing the loop to overshoot or miss the AppPage.

Consider either:

  1. Checking BackStack entries directly to find AppPage (via SourcePageType)
  2. Using Navigated events to confirm navigation completion before reading Content
🤖 Prompt for AI Agents
In @Streaming/moonlight_xbox_dxMain.cpp around lines 547 - 558, The loop reading
rootFrame->Content after calling rootFrame->GoBack() can race because Content
updates only after navigation completes; update the logic in the loop that uses
rootFrame, CanGoBack, GoBack, Content and AppPage: either inspect
rootFrame->BackStack entries (check each entry's SourcePageType for AppPage) to
determine if an AppPage exists before calling GoBack, or convert the loop to
trigger GoBack and wait for the Frame::Navigated event (or a short completion
wait tied to Navigated) before re-reading rootFrame->Content and continuing;
ensure you stop iterating when CanGoBack is false or when an AppPage is found
and keep maxSteps control.

@andygrundman
Copy link
Copy Markdown
Contributor

Isn't my approach more reliable in that it doesn't depend on a stack of pages and navigating back? I liken it to using the back button in a web browser vs clicking the link that goes to the home page. The point from CR about async back navigation also makes it seem a lot more fragile to use your method.

@mpotrykus
Copy link
Copy Markdown
Contributor Author

Isn't my approach more reliable in that it doesn't depend on a stack of pages and navigating back? I liken it to using the back button in a web browser vs clicking the link that goes to the home page. The point from CR about async back navigation also makes it seem a lot more fragile to use your method.

Open to suggestions. This is just attempting a safe "back". Swallowing errors, and if it can't be done, go back to host like we know works.

I tried a more direct approach, like you had, of going straight to the host's app page, but it messed with the order of selected pages, and hitting the back button or "b" would put you back into the stream page.

Unless I'm misunderstanding, we have to use "back" to keep history in line, otherwise we have to rewire all back functions to go exactly where we want instead of just back.

@andygrundman
Copy link
Copy Markdown
Contributor

OK, that's a fair point about breaking B-button back. I will apply this and test it out.

@andygrundman
Copy link
Copy Markdown
Contributor

andygrundman commented Jan 12, 2026

When I tested this by restarting Sunshine, I got sent back to an empty "apps page", I think this is why I didn't like using back, because you have to go back to the screen above this, the "host list" page. After quite a while sitting on the empty apps page I got a popup that said "Disconnected / Connection to host was lost". Hitting OK on that took me to the list of hosts, where my host had the disconnection icon. Restarting the app is the only thing you can do at this point.

One thing I would suggest is to log everything that ends up in one of the catch blocks. There could be useful info in those errors. I would also try to wait on the GoBack() call if it's async.

@mpotrykus
Copy link
Copy Markdown
Contributor Author

mpotrykus commented Jan 12, 2026

@andygrundman Interesting! I was wondering what issue you were running into. Mine goes to a filled app page every time. So that tells me that you are being sent back to the app page, but you lost the host connected to it.

Two things come to mind for me on that:

  1. We need to figure out why that is happening. Would you be able provide any further info?

You said you got the "Disconnected/Connection to host was lost". This is normal with some of the new UI changes I threw in. The current MDNS handler seems to kill the queries and connections after the app being open for a while. I added a fix that "should" handle that in my "mdns improvements" PR.

That being said, are you getting this after waiting a long time? Or immediate? Immediate could mean the app lost the host connection, If it sits a while and then that message appears (like it would in a normal scenario) that could mean you still have the host, but apps tiles are not being rendered. They could have missed the window to appear.

  1. Knowing that, I think this process should change. How do you feel about this:
  • Stream stops
  • We do a simple "back" to AppPage (no other fancy stuff)
  • AppPage has a validation on NavigatedTo that says
    • Do I have a host?
      • No? Hard send to HostSelectorPage.
      • Yes? Continue

AppPage should NEVER be used without a host, so I think this makes sense.

Thoughts on all that?

@mpotrykus
Copy link
Copy Markdown
Contributor Author

Knowing that, I think this process should change. How do you feel about this:
Stream stops
We do a simple "back" to AppPage (no other fancy stuff)
AppPage has a validation on NavigatedTo that says
Do I have a host?
No? Hard send to HostSelectorPage.
Yes? Continue

So anyways, I did this. I did some forced "null host" tests. Let me know if it fixes it for you. Should redirect right back to HostSelectorPage after stream.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @Streaming/moonlight_xbox_dxMain.cpp:
- Around line 545-553: The check of rootFrame->Content immediately after calling
rootFrame->GoBack() is unreliable because GoBack() initiates navigation
asynchronously; replace the immediate Content check and instead subscribe to
rootFrame->Navigated (and optionally NavigationFailed/NavigationStopped) to
detect completion and set reachedAppPage when the new Content is an AppPage;
implement a short-lived event handler or a TaskCompletionSource-like pattern
tied to the Navigated event that awaits navigation completion before inspecting
rootFrame->Content, and ensure you unsubscribe the handler after it fires to
avoid leaks.
🧹 Nitpick comments (1)
Streaming/moonlight_xbox_dxMain.cpp (1)

555-561: Fallback to null content is a last resort but leaves app in a broken state.

Setting rootFrame->Content = nullptr (line 559) when both GoBack and Navigate fail will leave the app with no visible UI. While this is an extreme edge case and better than crashing, users would see a blank screen with no way to recover.

Consider whether a more graceful fallback is possible, such as attempting to create a new HostSelectorPage instance directly:

Possible alternative fallback
 			try {
 				rootFrame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSelectorPage::typeid));
 			} catch (...) {
-				rootFrame->Content = nullptr;
+				try {
+					rootFrame->Content = ref new HostSelectorPage();
+				} catch (...) {
+					rootFrame->Content = nullptr;
+				}
 				Utils::Log("ExitStreamPage: Failed to return to HostSelectorPage\n");
 			}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 000406d and fe68c3d.

📒 Files selected for processing (2)
  • Pages/AppPage.xaml.cpp
  • Streaming/moonlight_xbox_dxMain.cpp
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-01-10T18:24:38.508Z
Learnt from: mpotrykus
Repo: TheElixZammuto/moonlight-xbox PR: 246
File: Pages/StreamPage.xaml.cpp:77-91
Timestamp: 2026-01-10T18:24:38.508Z
Learning: In the moonlight-xbox project's StreamPage::Page_Loaded, the inner DISPATCH_UI macro (lines 83-91 in StreamPage.xaml.cpp) is necessary to ensure the loading screen displays correctly. Even though the outer RunAsync executes on the UI thread, wrapping the heavy initialization (CreateDeviceDependentResources, CreateWindowSizeDependentResources, StartRenderLoop) in a separate DISPATCH_UI breaks the work into two message queue items, allowing the UI to update and show the loading screen between the moonlight_xbox_dxMain construction and the resource initialization.

Applied to files:

  • Pages/AppPage.xaml.cpp
  • Streaming/moonlight_xbox_dxMain.cpp
📚 Learning: 2025-12-14T22:36:59.945Z
Learnt from: andygrundman
Repo: TheElixZammuto/moonlight-xbox PR: 239
File: Streaming/moonlight_xbox_dxMain.cpp:150-159
Timestamp: 2025-12-14T22:36:59.945Z
Learning: In Streaming/moonlight_xbox_dxMain.cpp, initialize lastFramePts to 0 because the first decoded frame's PTS is never 0, enabling safe detection of frame changes in the render loop. Include a clarifying comment about this assumption. If this assumption could change in the future, add a one-time initialization using the first non-zero PTS to guard against regressions.

Applied to files:

  • Streaming/moonlight_xbox_dxMain.cpp
🧬 Code graph analysis (2)
Pages/AppPage.xaml.cpp (2)
Pages/AppPage.xaml.h (1)
  • MoonlightHost (31-35)
Utils.hpp (3)
  • Log (13-13)
  • Log (14-14)
  • Logf (15-15)
Streaming/moonlight_xbox_dxMain.cpp (3)
Pages/AppPage.xaml.cpp (1)
  • AppPage (28-35)
Utils.cpp (4)
  • Log (57-82)
  • Log (57-57)
  • Log (84-88)
  • Log (84-84)
Utils.hpp (2)
  • Log (13-13)
  • Log (14-14)
🪛 Cppcheck (2.19.0)
Pages/AppPage.xaml.cpp

[error] 45-45: syntax error

(syntaxError)

Streaming/moonlight_xbox_dxMain.cpp

[error] 557-557: syntax error

(syntaxError)

🔇 Additional comments (6)
Pages/AppPage.xaml.cpp (3)

5-5: LGTM!

The include is required to support the new redirect logic when navigating with a null host.


41-74: Solid defensive navigation logic with proper exception handling.

The implementation correctly uses WeakReference to safely access the page in the deferred lambda, preventing dangling pointer issues if the page is destroyed before the dispatcher callback executes. The nested try/catch blocks ensure exceptions are logged rather than crashing the app.

Note: The Cppcheck syntax error at line 45 is a false positive—the HostSelectorPage::typeid syntax is valid C++/CX for obtaining a Windows Runtime type.


358-370: LGTM!

Formatting-only changes with no functional impact.

Streaming/moonlight_xbox_dxMain.cpp (3)

7-8: LGTM!

Required includes for the new ExitStreamPage() functionality.


194-196: LGTM!

Clean refactoring that centralizes stream exit navigation into a dedicated function, improving maintainability and enabling reuse.


532-566: Good defensive implementation overall.

The function centralizes stream exit navigation with broad exception handling that prevents crashes, aligning with the PR objective to "swallow any errors and safely return." The logic correctly handles edge cases and provides fallback paths.

Note: The Cppcheck syntax error at line 557 is a false positive—HostSelectorPage::typeid is valid C++/CX syntax.

Comment on lines +545 to +553
try {
rootFrame->GoBack();
} catch (...) {
Utils::Log("ExitStreamPage: Failed to GoBack()\n");
}

if (!reachedAppPage) {
if (dynamic_cast<AppPage ^>(rootFrame->Content) != nullptr) reachedAppPage = true;
}
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

UWP Frame.GoBack() synchronous asynchronous timing behavior

💡 Result:

Short answer

  • Frame.GoBack() is a synchronous call (void) that immediately starts a navigation operation but does not block until the new page is loaded — the actual navigation completion happens later and is reported via Frame events (Navigated, NavigationStopped, NavigationFailed). [1][2]

How to know when the back navigation finished

  • Handle Frame.Navigated (or NavigationFailed/NavigationStopped) to detect when the navigation is complete/failed. [1][2]
  • If you need an awaitable pattern, create a TaskCompletionSource and complete it from a one-time Navigated handler (subscribe, call GoBack(), await the TCS, then unsubscribe). Example patterns are shown in community posts. [3]

References

  1. Microsoft docs — Back navigation / Frame navigation (Navigated etc.). [1]
  2. Microsoft docs — Navigation / peer-to-peer navigation (Frame behavior). [2]
  3. Community examples showing using Frame.Navigated to get the page instance or await navigation. [3]

Fix unreliable navigation state check after GoBack().

Frame::GoBack() is synchronous in return but asynchronous in effect—it immediately returns after initiating the navigation operation, but the actual page transition and Content update happen later. The code checks rootFrame->Content at line 552 immediately after calling GoBack() at line 546, but the Content property still reflects the old page at that point. This check is unreliable and explains why navigation failures were observed.

To properly detect when the back navigation completes, subscribe to Frame.Navigated (or handle NavigationFailed/NavigationStopped) instead of checking Content immediately. Alternatively, if you need the content to be updated before proceeding, you'll need to await navigation completion using a TaskCompletionSource pattern tied to the Navigated event.

🤖 Prompt for AI Agents
In @Streaming/moonlight_xbox_dxMain.cpp around lines 545 - 553, The check of
rootFrame->Content immediately after calling rootFrame->GoBack() is unreliable
because GoBack() initiates navigation asynchronously; replace the immediate
Content check and instead subscribe to rootFrame->Navigated (and optionally
NavigationFailed/NavigationStopped) to detect completion and set reachedAppPage
when the new Content is an AppPage; implement a short-lived event handler or a
TaskCompletionSource-like pattern tied to the Navigated event that awaits
navigation completion before inspecting rootFrame->Content, and ensure you
unsubscribe the handler after it fires to avoid leaks.

@mpotrykus mpotrykus changed the title Fix exit stream navigation [Fix] Exit Stream Navigation Jan 14, 2026
@mpotrykus
Copy link
Copy Markdown
Contributor Author

Buffed up this PR with error-handling on StartStreaming navigation exits.

Also completely marshalled the process between UI and background threads so UI updates can appear.

Screenshot 2026-02-10 084114 Screenshot 2026-02-10 084138 Screenshot_2026-02-10_08-43-51 Screenshot_2026-02-10_08-44-09

@mpotrykus
Copy link
Copy Markdown
Contributor Author

@andygrundman if you get the time, please review and let me know if you have questions on why I did anything the way I did to the StartStreaming process.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
State/MoonlightClient.cpp (1)

396-406: ⚠️ Potential issue | 🟠 Major

sprintf with %s and a potentially null LiGetFormattedStageName result is undefined behavior; also failingPorts is silently dropped from the format string.

Two issues here:

  1. LiGetFormattedStageName(stage) returns nullptr when LiGetStageName returns null (see pch.h line 240). Passing nullptr to sprintf %s is UB (typically a crash).
  2. The format string has 3 specifiers (%d, %s, %d) but 4 arguments are passed — failingPorts is computed and populated but never printed.
Proposed fix
-	sprintf(message, "Stage %d: '%s' - Failed with error: %d.\n", stage, LiGetFormattedStageName(stage), err, failingPorts);
+	const char* name = LiGetFormattedStageName(stage);
+	sprintf(message, "Stage %d: '%s' - Failed with error: %d. Ports: %s\n", stage, name ? name : "Unknown", err, failingPorts);
Pages/HostSelectorPage.xaml.cpp (1)

347-347: ⚠️ Potential issue | 🟡 Minor

Catch std::exception by const reference, not by value.

Catching by value slices derived exception types, losing the actual error information from what().

Proposed fix
-	catch (std::exception ex) {
+	catch (const std::exception& ex) {
🤖 Fix all issues with AI agents
In `@App.xaml.cpp`:
- Line 108: Remove the stray trailing character 'F' after the XML doc end tag in
App.xaml.cpp (the line containing "/// </summary>F"); edit the XML documentation
comment so it reads "/// </summary>" with no extra characters to restore
well-formed XML docs for the surrounding method/class documentation.

In `@moonlight-xbox-dx.vcxproj`:
- Line 674: The ErrorText XML element in moonlight-xbox-dx.vcxproj contains
mojibake ("ù", "è"); open the project file, correct the string in the
<ErrorText> tag to use the proper Italian characters ("più", "è") and then save
the file with UTF-8 encoding (ensure the XML declaration or your editor
specifies UTF-8 or include a UTF-8 BOM if your tooling requires it) so the
characters persist; verify by re-opening the file and checking the <ErrorText>
value reads "Questo progetto fa riferimento a uno o più pacchetti NuGet che non
sono presenti in questo computer. Usare lo strumento di ripristino dei pacchetti
NuGet per scaricarli. Per altre informazioni, vedere
http://go.microsoft.com/fwlink/?LinkID=322105. Il file mancante è {0}."

In `@Pages/StreamPage.xaml`:
- Line 77: The TextBlock named StepText uses an invalid FontSize string
("12px"); change the FontSize attribute on StepText to a numeric value without
the "px" suffix (e.g., FontSize="12") so UWP XAML parses correctly and uses
device-independent pixels.

In `@setup-dev.ps1`:
- Around line 58-65: The call to vswhere piped through ConvertFrom-Json returns
an array, so accessing $vsJson.installationPath and
$vsJson.catalog.productLineVersion can be null; update the logic that reads
$vsJson to index the first element (or select the first object) before using its
properties (e.g. replace direct uses of $vsJson.installationPath and
$vsJson.catalog.productLineVersion with references into the first array element
of the ConvertFrom-Json result) so you reliably obtain installationPath and
productLineVersion from the first returned installation.
- Around line 242-244: The recursive Get-ChildItem may find a third-party
.vcxproj; update the Try-Step "Updating .vcxproj with certificate info" to
target the known project filename or constrain recursion. Replace the generic
Get-ChildItem -Recurse -Filter *.vcxproj with a search for the specific file
(e.g. Get-ChildItem -Recurse -Filter "moonlight-xbox-dx.vcxproj" -File |
Select-Object -First 1) or limit depth (use -Depth 1/2 if available) before
assigning $proj and keep the existing null check.
- Around line 134-137: Detect-VS can return $null causing $CMakeGenerator to be
empty and later cmake calls to run with -G ""; update the script to bail out or
fail fast when no generator was detected by checking $CMakeGenerator (the
variable set from Detect-VS) before any cmake-dependent steps (e.g., inside
Try-Step "Configuring moonlight-common-c" and other Step 5 cmake blocks) and
throw or exit with a clear error like "No CMake generator available" so
subsequent cmake invocations are not reached with an empty -G argument.
- Around line 226-276: The $cert created in Try-Step "Generating UWP signing
certificate" is defined in a child scope and becomes null in Try-Step "Updating
.vcxproj with certificate info"; change the variable to script scope by
assigning it as $script:cert where New-SelfSignedCertificate is called (in the
"Generating UWP signing certificate" Try-Step) and update references in the
"Updating .vcxproj with certificate info" Try-Step to use $script:cert (e.g.,
$script:cert.Thumbprint), or alternatively merge the two Try-Step scriptblocks
so $cert remains in scope.

In `@State/MoonlightClient.cpp`:
- Around line 366-371: The function connection_status_update calls
LiGetFormattedStageName(status) twice and passes its return directly into
sprintf %s, which risks a nullptr dereference and wastes the previously captured
stageName; fix by reusing the captured stageName variable, check if stageName is
nullptr and substitute a safe placeholder (e.g. "<unknown>") before calling
sprintf/Utils::Log, and prefer a bounded/safer formatting function or ensure the
buffer size is respected when writing the message.
- Around line 373-376: The function connection_status_completed calls
LiGetFormattedStageName(status) and passes its return directly into sprintf
which risks dereferencing a nullptr (and possible overflow); update
connection_status_completed to first capture the return of
LiGetFormattedStageName into a local pointer, check it for nullptr and
substitute a safe fallback string (e.g. "<unknown>") if null, and use a bounded
formatting call (e.g. snprintf) or ensure the buffer size is respected before
calling Utils::Log so neither a null pointer nor buffer overflow can occur.

In `@Streaming/moonlight_xbox_dxMain.cpp`:
- Around line 20-23: The include for the C++/CX XAML header
Common/ModalDialog.xaml.h must be moved out of the extern "C" block because it
declares ref types/classes that cannot have C linkage; update the top of the
file so extern "C" only wraps the C header Limelight.h (keep `#include`
<Limelight.h> inside extern "C") and place `#include` <Common/ModalDialog.xaml.h>
outside/after the extern "C" block to restore correct C++/CX linkage for the
ModalDialog types.
- Around line 49-50: The continuations attached to
concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(...))
call t.get() without handling thrown exceptions; wrap the body of each lambda
continuation (the lambda capturing self, showLogsDialog and the other one at
lines ~82–83) in a try/catch that catches std::exception (or
Platform::Exception^ if appropriate for the project), log or handle the error
consistently with the surrounding codebase, and avoid letting the exception
escape the continuation (e.g., early-return after logging) so that cancelled or
failed dialogs do not produce unobserved task exceptions.
- Around line 115-131: The DISPATCH_UI lambda inside client->OnCompleted
currently calls Sleep(500), which blocks the UI thread; remove the Sleep(500)
call and replace it with a non-blocking delay (e.g., start a DispatcherTimer or
use a dispatch-with-delay) before setting streamPage->m_progressRing->IsActive =
false and streamPage->m_progressView->Visibility = Collapsed; locate the lambda
referenced by DISPATCH_UI in the OnCompleted handler (the closure that accesses
streamPage and modifies m_progressRing/m_progressView) and implement a
DispatcherTimer or equivalent delayed dispatch to perform the UI updates without
blocking.
- Around line 682-684: Replace the unsafe `[&]` lambda capture used in the
DISPATCH_UI calls inside ToggleLogs and ToggleStats with an explicit `[this]`
capture so member pointers remain valid when the lambda runs asynchronously;
update the lambdas that reference m_LogRenderer and m_statsTextRenderer (inside
DISPATCH_UI) to capture `this` instead of `[&]` in the moonlight_xbox_dxMain
methods ToggleLogs/ToggleStats to avoid dangling references if the object is
destroyed before the dispatched lambda executes.
- Around line 67-72: The loop comment says "last 24 lines" but the condition
(int)lines.size() - i < 6 actually picks the last 5 lines; update either the
comment or the filter: if you want the last 24 lines change the numeric literal
in the condition (used in the loop that appends to m_text) from 6 to 25 (or
otherwise use a named constant like LAST_N_LINES = 24 and adjust the check to
(int)lines.size() - i <= LAST_N_LINES), or if the intention was to keep 5 lines,
change the comment to match the code.

In `@Streaming/VideoRenderer.cpp`:
- Around line 98-100: Change the render guard so rendering only proceeds when
loading is both complete and successful: replace the current conjunction check
that uses m_loadingComplete and m_loadingSuccessful (in the render path inside
VideoRenderer::Render or the function containing that if) with a condition that
returns early unless m_loadingComplete.load(...) &&
m_loadingSuccessful.load(...); if you intentionally want to keep spinning after
a failed StartStreaming (where m_loadingComplete=true and
m_loadingSuccessful=false) to let IsConnectionTerminated() break the loop,
instead add a clarifying comment near the guard explaining that behavior.
🧹 Nitpick comments (6)
moonlight-xbox-dx.vcxproj (1)

139-142: Hardcoded certificate password in project file.

The plaintext password moonlight is committed to source control. While this is a local development signing certificate (generated by setup-dev.ps1) and not a production secret, committing it to a public repo means anyone can sign packages with this identity. If this is intentional for dev convenience, consider adding a brief comment in the file or README clarifying that the cert is for local development only.

setup-dev.ps1 (2)

226-238: No idempotency — re-running creates duplicate certificates.

Each run of the script creates a new self-signed certificate in Cert:\CurrentUser\My without checking for or removing a prior one. Over time this clutters the cert store. Consider removing an existing cert with the same subject before creating a new one.

Proposed fix
 Try-Step "Generating UWP signing certificate" {
+    # Remove any existing cert with the same subject to keep the store clean
+    Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Subject -eq $CertSubject } | Remove-Item -Force -ErrorAction SilentlyContinue
+
     $script:cert = New-SelfSignedCertificate `
         -Type CodeSigningCert `
         -Subject $CertSubject `
         -CertStoreLocation "Cert:\CurrentUser\My"

16-18: Certificate password is hardcoded in the script.

$CertPassword = "moonlight" is fine for a local dev signing cert, but it's worth adding a brief comment explaining this is intentionally a non-secret value for local development. This matches the same password committed in the .vcxproj at line 142.

State/MoonlightClient.cpp (1)

292-300: Good addition — surfacing gs_error details to the caller via OnFailed.

One minor note: consider using snprintf instead of sprintf for the errorMessage buffer to guard against an unexpectedly long gs_error string.

pch.h (1)

238-252: thread_local return pattern is fragile — returned c_str() is invalidated by the next call on the same thread.

In the current callers (immediate sprintf), this works fine. But line 369 in MoonlightClient.cpp calls LiGetFormattedStageName twice in the same expression — once storing the result in an unused stageName, then again inline in sprintf. If a single expression ever needs two different stage names, the second call would overwrite the first. Worth a brief comment on the function documenting this constraint.

Streaming/VideoRenderer.h (1)

79-79: Nit: inconsistent indentation — spaces vs. tabs.

Line 79 uses spaces while the surrounding lines (e.g., Line 78) use tabs.

<muxc:ProgressRing x:Name="MainProgressRing" Width="64" Height="64" IsActive="True" Margin="0,0,0,16" />
<TextBlock x:Name="StatusText" Text="Initializing Moonlight..."/>
<TextBlock x:Name="StatusText" TextAlignment="Center" Text="Initializing Moonlight..."/>
<TextBlock x:Name="StepText" FontSize="12px" Foreground="DimGray" TextAlignment="Center" Text="" Margin="0,16,0,0" />
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

UWP XAML FontSize "px" suffix support documentation

💡 Result:

UWP XAML does not support a "px" suffix for FontSize (or other double-typed size properties). In UWP, FontSize is a double value expressed in pixels (effective/device-independent pixels in practice), and XAML attribute values for primitives are parsed from plain numeric strings (so "12" works; "12px" won’t). [1][2]

Use:

<TextBlock FontSize="12" />

Related documentation:

  • UWP/WinUI API docs show FontSize as double and “measured in pixels” (example: FontIconSource.FontSize). [1]
  • UWP XAML syntax docs describe primitive properties (like double) being set via attribute syntax using the built-in string-to-primitive conversion (no unit suffixes described). [2]

Sources

  1. Microsoft Learn — FontIconSource.FontSize (UWP): double, measured in pixels. (learn.microsoft.com)
  2. Microsoft Learn — UWP “XAML syntax guide” (primitive types like double use built-in parser conversion from strings). (learn.microsoft.com)

Citations:


FontSize="12px" — UWP XAML does not support the px suffix; use FontSize="12" instead.

In UWP XAML, FontSize expects a bare numeric value (device-independent pixels). The "12px" string will cause a XAML parse error at runtime.

Proposed fix
-                <TextBlock x:Name="StepText" FontSize="12px" Foreground="DimGray" TextAlignment="Center" Text="" Margin="0,16,0,0" />
+                <TextBlock x:Name="StepText" FontSize="12" Foreground="DimGray" TextAlignment="Center" Text="" Margin="0,16,0,0" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TextBlock x:Name="StepText" FontSize="12px" Foreground="DimGray" TextAlignment="Center" Text="" Margin="0,16,0,0" />
<TextBlock x:Name="StepText" FontSize="12" Foreground="DimGray" TextAlignment="Center" Text="" Margin="0,16,0,0" />
🤖 Prompt for AI Agents
In `@Pages/StreamPage.xaml` at line 77, The TextBlock named StepText uses an
invalid FontSize string ("12px"); change the FontSize attribute on StepText to a
numeric value without the "px" suffix (e.g., FontSize="12") so UWP XAML parses
correctly and uses device-independent pixels.

setup-dev.ps1 Outdated
Comment on lines +58 to +65
$vsJson = & $vswhere -latest -format json | ConvertFrom-Json
if (-not $vsJson) {
Record-Failure "No Visual Studio installation detected"
return $null
}

$installPath = $vsJson.installationPath
$version = $vsJson.catalog.productLineVersion
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.

⚠️ Potential issue | 🟠 Major

vswhere -latest -format json returns a JSON array — index into it.

ConvertFrom-Json on an array yields a PowerShell array, so $vsJson.installationPath may return $null or behave unexpectedly. Index into the first element:

Proposed fix
-    $vsJson = & $vswhere -latest -format json | ConvertFrom-Json
-    if (-not $vsJson) {
+    $vsAll = & $vswhere -latest -format json | ConvertFrom-Json
+    if (-not $vsAll) {
         Record-Failure "No Visual Studio installation detected"
         return $null
     }
+    $vsJson = $vsAll[0]
 
     $installPath = $vsJson.installationPath
     $version     = $vsJson.catalog.productLineVersion
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$vsJson = & $vswhere -latest -format json | ConvertFrom-Json
if (-not $vsJson) {
Record-Failure "No Visual Studio installation detected"
return $null
}
$installPath = $vsJson.installationPath
$version = $vsJson.catalog.productLineVersion
$vsAll = & $vswhere -latest -format json | ConvertFrom-Json
if (-not $vsAll) {
Record-Failure "No Visual Studio installation detected"
return $null
}
$vsJson = $vsAll[0]
$installPath = $vsJson.installationPath
$version = $vsJson.catalog.productLineVersion
🤖 Prompt for AI Agents
In `@setup-dev.ps1` around lines 58 - 65, The call to vswhere piped through
ConvertFrom-Json returns an array, so accessing $vsJson.installationPath and
$vsJson.catalog.productLineVersion can be null; update the logic that reads
$vsJson to index the first element (or select the first object) before using its
properties (e.g. replace direct uses of $vsJson.installationPath and
$vsJson.catalog.productLineVersion with references into the first array element
of the ConvertFrom-Json result) so you reliably obtain installationPath and
productLineVersion from the first returned installation.

setup-dev.ps1 Outdated
Comment on lines +134 to +137
$CMakeGenerator = Detect-VS
if ($CMakeGenerator) {
Write-Host "Using CMake generator: $CMakeGenerator" -ForegroundColor Yellow
}
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.

⚠️ Potential issue | 🟠 Major

Script continues with $null generator after VS detection failure.

If Detect-VS returns $null (e.g., missing VS or missing components), $CMakeGenerator will be $null and the cmake invocations in Step 5 will receive -G "", which will produce confusing cmake errors. Consider bailing out early or recording a clear failure before reaching the cmake steps.

Proposed fix — guard before cmake steps
 $CMakeGenerator = Detect-VS
 if ($CMakeGenerator) {
     Write-Host "Using CMake generator: $CMakeGenerator" -ForegroundColor Yellow
 }
+else {
+    Record-Failure "Cannot proceed without a valid CMake generator (Visual Studio not detected)"
+}

Then, in Steps 5 (and any cmake-dependent step), guard on $CMakeGenerator:

Try-Step "Configuring moonlight-common-c" {
    if (-not $CMakeGenerator) { throw "No CMake generator available — skipping" }
    # ... cmake invocation ...
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$CMakeGenerator = Detect-VS
if ($CMakeGenerator) {
Write-Host "Using CMake generator: $CMakeGenerator" -ForegroundColor Yellow
}
$CMakeGenerator = Detect-VS
if ($CMakeGenerator) {
Write-Host "Using CMake generator: $CMakeGenerator" -ForegroundColor Yellow
}
else {
Record-Failure "Cannot proceed without a valid CMake generator (Visual Studio not detected)"
}
🤖 Prompt for AI Agents
In `@setup-dev.ps1` around lines 134 - 137, Detect-VS can return $null causing
$CMakeGenerator to be empty and later cmake calls to run with -G ""; update the
script to bail out or fail fast when no generator was detected by checking
$CMakeGenerator (the variable set from Detect-VS) before any cmake-dependent
steps (e.g., inside Try-Step "Configuring moonlight-common-c" and other Step 5
cmake blocks) and throw or exit with a clear error like "No CMake generator
available" so subsequent cmake invocations are not reached with an empty -G
argument.

Comment on lines +49 to +50
concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog1)).then([self, showLogsDialog](concurrency::task<Windows::UI::Xaml::Controls::ContentDialogResult> t) {
auto result = t.get();
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.

⚠️ Potential issue | 🟠 Major

Unhandled exception from t.get() in .then() continuations.

Both ShowOnceAsync continuations call t.get() without a try/catch. If the dialog is cancelled or fails, t.get() throws and the exception propagates as an unobserved task exception. Wrap these in try/catch to match the defensive error handling approach used throughout this PR.

Proposed fix (apply to both continuations)
 concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog1)).then([self, showLogsDialog](concurrency::task<Windows::UI::Xaml::Controls::ContentDialogResult> t) {
-    auto result = t.get();
+    Windows::UI::Xaml::Controls::ContentDialogResult result;
+    try { result = t.get(); } catch (...) {
+        self->StopRenderLoop();
+        self->ExitStreamPage();
+        return;
+    }
     if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Primary) {

Also applies to: 82-83

🤖 Prompt for AI Agents
In `@Streaming/moonlight_xbox_dxMain.cpp` around lines 49 - 50, The continuations
attached to
concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(...))
call t.get() without handling thrown exceptions; wrap the body of each lambda
continuation (the lambda capturing self, showLogsDialog and the other one at
lines ~82–83) in a try/catch that catches std::exception (or
Platform::Exception^ if appropriate for the project), log or handle the error
consistently with the surrounding codebase, and avoid letting the exception
escape the continuation (e.g., early-return after logging) so that cancelled or
failed dialogs do not produce unobserved task exceptions.

Comment on lines +67 to +72
for (int i = 0; i < (int)lines.size(); i++) {
// Get only the last 24 lines
if ((int)lines.size() - i < 6) {
m_text += lines[i];
}
}
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.

⚠️ Potential issue | 🟡 Minor

Comment says "last 24 lines" but code filters to last 5.

The condition (int)lines.size() - i < 6 selects entries where i > size - 6, which yields the last 5 lines, not 24.

Fix either the comment or the filter
-                // Get only the last 24 lines
-                if ((int)lines.size() - i < 6) {
+                // Get only the last 24 lines
+                if ((int)lines.size() - i < 25) {

Or update the comment to match the code:

-                // Get only the last 24 lines
-                if ((int)lines.size() - i < 6) {
+                // Get only the last 5 lines
+                if ((int)lines.size() - i < 6) {
🤖 Prompt for AI Agents
In `@Streaming/moonlight_xbox_dxMain.cpp` around lines 67 - 72, The loop comment
says "last 24 lines" but the condition (int)lines.size() - i < 6 actually picks
the last 5 lines; update either the comment or the filter: if you want the last
24 lines change the numeric literal in the condition (used in the loop that
appends to m_text) from 6 to 25 (or otherwise use a named constant like
LAST_N_LINES = 24 and adjust the check to (int)lines.size() - i <=
LAST_N_LINES), or if the intention was to keep 5 lines, change the comment to
match the code.

Comment on lines +115 to +131
client->OnCompleted = ([this, streamPage, configuration]() {
concurrency::create_task([this]() {
while (this->m_sceneRenderer && !this->m_sceneRenderer->IsLoadingComplete() && !this->moonlightClient->IsConnectionTerminated()) {
Sleep(50);
}
}).then([this, streamPage, configuration](concurrency::task<void> t) {

if (this->m_sceneRenderer && this->m_sceneRenderer->IsLoadingSuccessful()) {
DISPATCH_UI(([streamPage]() {
Sleep(500);
streamPage->m_progressRing->IsActive = false;
streamPage->m_progressView->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
}));
}

});
});
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.

⚠️ Potential issue | 🟠 Major

Sleep(500) blocks the UI thread.

Line 124 calls Sleep(500) inside a DISPATCH_UI lambda, which freezes the UI for half a second. Use a timer or dispatch-delayed pattern instead, or simply remove the sleep if it's not strictly needed.

Proposed fix — remove the sleep
 			if (this->m_sceneRenderer && this->m_sceneRenderer->IsLoadingSuccessful()) {
 				DISPATCH_UI(([streamPage]() {
-					Sleep(500);
 					streamPage->m_progressRing->IsActive = false;
 					streamPage->m_progressView->Visibility = Windows::UI::Xaml::Visibility::Collapsed;
 				}));
🤖 Prompt for AI Agents
In `@Streaming/moonlight_xbox_dxMain.cpp` around lines 115 - 131, The DISPATCH_UI
lambda inside client->OnCompleted currently calls Sleep(500), which blocks the
UI thread; remove the Sleep(500) call and replace it with a non-blocking delay
(e.g., start a DispatcherTimer or use a dispatch-with-delay) before setting
streamPage->m_progressRing->IsActive = false and
streamPage->m_progressView->Visibility = Collapsed; locate the lambda referenced
by DISPATCH_UI in the OnCompleted handler (the closure that accesses streamPage
and modifies m_progressRing/m_progressView) and implement a DispatcherTimer or
equivalent delayed dispatch to perform the UI updates without blocking.

Comment on lines +682 to 684
DISPATCH_UI([&] {
m_LogRenderer->ToggleVisible();
});
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.

⚠️ Potential issue | 🟠 Major

[&] capture in DISPATCH_UI is dangerous if the lambda outlives the caller.

ToggleLogs and ToggleStats use [&] capture in DISPATCH_UI, which dispatches asynchronously to the UI thread. If moonlight_xbox_dxMain is destroyed before the dispatched lambda runs, the captured references are dangling. Use [this] instead — m_LogRenderer and m_statsTextRenderer are members, so this capture is sufficient and explicit.

Proposed fix
-	DISPATCH_UI([&] {
+	DISPATCH_UI([this] {
 		m_LogRenderer->ToggleVisible();
 	});
-	DISPATCH_UI([&] {
+	DISPATCH_UI([this] {
 		m_statsTextRenderer->ToggleVisible();
 	});

Also applies to: 692-694

🤖 Prompt for AI Agents
In `@Streaming/moonlight_xbox_dxMain.cpp` around lines 682 - 684, Replace the
unsafe `[&]` lambda capture used in the DISPATCH_UI calls inside ToggleLogs and
ToggleStats with an explicit `[this]` capture so member pointers remain valid
when the lambda runs asynchronously; update the lambdas that reference
m_LogRenderer and m_statsTextRenderer (inside DISPATCH_UI) to capture `this`
instead of `[&]` in the moonlight_xbox_dxMain methods ToggleLogs/ToggleStats to
avoid dangling references if the object is destroyed before the dispatched
lambda executes.

Comment on lines +98 to 100
if (!m_loadingComplete.load(std::memory_order_acquire) && !m_loadingSuccessful.load(std::memory_order_acquire)) {
return true;
}
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.

⚠️ Potential issue | 🟠 Major

Render guard condition allows rendering after a failed load.

The condition !m_loadingComplete && !m_loadingSuccessful skips rendering only when both are false. After a failed StartStreaming (m_loadingComplete=true, m_loadingSuccessful=false), the guard falls through and attempts to render with no valid stream. While this likely won't crash (null texture check at Line 114 returns false), the intent seems clearer as an OR:

-	if (!m_loadingComplete.load(std::memory_order_acquire) && !m_loadingSuccessful.load(std::memory_order_acquire)) {
+	if (!m_loadingComplete.load(std::memory_order_acquire) || !m_loadingSuccessful.load(std::memory_order_acquire)) {

This way, rendering only proceeds when loading is both complete and successful. If the intent is to allow the render loop to continue spinning (so IsConnectionTerminated() can break it), consider adding a comment explaining why the && is deliberate.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!m_loadingComplete.load(std::memory_order_acquire) && !m_loadingSuccessful.load(std::memory_order_acquire)) {
return true;
}
if (!m_loadingComplete.load(std::memory_order_acquire) || !m_loadingSuccessful.load(std::memory_order_acquire)) {
return true;
}
🤖 Prompt for AI Agents
In `@Streaming/VideoRenderer.cpp` around lines 98 - 100, Change the render guard
so rendering only proceeds when loading is both complete and successful: replace
the current conjunction check that uses m_loadingComplete and
m_loadingSuccessful (in the render path inside VideoRenderer::Render or the
function containing that if) with a condition that returns early unless
m_loadingComplete.load(...) && m_loadingSuccessful.load(...); if you
intentionally want to keep spinning after a failed StartStreaming (where
m_loadingComplete=true and m_loadingSuccessful=false) to let
IsConnectionTerminated() break the loop, instead add a clarifying comment near
the guard explaining that behavior.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@Streaming/moonlight_xbox_dxMain.cpp`:
- Around line 633-639: The catch-all handler for
rootFrame->Navigate(HostSelectorPage::typeid) currently nukes the UI by setting
rootFrame->Content = nullptr and logs only a generic message; instead, catch the
exception, log the actual exception details via Utils::Log (include what failed
and exception info) and perform a graceful recovery: attempt an alternative
recovery path such as calling Window::Current->Close() or navigating to a
lightweight fallback page rather than clearing rootFrame->Content; update the
catch block around the Navigate call in the ExitStreamPage flow to implement
these changes (referencing rootFrame, Navigate, HostSelectorPage, Utils::Log and
Window::Current->Close()).
- Around line 67-70: The for-loop that intends to show the "last 8 lines" uses
the condition (int)lines.size() - i < 8 which actually selects 7 lines; update
the condition in the loop that iterates over lines (the block containing the
comment and the if using lines.size() - i) to use <= 8 (or < 9) so the last 8
lines are included, and ensure the inline comment matches the corrected
condition.
- Around line 38-97: The two std::shared_ptr lambdas showErrorDialog and
showLogsDialog form a reference cycle and leak; break it by making one direction
a std::weak_ptr (e.g., declare showLogsDialog as std::shared_ptr but capture a
std::weak_ptr to it inside the showErrorDialog lambda, or vice versa) and lock()
it before invoking, or alternatively call reset() on the other shared_ptr
immediately after you invoke it in each branch; update the captures in the
enclosing DISPATCH_UI lambda to use the weak_ptr and adjust invocation sites
(the closures referenced by showErrorDialog and showLogsDialog) to lock() and
check for emptiness before calling to avoid the cycle.

Comment on lines +38 to +97
DISPATCH_UI(([msgCopy, self, appName]() {
auto showErrorDialog = std::make_shared<std::function<void()>>();
auto showLogsDialog = std::make_shared<std::function<void()>>();

*showErrorDialog = [self, msgCopy, showLogsDialog, appName]() {
auto dialog1 = ref new Windows::UI::Xaml::Controls::ContentDialog();
dialog1->Title = L"Failed to start " + appName;
dialog1->Content = Utils::StringFromStdString(msgCopy);
dialog1->PrimaryButtonText = L"OK";
dialog1->SecondaryButtonText = L"Show Logs";

concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog1)).then([self, showLogsDialog](concurrency::task<Windows::UI::Xaml::Controls::ContentDialogResult> t) {
auto result = t.get();
if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Primary) {
self->StopRenderLoop();
self->ExitStreamPage();
}
else if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Secondary) {
(*showLogsDialog)();
}
});
};

*showLogsDialog = [self, showErrorDialog]() {
auto dialog2 = ref new Windows::UI::Xaml::Controls::ContentDialog();

std::wstring m_text = L"";
std::vector<std::wstring> lines = Utils::GetLogLines();

for (int i = 0; i < (int)lines.size(); i++) {
// Get only the last 8 lines
// More than that cannot be fully viewed on the screen at the current scaling
if ((int)lines.size() - i < 8) {
m_text += lines[i];
}
}

Utils::showLogs = true;

dialog2->MaxWidth = 600;
dialog2->Title = "Logs";
dialog2->Content = ref new Platform::String(m_text.c_str());
dialog2->PrimaryButtonText = L"OK";
dialog2->SecondaryButtonText = L"Show Error";

concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog2)).then([self, showErrorDialog](concurrency::task<Windows::UI::Xaml::Controls::ContentDialogResult> t) {
auto result = t.get();
if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Primary) {
self->StopRenderLoop();
self->ExitStreamPage();
}
else if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Secondary) {
(*showErrorDialog)();
}
});
};

// Start by showing the error dialog
(*showErrorDialog)();
}));
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.

⚠️ Potential issue | 🟠 Major

Circular shared_ptr reference between showErrorDialog and showLogsDialog leaks memory.

showErrorDialog captures showLogsDialog (line 42) and showLogsDialog captures showErrorDialog (line 61). Since both are std::shared_ptr, neither ref count reaches zero — they keep each other alive indefinitely after the enclosing lambda returns.

Break the cycle by resetting the other pointer inside each branch once it's no longer needed, or by using a weak_ptr for one direction.

Proposed fix — reset after use to break the cycle
 *showErrorDialog = [self, msgCopy, showLogsDialog, appName]() {
     // ...
     concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog1)).then([self, showLogsDialog](concurrency::task<Windows::UI::Xaml::Controls::ContentDialogResult> t) {
         auto result = t.get();
         if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Primary) {
             self->StopRenderLoop();
             self->ExitStreamPage();
         }
         else if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Secondary) {
             (*showLogsDialog)();
         }
     });
 };

-*showLogsDialog = [self, showErrorDialog]() {
+auto weakError = std::weak_ptr<std::function<void()>>(showErrorDialog);
+*showLogsDialog = [self, weakError]() {
     // ...
-    concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog2)).then([self, showErrorDialog](concurrency::task<Windows::UI::Xaml::Controls::ContentDialogResult> t) {
+    concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(dialog2)).then([self, weakError](concurrency::task<Windows::UI::Xaml::Controls::ContentDialogResult> t) {
         auto result = t.get();
         if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Primary) {
             self->StopRenderLoop();
             self->ExitStreamPage();
         }
         else if (result == Windows::UI::Xaml::Controls::ContentDialogResult::Secondary) {
-            (*showErrorDialog)();
+            if (auto err = weakError.lock()) (*err)();
         }
     });
 };
🤖 Prompt for AI Agents
In `@Streaming/moonlight_xbox_dxMain.cpp` around lines 38 - 97, The two
std::shared_ptr lambdas showErrorDialog and showLogsDialog form a reference
cycle and leak; break it by making one direction a std::weak_ptr (e.g., declare
showLogsDialog as std::shared_ptr but capture a std::weak_ptr to it inside the
showErrorDialog lambda, or vice versa) and lock() it before invoking, or
alternatively call reset() on the other shared_ptr immediately after you invoke
it in each branch; update the captures in the enclosing DISPATCH_UI lambda to
use the weak_ptr and adjust invocation sites (the closures referenced by
showErrorDialog and showLogsDialog) to lock() and check for emptiness before
calling to avoid the cycle.

Comment on lines +633 to +639
if (!reachedAppPage) {
try {
rootFrame->Navigate(Windows::UI::Xaml::Interop::TypeName(HostSelectorPage::typeid));
} catch (...) {
rootFrame->Content = nullptr;
Utils::Log("ExitStreamPage: Failed to return to HostSelectorPage\n");
}
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.

⚠️ Potential issue | 🟡 Minor

Fallback to HostSelectorPage with Content = nullptr on failure is harsh.

If Navigate(HostSelectorPage) throws, setting rootFrame->Content = nullptr (line 637) leaves the app with a blank frame and no recovery path. Consider logging the exception details (not just a generic message) and, if possible, attempting Window::Current->Close() or another graceful shutdown rather than blanking the frame.

🧰 Tools
🪛 Cppcheck (2.19.0)

[error] 635-635: syntax error

(syntaxError)

🤖 Prompt for AI Agents
In `@Streaming/moonlight_xbox_dxMain.cpp` around lines 633 - 639, The catch-all
handler for rootFrame->Navigate(HostSelectorPage::typeid) currently nukes the UI
by setting rootFrame->Content = nullptr and logs only a generic message;
instead, catch the exception, log the actual exception details via Utils::Log
(include what failed and exception info) and perform a graceful recovery:
attempt an alternative recovery path such as calling Window::Current->Close() or
navigating to a lightweight fallback page rather than clearing
rootFrame->Content; update the catch block around the Navigate call in the
ExitStreamPage flow to implement these changes (referencing rootFrame, Navigate,
HostSelectorPage, Utils::Log and Window::Current->Close()).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
moonlight-xbox-dx.sln (1)

7-18: ⚠️ Potential issue | 🔴 Critical

Fix stale ProjectDependencies: three GUIDs no longer match declared projects.

The ProjectSection(ProjectDependencies) on lines 8–10 references three GUIDs that don't match any currently declared projects in the solution:

  • Line 8: {842AAF8B-4505-3D3B-AAE5-CFF297F90298} — unmatched
  • Line 9: {873D03B2-B99F-3917-AFDC-9FB954F9962E} — unmatched
  • Line 10: {E025A414-0DA7-3C61-A890-9487ED6E1D91} — unmatched

These are likely remnants from before the project GUIDs were regenerated. Line 11's {F4776924-619C-42C7-88B2-82C947CCC9E7} correctly references DirectXTK (line 20) and should remain.

Since Visual Studio resolves dependencies by GUID, the three stale entries are silently ignored, breaking the intended build order. Remove these three lines, or if they should reference moonlight-common-c, gamestream, and enet, update them to the current GUIDs:

  • moonlight-common-c: {56F17673-A685-3B16-9094-8B11198EF0BC}
  • gamestream: {B089477D-7A5A-3EF1-8139-BC31842134E6}
  • enet: {2CB3B76D-BADC-3BF6-90C9-9D52F4B53A93}
State/MoonlightClient.cpp (1)

396-406: ⚠️ Potential issue | 🟡 Minor

failingPorts is computed but never printed; same nullptr risk with LiGetFormattedStageName.

The format string on line 402 has three specifiers (%d, %s, %d) but four arguments are passed — failingPorts is silently ignored by sprintf. If you intend to log failing ports, add a %s specifier; otherwise, remove the dead code (failingPorts computation + LiStringifyPortFlags call). Also guard LiGetFormattedStageName against nullptr.

Proposed fix (include failing ports)
-	sprintf(message, "Stage %d: '%s' - Failed with error: %d.\n", stage, LiGetFormattedStageName(stage), err, failingPorts);
+	const char* stageName = LiGetFormattedStageName(stage);
+	sprintf(message, "Stage %d: '%s' - Failed with error: %d. Ports: %s\n", stage, stageName ? stageName : "Unknown", err, failingPorts);
🤖 Fix all issues with AI agents
In `@Pages/StreamPage.xaml.cpp`:
- Around line 106-115: The try/catch allows StopRenderLoop() to throw and skips
m_main.reset(), leaking the resource and leaving the "m_main reset" log
incorrect; refactor Page_Unloaded so m_main.reset() always runs regardless of
StopRenderLoop() outcome (e.g., call StopRenderLoop() in its try/catch but move
m_main.reset() into a single post-catch cleanup section or call m_main.reset()
in each catch), preserve logging of exceptions using the existing catch blocks
(Utils::Logf/Utils::Log) and only log "m_main reset" after m_main.reset()
actually executes; update references in the method using StopRenderLoop,
m_main.reset and the catch handlers accordingly.
- Around line 75-95: The catch blocks inside the DISPATCH_UI lambda that
initializes m_main (created via new moonlight_xbox_dxMain and started with
StartRenderLoop) only log errors and leave the UI stuck; update each catch
(std::exception, std::string, Platform::Exception ^, ...) to perform recovery by
calling the existing ExitStreamPage() method (or at minimum hide/stop the
progress view and re-enable back navigation) after logging so the user can leave
the loading screen; ensure you call ExitStreamPage() (or invoke the same cleanup
code used elsewhere to stop the progress ring and restore UI state) and avoid
throwing from the catch handlers.
🧹 Nitpick comments (3)
moonlight-xbox-dx.sln (1)

3-4: Consider team's Visual Studio version support when keeping v18.

Visual Studio Version 18 (VS 2026) exists and is stable as of February 2026 (v18.2 released Jan 20). VS 2022 developers can still open this solution—they'll receive an upgrade prompt but can continue working, and the .sln format itself hasn't changed. If your team primarily uses VS 2022, you may want to keep VisualStudioVersion at 17 to avoid version mismatch prompts for all contributors; otherwise, v18 is acceptable.

Pages/AppPage.xaml.cpp (1)

152-181: New appName derivation looks correct; pre-existing debug artifact on line 179.

The GetAppById usage and fallback to "App" on line 161 are sound. The extended config population (lines 165–173) mirrors the host settings cleanly.

Line 179 (printf("C")) is a pre-existing debug artifact — consider replacing it with a proper Utils::Log call for consistency with the rest of the error-handling improvements in this PR.

Streaming/moonlight_xbox_dxMain.h (1)

32-32: Declaration looks good; implementation in the .cpp has broad catch-alls.

The ExitStreamPage() public method fits well in this class. Looking at the implementation in Streaming/moonlight_xbox_dxMain.cpp (lines 609–643), the GoBack() call on line 623 uses catch (...) which discards the actual error. Consider catching Platform::Exception^ and std::exception& first (as done elsewhere in this PR) to log the specific failure reason — this will help diagnose the navigation issues discussed in the PR comments.

Comment on lines 106 to 115
try {
this->m_main->StopRenderLoop();
this->m_main.reset();
} catch (std::exception &ex) {
Utils::Logf("StreamPage::Page_Unloaded m_main threw an exception: %s\n", ex.what());
} catch (...) {
Utils::Log("StreamPage::Page_Unloaded StopRenderLoop threw an exception\n");
Utils::Log("StreamPage::Page_Unloaded m_main threw an exception\n");
}

this->m_main.reset();
Utils::Log("StreamPage::Page_Unloaded m_main reset\n");
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.

⚠️ Potential issue | 🟡 Minor

m_main.reset() is skipped if StopRenderLoop() throws — resource leak.

If StopRenderLoop() throws, execution jumps to the catch block and m_main.reset() on line 108 is never reached, yet line 115 still logs "m_main reset". Move the reset into a finally-style pattern (or duplicate it in the catch blocks) so the resource is always released.

Proposed fix
 	try {
 		this->m_main->StopRenderLoop();
-		this->m_main.reset();
 	} catch (std::exception &ex) {
 		Utils::Logf("StreamPage::Page_Unloaded m_main threw an exception: %s\n", ex.what());
 	} catch (...) {
 		Utils::Log("StreamPage::Page_Unloaded m_main threw an exception\n");
 	}
+
+	this->m_main.reset();
 
 	Utils::Log("StreamPage::Page_Unloaded m_main reset\n");
🤖 Prompt for AI Agents
In `@Pages/StreamPage.xaml.cpp` around lines 106 - 115, The try/catch allows
StopRenderLoop() to throw and skips m_main.reset(), leaking the resource and
leaving the "m_main reset" log incorrect; refactor Page_Unloaded so
m_main.reset() always runs regardless of StopRenderLoop() outcome (e.g., call
StopRenderLoop() in its try/catch but move m_main.reset() into a single
post-catch cleanup section or call m_main.reset() in each catch), preserve
logging of exceptions using the existing catch blocks (Utils::Logf/Utils::Log)
and only log "m_main reset" after m_main.reset() actually executes; update
references in the method using StopRenderLoop, m_main.reset and the catch
handlers accordingly.

if (lines.size() - i < 24) {
m_text += lines[i];
}
DISPATCH_THREADPOOL(([this, devRes = m_deviceResources, cfg = configuration] {
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.

I specifically pulled this out of being async, why add it back? I haven't tried this yet but will see what happens.

@andygrundman
Copy link
Copy Markdown
Contributor

Does this fix the disconnected hosts after 5 minutes issue? I was thinking, why do we even sit there and spam the servers? It should simply always let me connect to my server, whether it's up or not. Let me try at least, and then fail if it's down.

@andygrundman
Copy link
Copy Markdown
Contributor

My very first try with this branch, and HDR is completely broken. Please remove any changes to the video code. :(

@andygrundman
Copy link
Copy Markdown
Contributor

I already spent way too much time debugging the HDR issues, I don't want to do it again. Here's a log if you want to compare. The reason the HDR section is missing in yours is because the async change you made causes things to happen out of order, it needs to wait for StartStreaming to finish.

Good:

[00:39.858] Received first audio packet after 200 ms
[00:39.858] Initial audio resync period: 500 milliseconds
[00:39.988] SetDisplayHDR(): current display mode:     3840x2160 @ 119.88hz, 30 bpp, RgbLimited, Rgb444
[00:39.988] Supported display modes:
[00:39.989]  :     3840x2160 @ 23.98hz, 30 bpp, RgbLimited, Rgb444
[00:39.990]  :     3840x2160 @ 119.88hz, 30 bpp, RgbLimited, Rgb444
[00:39.990]  :     3840x2160 @ 50.00hz, 30 bpp, BT709, Ycc420
[00:39.991]  :     3840x2160 @ 59.94hz, 30 bpp, BT709, Ycc420
[00:39.991]  :     1920x1080 @ 23.98hz, 30 bpp, RgbLimited, Rgb444
[00:39.992]  :     1280x720 @ 50.00hz, 30 bpp, RgbLimited, Rgb444
[00:39.992]  :     1920x1080 @ 50.00hz, 30 bpp, RgbLimited, Rgb444
[00:39.993]  : HDR 3840x2160 @ 23.98hz, 30 bpp, BT2020, Rgb444
[00:39.993]  :     3840x2160 @ 23.98hz, 30 bpp, BT709, Ycc444
[00:39.994]  : HDR 3840x2160 @ 119.88hz, 30 bpp, BT2020, Rgb444
[00:39.994]  :     3840x2160 @ 119.88hz, 24 bpp, BT709, Ycc422
[00:39.995]  :     3840x2160 @ 119.88hz, 24 bpp, BT2020, Ycc422
[00:39.995]  :     3840x2160 @ 50.00hz, 24 bpp, BT709, Ycc422
[00:39.996]  : HDR 3840x2160 @ 50.00hz, 24 bpp, BT2020, Ycc422
[00:39.996]  :     3840x2160 @ 59.94hz, 24 bpp, BT709, Ycc422
[00:39.997]  : HDR 3840x2160 @ 59.94hz, 24 bpp, BT2020, Ycc422
[00:40.000] SetDisplayHDR(true): switching to HDR mode: HDR 3840x2160 @ 119.88hz, 30 bpp, BT2020, Rgb444
[00:40.000] Sending HDR10 metadata: Min/MaxLuminance 0 / 800
[00:40.158] Received first video packet after 500 ms
[00:40.160] IDR frame received
[00:40.781] SetDisplayHDR(): successfully changed to: HDR 3840x2160 @ 119.88hz, 30 bpp, BT2020, Rgb444
 codec=HEVC 10 bit 
CVideoDecoderContext::Initialize enter(00000249FB87C010): pDesc = 0000004BCBEFDC50
CVideoDecoderContext::CreateBuffers: (00000249FB87C010)  (width=3840 height=2176)
CVideoDecoderContext::ModifyDPB: (00000249FB87C010) Modify DPB  (newSize=100270080 oldSize=0)
CH265DecoderContext::ProcessFormatChange:  ctx(new=1667072, old=24576)
CVideoDecodeCommandList::_DecodeFrame:  format changed  (Width=3840 Height=2176)
[00:41.424] Setup vertex shader params: uMax 1.000000, vMax 0.992647
[00:41.425] Shader config: Rec. 2020 10-bit limited/standard range, (AVColorSpace 9, AVChromaLocation 1)
[00:41.425] Setup pixel shader params: chromaOffset[0] 0.000130, chromaOffset[1] 0.000000, chromaUVMax[0] 1.000000, chromaUVMax[1] 0.992188
[00:41.427] Colorspace changed to DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020

Broken:

[51:56.821] Received first audio packet after 400 ms
[51:56.821] Initial audio resync period: 500 milliseconds
[51:56.863] Received first video packet after 400 ms
[51:56.864] IDR frame received
 codec=HEVC 10 bit 
CVideoDecoderContext::Initialize enter(0000020DC9CD5FA0): pDesc = 00000059B4EFDE70
CVideoDecoderContext::CreateBuffers: (0000020DC9CD5FA0)  (width=3840 height=2176)
CVideoDecoderContext::ModifyDPB: (0000020DC9CD5FA0) Modify DPB  (newSize=100270080 oldSize=0)
CH265DecoderContext::ProcessFormatChange:  ctx(new=1667072, old=24576)
CVideoDecodeCommandList::_DecodeFrame:  format changed  (Width=3840 Height=2176)
[51:56.895] Setup vertex shader params: uMax 1.000000, vMax 0.992647
[51:56.895] Shader config: Rec. 2020 10-bit limited/standard range, (AVColorSpace 9, AVChromaLocation 1)
[51:56.895] Setup pixel shader params: chromaOffset[0] 0.000130, chromaOffset[1] 0.000000, chromaUVMax[0] 1.000000, chromaUVMax[1] 0.992188
[51:56.896] Colorspace changed to DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020

concurrency::create_task(::moonlight_xbox_dx::ModalDialog::ShowOnceAsync(confirm));
}
else {
if (!success) {
Copy link
Copy Markdown
Contributor Author

@mpotrykus mpotrykus Feb 11, 2026

Choose a reason for hiding this comment

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

Snuck this in. Removed the WoL Success dialog. This just feels silly to have now that we have a visual polling feedback.

@mpotrykus
Copy link
Copy Markdown
Contributor Author

@andygrundman Woops! Looks like the SetHDR got taken out when I merged in master. I added it back with a flag check for safety. Just tested. Sorry about that.

The entire point of the new edits is to marshal the work between UI and backend. StartStreaming is 95% of what's happening, so that was the important one. Most of the other work needs to happen on the UI thread to grab screen information, but StartStreaming can happen on the background to free up the UI thread to make updates. Without it, the UI is in a frozen state until all changes have been made. This was particularly bad on errors, because some of the library calls have cleanup after a return. So everything just hung for ~20 seconds.

I did not WANT to make any of it async, but the only safe way to offload work to another thread without deadlocking was to send the async call and check flags for completion.

So to put it bluntly, we needed to pick an executing thread and send the needed work to the other.

@mpotrykus
Copy link
Copy Markdown
Contributor Author

mpotrykus commented Feb 11, 2026

Does this fix the disconnected hosts after 5 minutes issue? I was thinking, why do we even sit there and spam the servers? It should simply always let me connect to my server, whether it's up or not. Let me try at least, and then fail if it's down.

I'm working on this one next as part of the mdns improvements.

Good thought. I believe this issue is the network utility actually shuts down though. So it's less of a reporting issues and more of a "I'm gone, you can't even use me" scenario. My current theory is the network is being taken down on a "sleep" and not spinning up, but I still have to test.

I'll try this idea out though, to see if we get lucky.

@mpotrykus mpotrykus marked this pull request as draft February 11, 2026 15:40
@mpotrykus
Copy link
Copy Markdown
Contributor Author

mpotrykus commented Feb 11, 2026

With everything this now touches, converting to a draft until all concerns are addressed and there is ample time to test.

@andygrundman
Copy link
Copy Markdown
Contributor

@andygrundman Woops! Looks like the SetHDR got taken out when I merged in master. I added it back with a flag check for safety. Just tested. Sorry about that.

The entire point of the new edits is to marshal the work between UI and backend. StartStreaming is 95% of what's happening, so that was the important one. Most of the other work needs to happen on the UI thread to grab screen information, but StartStreaming can happen on the background to free up the UI thread to make updates. Without it, the UI is in a frozen state until all changes have been made. This was particularly bad on errors, because some of the library calls have cleanup after a return. So everything just hung for ~20 seconds.

I did not WANT to make any of it async, but the only safe way to offload work to another thread without deadlocking was to send the async call and check flags for completion.

So to put it bluntly, we needed to pick an executing thread and send the needed work to the other.

OK, this does fix HDR. I guess I've never ran into a "hung for 20 seconds" issue, probably my network is too good or something.

@mpotrykus
Copy link
Copy Markdown
Contributor Author

Releasing this PR.

Been playing with it for a while with no issues. I also played with a few different ways to marshall threads. This option worked the best in terms of performance and reliability.

@mpotrykus mpotrykus marked this pull request as ready for review February 24, 2026 12:22
Copy link
Copy Markdown
Contributor

@andygrundman andygrundman left a comment

Choose a reason for hiding this comment

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

I've been running this for a while and it's generally fine. There are still the same old mostly rare issues that have been around in all versions, so I'm good with this PR.

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