Make in-stream control panel persistent and readable

This commit is contained in:
Joey Yakimowich-Payne 2026-02-12 01:15:03 -07:00
commit 8a7be971a4
3 changed files with 138 additions and 50 deletions

View file

@ -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:

View file

@ -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<float> m_AudioVolumeScalar;
std::atomic<bool> m_AllowGamepadInput;
std::atomic<bool> m_AllowKeyboardMouseInput;
std::atomic<bool> m_ControlPanelVisible;
std::atomic<int> m_ConnectionStatus;
std::atomic<uint32_t> 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;

View file

@ -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);