From 8a7be971a4007cd72476abc43d9ab635c0a06481 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 12 Feb 2026 01:15:03 -0700 Subject: [PATCH] Make in-stream control panel persistent and readable --- app/streaming/session.cpp | 132 +++++++++++++++++-------- app/streaming/session.h | 5 + app/streaming/video/overlaymanager.cpp | 51 +++++++--- 3 files changed, 138 insertions(+), 50 deletions(-) diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 69bf4d02..57ba29b1 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -178,29 +178,8 @@ void Session::clConnectionStatusUpdate(int connectionStatus) "Connection status update: %d", connectionStatus); - if (!s_ActiveSession->m_Preferences->connectionWarnings) { - return; - } - - if (s_ActiveSession->m_MouseEmulationRefCount > 0) { - // Don't display the overlay if mouse emulation is already using it - return; - } - - switch (connectionStatus) - { - case CONN_STATUS_POOR: - s_ActiveSession->m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); - s_ActiveSession->m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, - s_ActiveSession->m_StreamConfig.bitrate > 5000 ? - "Slow connection to PC\nReduce your bitrate" : "Poor connection to PC"); - s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); - break; - case CONN_STATUS_OKAY: - s_ActiveSession->m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); - s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); - break; - } + s_ActiveSession->m_ConnectionStatus.store(connectionStatus, std::memory_order_relaxed); + s_ActiveSession->refreshControlPanelOverlay(); } void Session::clSetHdrMode(bool enabled) @@ -579,6 +558,8 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_AudioVolumeScalar(1.0f), m_AllowGamepadInput(true), m_AllowKeyboardMouseInput(false), + m_ControlPanelVisible(true), + m_ConnectionStatus(CONN_STATUS_OKAY), m_StatusOverlayGeneration(0), m_QtWindow(nullptr), m_UnexpectedTermination(true), // Failure prior to streaming is unexpected @@ -593,6 +574,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere m_AudioSampleCount(0), m_DropAudioEndTime(0) { + m_TemporaryStatusOverlayText[0] = '\0'; } void Session::setGamepadInputAllowed(bool allowed) @@ -661,6 +643,13 @@ void Session::toggleKeyboardMouseInputAllowed() setKeyboardMouseInputAllowed(!isKeyboardMouseInputAllowed()); } +void Session::toggleControlPanelVisibility() +{ + const bool visible = !m_ControlPanelVisible.load(std::memory_order_relaxed); + m_ControlPanelVisible.store(visible, std::memory_order_relaxed); + showTemporaryStatusOverlay(visible ? "Control panel shown" : "Control panel hidden"); +} + void Session::notifyInputPermissionState() { char buffer[96]; @@ -700,14 +689,80 @@ Uint32 Session::statusOverlayTimeoutCallback(Uint32, void* param) return 0; } -void Session::showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs) +void Session::refreshControlPanelOverlay() { - if (m_MouseEmulationRefCount > 0) { + if (!m_ControlPanelVisible.load(std::memory_order_relaxed)) { + if (m_TemporaryStatusOverlayText[0] != '\0') { + m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, m_TemporaryStatusOverlayText); + m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); + } + else { + m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); + } return; } - m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, text); + char volumeState[24]; + if (m_ManualAudioMuted.load(std::memory_order_relaxed)) { + SDL_snprintf(volumeState, sizeof(volumeState), "MUTED"); + } + else { + const int percent = (int)SDL_roundf(getAudioVolumeScalar() * 100.0f); + SDL_snprintf(volumeState, sizeof(volumeState), "%d%%", percent); + } + + char panelText[640]; + SDL_snprintf(panelText, + sizeof(panelText), + "Stream Controls\n" + "KB/M (Ctrl+Alt+Shift+K): %s\n" + "Pad (Ctrl+Alt+Shift+G): %s\n" + "Vol (U/J, mute N): %s\n" + "UI (Ctrl+Alt+Shift+P / Select+L1+R1+B): ON", + isKeyboardMouseInputAllowed() ? "ON" : "OFF", + isGamepadInputAllowed() ? "ON" : "OFF", + volumeState); + + size_t used = SDL_strlen(panelText); + if (used < sizeof(panelText) && m_MouseEmulationRefCount > 0) { + SDL_snprintf(panelText + used, + sizeof(panelText) - used, + "\nMouse mode: ACTIVE (hold Start to disable)"); + used = SDL_strlen(panelText); + } + + if (used < sizeof(panelText) && + m_Preferences->connectionWarnings && + m_ConnectionStatus.load(std::memory_order_relaxed) == CONN_STATUS_POOR) { + SDL_snprintf(panelText + used, + sizeof(panelText) - used, + "\nNetwork: %s", + m_StreamConfig.bitrate > 5000 ? "Slow (reduce bitrate)" : "Poor"); + used = SDL_strlen(panelText); + } + + if (used < sizeof(panelText) && m_TemporaryStatusOverlayText[0] != '\0') { + SDL_snprintf(panelText + used, + sizeof(panelText) - used, + "\nStatus: %s", + m_TemporaryStatusOverlayText); + } + + m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, panelText); m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); +} + +void Session::showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs) +{ + if (text == nullptr || text[0] == '\0') { + return; + } + + SDL_snprintf(m_TemporaryStatusOverlayText, + sizeof(m_TemporaryStatusOverlayText), + "%s", + text); + refreshControlPanelOverlay(); const uint32_t generation = m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed) + 1; SDL_AddTimer(timeoutMs, statusOverlayTimeoutCallback, (void*)(uintptr_t)generation); @@ -727,6 +782,9 @@ void Session::applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool all if (reason != LI_SESSION_INPUT_POLICY_REASON_STREAM_START) { notifyInputPermissionState(); } + else { + refreshControlPanelOverlay(); + } } void Session::updateEffectiveAudioMuteState() @@ -1709,16 +1767,8 @@ void Session::notifyMouseEmulationMode(bool enabled) m_MouseEmulationRefCount += enabled ? 1 : -1; SDL_assert(m_MouseEmulationRefCount >= 0); - // We re-use the status update overlay for mouse mode notification - if (m_MouseEmulationRefCount > 0) { - m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); - m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate, "Gamepad mouse mode active\nLong press Start to deactivate"); - m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true); - } - else { - m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); - m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); - } + showTemporaryStatusOverlay(enabled ? "Gamepad mouse mode enabled" : "Gamepad mouse mode disabled"); + refreshControlPanelOverlay(); } class AsyncConnectionStartThread : public QThread @@ -1924,7 +1974,10 @@ void Session::start() m_ManualAudioMuted.store(false, std::memory_order_relaxed); m_AudioMuted.store(false, std::memory_order_relaxed); m_AudioVolumeScalar.store(1.0f, std::memory_order_relaxed); + m_ControlPanelVisible.store(true, std::memory_order_relaxed); + m_ConnectionStatus.store(CONN_STATUS_OKAY, std::memory_order_relaxed); m_StatusOverlayGeneration.fetch_add(1, std::memory_order_relaxed); + m_TemporaryStatusOverlayText[0] = '\0'; // Initialize the gamepad code with our preferences // NB: m_InputHandler must be initialize before starting the connection. @@ -2128,6 +2181,7 @@ void Session::exec() // Toggle the stats overlay if requested by the user m_OverlayManager.setOverlayState(Overlay::OverlayDebug, m_Preferences->showPerformanceOverlay); + refreshControlPanelOverlay(); // Switch to async logging mode when we enter the SDL loop StreamUtils::enterAsyncLoggingMode(); @@ -2223,9 +2277,9 @@ void Session::exec() break; } case SDL_CODE_HIDE_STATUS_OVERLAY: - if ((uint32_t)(uintptr_t)event.user.data1 == m_StatusOverlayGeneration.load(std::memory_order_relaxed) && - m_MouseEmulationRefCount == 0) { - m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false); + if ((uint32_t)(uintptr_t)event.user.data1 == m_StatusOverlayGeneration.load(std::memory_order_relaxed)) { + m_TemporaryStatusOverlayText[0] = '\0'; + refreshControlPanelOverlay(); } break; default: diff --git a/app/streaming/session.h b/app/streaming/session.h index 8e5bd35f..13eca494 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -140,6 +140,7 @@ public: void setKeyboardMouseInputAllowed(bool allowed); void toggleGamepadInputAllowed(); void toggleKeyboardMouseInputAllowed(); + void toggleControlPanelVisibility(); void notifyInputPermissionState(); float getAudioVolumeScalar() const @@ -275,6 +276,7 @@ private: static Uint32 statusOverlayTimeoutCallback(Uint32 interval, void* param); + void refreshControlPanelOverlay(); void showTemporaryStatusOverlay(const char* text, Uint32 timeoutMs = 1500); void applyHostInputPolicy(bool allowKeyboard, bool allowMouse, bool allowGamepad, uint8_t reason); void sendInputPermissionStateToHost(uint8_t reason); @@ -297,6 +299,8 @@ private: std::atomic m_AudioVolumeScalar; std::atomic m_AllowGamepadInput; std::atomic m_AllowKeyboardMouseInput; + std::atomic m_ControlPanelVisible; + std::atomic m_ConnectionStatus; std::atomic m_StatusOverlayGeneration; Uint32 m_FullScreenFlag; QQuickWindow* m_QtWindow; @@ -321,6 +325,7 @@ private: OPUS_MULTISTREAM_CONFIGURATION m_OriginalAudioConfig; int m_AudioSampleCount; Uint32 m_DropAudioEndTime; + char m_TemporaryStatusOverlayText[160]; Overlay::OverlayManager m_OverlayManager; diff --git a/app/streaming/video/overlaymanager.cpp b/app/streaming/video/overlaymanager.cpp index 17e5e5c5..77150cc9 100644 --- a/app/streaming/video/overlaymanager.cpp +++ b/app/streaming/video/overlaymanager.cpp @@ -12,8 +12,8 @@ OverlayManager::OverlayManager() : m_Overlays[OverlayType::OverlayDebug].color = {0xD0, 0xD0, 0x00, 0xFF}; m_Overlays[OverlayType::OverlayDebug].fontSize = 20; - m_Overlays[OverlayType::OverlayStatusUpdate].color = {0xCC, 0x00, 0x00, 0xFF}; - m_Overlays[OverlayType::OverlayStatusUpdate].fontSize = 36; + m_Overlays[OverlayType::OverlayStatusUpdate].color = {0xF0, 0xF0, 0xF0, 0xFF}; + m_Overlays[OverlayType::OverlayStatusUpdate].fontSize = 24; // While TTF will usually not be initialized here, it is valid for that not to // be the case, since Session destruction is deferred and could overlap with @@ -146,16 +146,45 @@ void OverlayManager::notifyOverlayUpdated(OverlayType type) } } + SDL_Surface* newSurface = nullptr; + if (m_Overlays[type].enabled) { + // The _Wrapped variant is required for line breaks to work + SDL_Surface* textSurface = TTF_RenderText_Blended_Wrapped(m_Overlays[type].font, + m_Overlays[type].text, + m_Overlays[type].color, + 1024); + if (textSurface != nullptr && type == OverlayStatusUpdate) { + constexpr int kHorizontalPadding = 18; + constexpr int kVerticalPadding = 12; + + SDL_Surface* panelSurface = SDL_CreateRGBSurfaceWithFormat(0, + textSurface->w + (kHorizontalPadding * 2), + textSurface->h + (kVerticalPadding * 2), + 32, + SDL_PIXELFORMAT_ARGB8888); + if (panelSurface != nullptr) { + SDL_FillRect(panelSurface, + nullptr, + SDL_MapRGBA(panelSurface->format, 0x00, 0x00, 0x00, 0xA0)); + + SDL_Rect destination = {kHorizontalPadding, kVerticalPadding, textSurface->w, textSurface->h}; + SDL_SetSurfaceBlendMode(textSurface, SDL_BLENDMODE_BLEND); + SDL_BlitSurface(textSurface, nullptr, panelSurface, &destination); + SDL_FreeSurface(textSurface); + newSurface = panelSurface; + } + else { + newSurface = textSurface; + } + } + else { + newSurface = textSurface; + } + } + // Exchange the old surface with the new one - SDL_Surface* oldSurface = (SDL_Surface*)SDL_AtomicSetPtr( - (void**)&m_Overlays[type].surface, - m_Overlays[type].enabled ? - // The _Wrapped variant is required for line breaks to work - TTF_RenderText_Blended_Wrapped(m_Overlays[type].font, - m_Overlays[type].text, - m_Overlays[type].color, - 1024) - : nullptr); + SDL_Surface* oldSurface = (SDL_Surface*)SDL_AtomicSetPtr((void**)&m_Overlays[type].surface, + newSurface); // Notify the renderer m_Renderer->notifyOverlayUpdated(type);