From 4453e4aef1064803dfc60d8008e93ea47f1dcea6 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 18 Jul 2026 16:38:40 -0500 Subject: [PATCH] Add a renderer selection option for macOS There are numerous conflicting reports about which Mac renderer works best (even on the same systems!). I give up, just let the users figure it out. --- app/backend/systemproperties.cpp | 7 ++++ app/backend/systemproperties.h | 2 + app/gui/SettingsView.qml | 56 +++++++++++++++++++++++++++ app/settings/streamingpreferences.cpp | 4 ++ app/settings/streamingpreferences.h | 14 +++++++ app/streaming/session.cpp | 20 +++++++++- app/streaming/session.h | 1 + app/streaming/video/decoder.h | 1 + app/streaming/video/ffmpeg.cpp | 16 +++++--- app/streaming/video/ffmpeg.h | 2 +- 10 files changed, 116 insertions(+), 7 deletions(-) diff --git a/app/backend/systemproperties.cpp b/app/backend/systemproperties.cpp index 272184a5..739abdb8 100644 --- a/app/backend/systemproperties.cpp +++ b/app/backend/systemproperties.cpp @@ -51,6 +51,13 @@ SystemProperties::SystemProperties() isRunningWayland = WMUtils::isRunningWayland(); isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb"; usesMaterial3Theme = QLibraryInfo::version() >= QVersionNumber(6, 5, 0); + +#ifdef Q_OS_DARWIN + isDarwin = true; +#else + isDarwin = false; +#endif + QString nativeArch = QSysInfo::currentCpuArchitecture(); #ifdef Q_OS_WIN32 diff --git a/app/backend/systemproperties.h b/app/backend/systemproperties.h index b3859d12..8e5b5742 100644 --- a/app/backend/systemproperties.h +++ b/app/backend/systemproperties.h @@ -19,6 +19,7 @@ public: Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT) Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT) Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT) + Q_PROPERTY(bool isDarwin MEMBER isDarwin CONSTANT) Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT) Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT) Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT) @@ -66,6 +67,7 @@ private: bool hasDiscordIntegration; QString versionString; bool usesMaterial3Theme; + bool isDarwin; // Properties only set if startAsyncLoad() is called bool hasHardwareAcceleration; diff --git a/app/gui/SettingsView.qml b/app/gui/SettingsView.qml index c166247e..c119d155 100644 --- a/app/gui/SettingsView.qml +++ b/app/gui/SettingsView.qml @@ -1656,6 +1656,62 @@ Flickable { } } + Label { + width: parent.width + id: rendererTitle + text: qsTr("Renderer") + font.pointSize: 12 + wrapMode: Text.Wrap + visible: SystemProperties.isDarwin + } + + AutoResizingComboBox { + // ignore setting the index at first, and actually set it when the component is loaded + Component.onCompleted: { + var saved_rs = StreamingPreferences.rendererSelection + + // Default to Automatic + currentIndex = 0 + + for(var i = 0; i < rendererListModel.count; i++) { + var el_rs = rendererListModel.get(i).val; + if (saved_rs === el_rs) { + currentIndex = i + break + } + } + + activated(currentIndex) + } + + id: rendererComboBox + visible: SystemProperties.isDarwin + textRole: "text" + model: ListModel { + id: rendererListModel + ListElement { + text: qsTr("Automatic (Recommended)") + val: StreamingPreferences.RS_AUTO + } + ListElement { + text: "Vulkan" + val: StreamingPreferences.RS_VULKAN + } + ListElement { + text: "Metal" + val: StreamingPreferences.RS_METAL + } + ListElement { + text: "AVSampleBufferDisplayLayer" + val: StreamingPreferences.RS_AVSBDL + } + } + // ::onActivated must be used, as it only listens for when the index is changed by a human + onActivated : { + StreamingPreferences.rendererSelection = rendererListModel.get(currentIndex).val + } + } + CheckBox { id: enableYUV444 width: parent.width diff --git a/app/settings/streamingpreferences.cpp b/app/settings/streamingpreferences.cpp index 40c3e782..0c2a6c98 100644 --- a/app/settings/streamingpreferences.cpp +++ b/app/settings/streamingpreferences.cpp @@ -51,6 +51,7 @@ #define SER_CAPTURESYSKEYS "capturesyskeys" #define SER_KEEPAWAKE "keepawake" #define SER_LANGUAGE "language" +#define SER_RENDERER "renderer" #define CURRENT_DEFAULT_VER 2 @@ -159,6 +160,8 @@ void StreamingPreferences::reload() static_cast(VideoCodecConfig::VCC_AUTO)).toInt()); videoDecoderSelection = static_cast(settings.value(SER_VIDEODEC, static_cast(VideoDecoderSelection::VDS_AUTO)).toInt()); + rendererSelection = static_cast(settings.value(SER_RENDERER, + static_cast(RendererSelection::RS_AUTO)).toInt()); windowMode = static_cast(settings.value(SER_WINDOWMODE, // Try to load from the old preference value too static_cast(settings.value(SER_FULLSCREEN, true).toBool() ? @@ -347,6 +350,7 @@ void StreamingPreferences::save() settings.setValue(SER_YUV444, enableYUV444); settings.setValue(SER_VIDEOCFG, static_cast(videoCodecConfig)); settings.setValue(SER_VIDEODEC, static_cast(videoDecoderSelection)); + settings.setValue(SER_RENDERER, static_cast(rendererSelection)); settings.setValue(SER_WINDOWMODE, static_cast(windowMode)); settings.setValue(SER_UIDISPLAYMODE, static_cast(uiDisplayMode)); settings.setValue(SER_LANGUAGE, static_cast(language)); diff --git a/app/settings/streamingpreferences.h b/app/settings/streamingpreferences.h index 2d2d39b6..ad611f6a 100644 --- a/app/settings/streamingpreferences.h +++ b/app/settings/streamingpreferences.h @@ -44,6 +44,17 @@ public: }; Q_ENUM(VideoDecoderSelection) + // Mac only (for now) + enum RendererSelection + { + RS_PROBE_ONLY = -1, // Only valid for probing decoder properties + RS_AUTO, + RS_VULKAN, + RS_METAL, + RS_AVSBDL + }; + Q_ENUM(RendererSelection) + enum WindowMode { WM_FULLSCREEN, @@ -134,6 +145,7 @@ public: Q_PROPERTY(bool enableHdr MEMBER enableHdr NOTIFY enableHdrChanged) Q_PROPERTY(bool enableYUV444 MEMBER enableYUV444 NOTIFY enableYUV444Changed) Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged) + Q_PROPERTY(RendererSelection rendererSelection MEMBER rendererSelection NOTIFY rendererSelectionChanged) Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged) Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT) Q_PROPERTY(UIDisplayMode uiDisplayMode MEMBER uiDisplayMode NOTIFY uiDisplayModeChanged) @@ -187,6 +199,7 @@ public: UIDisplayMode uiDisplayMode; Language language; CaptureSysKeysMode captureSysKeysMode; + RendererSelection rendererSelection; signals: void displayModeChanged(); @@ -224,6 +237,7 @@ signals: void captureSysKeysModeChanged(); void keepAwakeChanged(); void languageChanged(); + void rendererSelectionChanged(); private: explicit StreamingPreferences(QQmlEngine *qmlEngine); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index d934b3de..40c1e59c 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -276,6 +276,7 @@ void Session::clSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlag bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, + StreamingPreferences::RendererSelection renderer, SDL_Window* window, int videoFormat, int width, int height, int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder) { @@ -295,6 +296,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, params.enableFramePacing = enableFramePacing; params.testOnly = testOnly; params.vds = vds; + params.renderer = renderer; SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "V-sync %s", @@ -396,6 +398,7 @@ void Session::getDecoderInfo(SDL_Window* window, // Try an HEVC Main10 decoder first to see if we have HDR support if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE, + StreamingPreferences::RS_PROBE_ONLY, window, VIDEO_FORMAT_H265_MAIN10, 1920, 1080, 60, false, false, true, decoder)) { isHardwareAccelerated = decoder->isHardwareAccelerated(); @@ -409,6 +412,7 @@ void Session::getDecoderInfo(SDL_Window* window, // Try an AV1 Main10 decoder next to see if we have HDR support if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE, + StreamingPreferences::RS_PROBE_ONLY, window, VIDEO_FORMAT_AV1_MAIN10, 1920, 1080, 60, false, false, true, decoder)) { // If we've got a working AV1 Main 10-bit decoder, we'll enable the HDR checkbox @@ -421,9 +425,11 @@ void Session::getDecoderInfo(SDL_Window* window, // If we found no hardware decoders with HDR, check for a renderer // that supports HDR rendering with software decoded frames. if (chooseDecoder(StreamingPreferences::VDS_FORCE_SOFTWARE, + StreamingPreferences::RS_PROBE_ONLY, window, VIDEO_FORMAT_H265_MAIN10, 1920, 1080, 60, false, false, true, decoder) || chooseDecoder(StreamingPreferences::VDS_FORCE_SOFTWARE, + StreamingPreferences::RS_PROBE_ONLY, window, VIDEO_FORMAT_AV1_MAIN10, 1920, 1080, 60, false, false, true, decoder)) { isHdrSupported = decoder->isHdrSupported(); @@ -438,6 +444,7 @@ void Session::getDecoderInfo(SDL_Window* window, // Try a regular hardware accelerated HEVC decoder now if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE, + StreamingPreferences::RS_PROBE_ONLY, window, VIDEO_FORMAT_H265, 1920, 1080, 60, false, false, true, decoder)) { isHardwareAccelerated = decoder->isHardwareAccelerated(); @@ -451,6 +458,7 @@ void Session::getDecoderInfo(SDL_Window* window, #if 0 // See AV1 comment at the top of this function if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE, + StreamingPreferences::RS_PROBE_ONLY, window, VIDEO_FORMAT_AV1_MAIN8, 1920, 1080, 60, false, false, true, decoder)) { isHardwareAccelerated = decoder->isHardwareAccelerated(); @@ -465,6 +473,7 @@ void Session::getDecoderInfo(SDL_Window* window, // If we still didn't find a hardware decoder, try H.264 now. // This will fall back to software decoding, so it should always work. if (chooseDecoder(StreamingPreferences::VDS_AUTO, + StreamingPreferences::RS_PROBE_ONLY, window, VIDEO_FORMAT_H264, 1920, 1080, 60, false, false, true, decoder)) { isHardwareAccelerated = decoder->isHardwareAccelerated(); @@ -486,7 +495,10 @@ Session::getDecoderAvailability(SDL_Window* window, { IVideoDecoder* decoder; - if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, false, false, true, decoder)) { + if (!chooseDecoder(vds, + StreamingPreferences::RS_PROBE_ONLY, + window, videoFormat, width, height, frameRate, + false, false, true, decoder)) { return DecoderAvailability::None; } @@ -501,7 +513,12 @@ bool Session::populateDecoderProperties(SDL_Window* window) { IVideoDecoder* decoder; + // NB: We pass the real renderer selection rather than RS_PROBE_ONLY + // here because this is operating on the real streaming window, and + // instantiating Metal or AVSBDL renderers can interfere with MoltenVK's + // attempt to change the window's colorspace, causing washed out colors. if (!chooseDecoder(m_Preferences->videoDecoderSelection, + m_Preferences->rendererSelection, window, m_SupportedVideoFormats.first(), m_StreamConfig.width, @@ -2192,6 +2209,7 @@ void Session::exec() // Choose a new decoder (hopefully the same one, but possibly // not if a GPU was removed or something). if (!chooseDecoder(m_Preferences->videoDecoderSelection, + m_Preferences->rendererSelection, m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth, m_ActiveVideoHeight, m_ActiveVideoFrameRate, enableVsync, diff --git a/app/streaming/session.h b/app/streaming/session.h index b4bba36d..53ac9e06 100644 --- a/app/streaming/session.h +++ b/app/streaming/session.h @@ -184,6 +184,7 @@ private: static bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, + StreamingPreferences::RendererSelection renderer, SDL_Window* window, int videoFormat, int width, int height, int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, diff --git a/app/streaming/video/decoder.h b/app/streaming/video/decoder.h index b5c3eb34..2214850a 100644 --- a/app/streaming/video/decoder.h +++ b/app/streaming/video/decoder.h @@ -36,6 +36,7 @@ typedef struct _VIDEO_STATS { typedef struct _DECODER_PARAMETERS { SDL_Window* window; StreamingPreferences::VideoDecoderSelection vds; + StreamingPreferences::RendererSelection renderer; int videoFormat; int width; diff --git a/app/streaming/video/ffmpeg.cpp b/app/streaming/video/ffmpeg.cpp index 055601a3..5e1f2c62 100644 --- a/app/streaming/video/ffmpeg.cpp +++ b/app/streaming/video/ffmpeg.cpp @@ -988,7 +988,7 @@ void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title) } } -IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig* hwDecodeCfg, int pass) +IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig* hwDecodeCfg, PDECODER_PARAMETERS params, int pass) { if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) { return nullptr; @@ -1007,11 +1007,17 @@ IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig case AV_HWDEVICE_TYPE_VIDEOTOOLBOX: // Prefer the libplacebo (on MoltenVK) renderer unless explicitly opted out #ifdef HAVE_LIBPLACEBO_VULKAN - if (qgetenv("PREFER_VULKAN") != "0") { + if (params->renderer == StreamingPreferences::RS_AUTO || params->renderer == StreamingPreferences::RS_VULKAN) { return new PlVkRenderer(hwDecodeCfg->device_type); } #endif - return VTMetalRendererFactory::createRenderer(true); + if (params->renderer == StreamingPreferences::RS_AVSBDL) { + return VTRendererFactory::createRenderer(); + } + else { + // This covers both Metal explicitly selected and probe-only (since Metal is cheap to instantiate) + return VTMetalRendererFactory::createRenderer(true); + } #endif #ifdef HAVE_LIBVA case AV_HWDEVICE_TYPE_VAAPI: @@ -1334,7 +1340,7 @@ bool FFmpegVideoDecoder::tryInitializeRendererForUnknownDecoder(const AVCodec* d // Initialize the hardware codec and submit a test frame if the renderer needs it IFFmpegRenderer::InitFailureReason failureReason; if (tryInitializeRenderer(decoder, AV_PIX_FMT_NONE, params, config, &failureReason, - [config, pass]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, pass); })) { + [config, params, pass]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, params, pass); })) { return true; } else if (failureReason == IFFmpegRenderer::InitFailureReason::NoHardwareSupport) { @@ -1534,7 +1540,7 @@ bool FFmpegVideoDecoder::tryInitializeHwAccelDecoder(PDECODER_PARAMETERS params, // Initialize the hardware codec and submit a test frame if the renderer needs it IFFmpegRenderer::InitFailureReason failureReason; if (tryInitializeRenderer(decoder, AV_PIX_FMT_NONE, params, config, &failureReason, - [config, pass]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, pass); })) { + [config, params, pass]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, params, pass); })) { return true; } else if (failureReason == IFFmpegRenderer::InitFailureReason::NoHardwareSupport) { diff --git a/app/streaming/video/ffmpeg.h b/app/streaming/video/ffmpeg.h index 111df338..1e9e9d18 100644 --- a/app/streaming/video/ffmpeg.h +++ b/app/streaming/video/ffmpeg.h @@ -86,7 +86,7 @@ private: IFFmpegRenderer::InitFailureReason* failureReason, std::function createRendererFunc); - static IFFmpegRenderer* createHwAccelRenderer(const AVCodecHWConfig* hwDecodeCfg, int pass); + static IFFmpegRenderer* createHwAccelRenderer(const AVCodecHWConfig* hwDecodeCfg, PDECODER_PARAMETERS params, int pass); bool initializeRendererInternal(IFFmpegRenderer* renderer, PDECODER_PARAMETERS params);