From e752f50195445fe647286de3795b10026055401b Mon Sep 17 00:00:00 2001 From: naughtyGitCat Date: Sat, 6 Jun 2026 11:46:25 +0800 Subject: [PATCH] Add graphics adapter (GPU) selection Add a "Graphics Adapter" submenu under Settings to choose which physical GPU Shotcut renders, decodes, and encodes with. On systems where a low-power integrated GPU drives the display while a more capable discrete GPU is also present (e.g. an NVIDIA card with no monitor attached), this lets the user direct preview rendering, hardware decoding and hardware export to the discrete GPU. - gpuinfo.{h,cpp} (new): enumerate physical adapters via DXGI, de-duplicating the same GPU that some drivers report multiple times; resolve a GPU's live DXGI adapter index from its stable vendor+device id (the raw index is not stable across runs on some systems); and pick the hardware encoder family matching the selected vendor (NVIDIA->nvenc, AMD->amf, Intel->qsv), skipping H.264 hardware encoders for 10-bit video since they do not support it. - main.cpp: set QT_D3D_ADAPTER_INDEX (Qt RHI preview/UI) and MLT_AVFORMAT_HWACCEL_DEVICE (FFmpeg hardware decoder) to the selected GPU before the RHI is initialized (Windows Direct3D backend). - docks/encodedock.cpp: when hardware encoding is enabled, prefer the encoder family matching the selected GPU. - mainwindow: Settings > Graphics Adapter menu with a restart prompt. - tests: a QtTest suite for the encoder selection and adapter logic, built behind the off-by-default SHOTCUT_BUILD_TESTS option (configure with -DSHOTCUT_BUILD_TESTS=ON, run with ctest). UI translations are intentionally omitted (handled via Transifex). Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 6 ++ src/CMakeLists.txt | 3 +- src/docks/encodedock.cpp | 21 +++--- src/gpuinfo.cpp | 157 +++++++++++++++++++++++++++++++++++++++ src/gpuinfo.h | 63 ++++++++++++++++ src/main.cpp | 25 +++++++ src/mainwindow.cpp | 57 ++++++++++++++ src/mainwindow.h | 1 + src/mainwindow.ui | 6 ++ src/settings.cpp | 24 ++++++ src/settings.h | 4 + tests/CMakeLists.txt | 12 +++ tests/test_gpuinfo.cpp | 133 +++++++++++++++++++++++++++++++++ 13 files changed, 502 insertions(+), 10 deletions(-) create mode 100644 src/gpuinfo.cpp create mode 100644 src/gpuinfo.h create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_gpuinfo.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 294c091d20..a59919656d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,12 @@ if(UNIX AND NOT APPLE AND BUILD_MINIMAL_MEDIA_BACKEND) add_subdirectory(MinimalMediaBackend) endif() +option(SHOTCUT_BUILD_TESTS "Build unit tests" OFF) +if(SHOTCUT_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() + feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) add_custom_target(codespell COMMAND diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d7a802906e..b69e9833ca 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -66,6 +66,7 @@ add_executable(shotcut WIN32 MACOSX_BUNDLE docks/subtitlesdock.cpp docks/subtitlesdock.h docks/timelinedock.cpp docks/timelinedock.h FlatpakWrapperGenerator.cpp FlatpakWrapperGenerator.h + gpuinfo.cpp gpuinfo.h htmlgenerator.h htmlgenerator.cpp jobqueue.cpp jobqueue.h jobs/abstractjob.cpp jobs/abstractjob.h @@ -340,7 +341,7 @@ if(WIN32) target_sources(shotcut PRIVATE windowstools.cpp windowstools.h) target_sources(shotcut PRIVATE widgets/d3dvideowidget.h widgets/d3dvideowidget.cpp) target_sources(shotcut PRIVATE widgets/openglvideowidget.h widgets/openglvideowidget.cpp) - target_link_libraries(shotcut PRIVATE d3d11 d3dcompiler ole32) + target_link_libraries(shotcut PRIVATE d3d11 d3dcompiler dxgi dxguid ole32) # Runtime exception handler for debug only if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") diff --git a/src/docks/encodedock.cpp b/src/docks/encodedock.cpp index 7861e51dbb..f44a148efb 100644 --- a/src/docks/encodedock.cpp +++ b/src/docks/encodedock.cpp @@ -23,6 +23,7 @@ #include "dialogs/listselectiondialog.h" #include "dialogs/multifileexportdialog.h" #include "findanalysisfilterparser.h" +#include "gpuinfo.h" #include "jobqueue.h" #include "jobs/encodejob.h" #include "mainwindow.h" @@ -211,16 +212,18 @@ void EncodeDock::loadPresetFromProperties(Mlt::Properties &preset) ui->metaLanguageLineEdit->clear(); if (ui->hwencodeCheckBox->isChecked()) { + // Prefer the hardware encoder family that matches the user-selected GPU vendor + // (NVIDIA -> *_nvenc, AMD -> *_amf, Intel -> *_qsv) so that selecting the + // discrete NVIDIA GPU drives export through NVENC even when an AMD (AMF) encoder + // also passed detection and happens to come first. 10-bit video skips H.264 + // hardware encoders, which do not support it. See gpuinfo.cpp. const bool is10bit = QString::fromLatin1(preset.get("pix_fmt")).contains("p10le"); - for (const QString &hw : Settings.encodeHardware()) { - if ((vcodec == "libx264" && hw.startsWith("h264") && !is10bit) - || (vcodec == "libx265" && hw.startsWith("hevc")) - || (vcodec == "libvpx-vp9" && hw.startsWith("vp9")) - || (vcodec == "libsvtav1" && hw.startsWith("av1"))) { - vcodec = hw; - break; - } - } + const QString chosen = preferredHardwareVcodec(Settings.encodeHardware(), + vcodec, + Settings.gpuAdapterVendorId(), + is10bit); + if (!chosen.isEmpty()) + vcodec = chosen; } ui->disableAudioCheckbox->setChecked(preset.get_int("an")); diff --git a/src/gpuinfo.cpp b/src/gpuinfo.cpp new file mode 100644 index 0000000000..49b65494a1 --- /dev/null +++ b/src/gpuinfo.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026 Meltytech, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gpuinfo.h" + +#include +#include + +// Platform-independent: pick the hardware encoder matching the GPU vendor, with a +// fallback to the first type-compatible encoder. Kept out of the Windows-only block +// so it builds and is unit-testable everywhere. +QString preferredHardwareVcodec(const QStringList &hardwareCodecs, + const QString &softwareVcodec, + uint vendorId, + bool is10bit) +{ + auto matchesType = [&softwareVcodec, is10bit](const QString &hw) { + // No known H.264 hardware encoder supports 10-bit, so skip it in that case. + return (softwareVcodec == QLatin1String("libx264") + && hw.startsWith(QLatin1String("h264")) && !is10bit) + || (softwareVcodec == QLatin1String("libx265") + && hw.startsWith(QLatin1String("hevc"))) + || (softwareVcodec == QLatin1String("libvpx-vp9") + && hw.startsWith(QLatin1String("vp9"))) + || (softwareVcodec == QLatin1String("libsvtav1") + && hw.startsWith(QLatin1String("av1"))); + }; + QString preferredSuffix; + switch (vendorId) { + case kGpuVendorNvidia: + preferredSuffix = QStringLiteral("_nvenc"); + break; + case kGpuVendorAmd: + preferredSuffix = QStringLiteral("_amf"); + break; + case kGpuVendorIntel: + preferredSuffix = QStringLiteral("_qsv"); + break; + default: + break; + } + if (!preferredSuffix.isEmpty()) { + for (const QString &hw : hardwareCodecs) { + if (matchesType(hw) && hw.endsWith(preferredSuffix)) + return hw; + } + } + for (const QString &hw : hardwareCodecs) { + if (matchesType(hw)) + return hw; + } + return QString(); +} + +#ifdef Q_OS_WIN +#include + +QList enumerateGpuAdapters() +{ + QList result; + IDXGIFactory1 *factory = nullptr; + // IID_IDXGIFactory1 is provided by the dxguid import library (linked in CMake), + // which keeps this portable across MinGW/GCC and MSVC. + if (FAILED(CreateDXGIFactory1(IID_IDXGIFactory1, reinterpret_cast(&factory))) + || !factory) + return result; + + // Some drivers (notably AMD integrated graphics) enumerate the same physical GPU + // many times with identical hardware ids but different LUIDs. De-duplicate by a + // stable hardware key so each GPU appears once in the menu, while preserving the + // true DXGI adapter index (which is what QT_D3D_ADAPTER_INDEX expects). + QSet seen; + IDXGIAdapter1 *adapter = nullptr; + for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i) { + if (adapter) { + DXGI_ADAPTER_DESC1 desc; + if (SUCCEEDED(adapter->GetDesc1(&desc)) + // Hide the Microsoft Basic Render Driver (software rasterizer). + && !(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)) { + const QString key = QStringLiteral("%1:%2:%3:%4") + .arg(desc.VendorId) + .arg(desc.DeviceId) + .arg(desc.SubSysId) + .arg(desc.Revision); + if (!seen.contains(key)) { + seen.insert(key); + GpuAdapterInfo info; + info.index = static_cast(i); + info.name = QString::fromWCharArray(desc.Description); + info.vendorId = desc.VendorId; + info.deviceId = desc.DeviceId; + result.append(info); + } + } + adapter->Release(); + adapter = nullptr; + } + } + factory->Release(); + return result; +} + +int gpuAdapterIndexFor(uint vendorId, uint deviceId) +{ + if (vendorId == 0) + return -1; + IDXGIFactory1 *factory = nullptr; + if (FAILED(CreateDXGIFactory1(IID_IDXGIFactory1, reinterpret_cast(&factory))) + || !factory) + return -1; + int found = -1; + IDXGIAdapter1 *adapter = nullptr; + for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i) { + if (adapter) { + DXGI_ADAPTER_DESC1 desc; + if (found < 0 && SUCCEEDED(adapter->GetDesc1(&desc)) + && !(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) && desc.VendorId == vendorId + && desc.DeviceId == deviceId) { + found = static_cast(i); + } + adapter->Release(); + adapter = nullptr; + } + } + factory->Release(); + return found; +} + +#else // !Q_OS_WIN + +QList enumerateGpuAdapters() +{ + // Index-based GPU selection is currently only implemented for the Windows + // Direct3D RHI backend. Returning empty hides the selection UI elsewhere. + return QList(); +} + +int gpuAdapterIndexFor(uint, uint) +{ + return -1; +} + +#endif diff --git a/src/gpuinfo.h b/src/gpuinfo.h new file mode 100644 index 0000000000..ceaa2b349b --- /dev/null +++ b/src/gpuinfo.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 Meltytech, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef GPUINFO_H +#define GPUINFO_H + +#include +#include +#include + +// PCI vendor ids of the common GPU vendors. +enum GpuVendorId { + kGpuVendorNvidia = 0x10DE, + kGpuVendorAmd = 0x1002, + kGpuVendorIntel = 0x8086, +}; + +struct GpuAdapterInfo +{ + int index = -1; // current DXGI adapter index; matches QT_D3D_ADAPTER_INDEX + QString name; // human-readable description, e.g. "NVIDIA GeForce RTX 3090" + uint vendorId = 0; // PCI vendor id (see GpuVendorId) + uint deviceId = 0; // PCI device id; together with vendorId identifies the GPU +}; + +// Enumerate the physical GPU adapters usable by the renderer. +// On Windows this uses DXGI (the same enumeration order Qt's D3D RHI backend uses). +// Returns an empty list on platforms/backends where index-based selection is not +// supported, in which case the caller should hide the selection UI. +QList enumerateGpuAdapters(); + +// Resolve the CURRENT DXGI adapter index (as QT_D3D_ADAPTER_INDEX expects) of the GPU +// identified by vendorId+deviceId, or -1 if not found. Some drivers enumerate adapters +// in an unstable order across runs, so the index must be resolved live at startup from +// the GPU's stable identity rather than persisted from a previous session. +int gpuAdapterIndexFor(uint vendorId, uint deviceId); + +// Choose the hardware video encoder for a given software codec, preferring the encoder +// family matching the selected GPU vendor (NVIDIA->*_nvenc, AMD->*_amf, Intel->*_qsv) +// and otherwise falling back to the first type-compatible encoder in hardwareCodecs. +// When is10bit is true, H.264 hardware encoders are skipped because they do not support +// 10-bit. Returns an empty string when no compatible hardware encoder is available. +// This is a pure, platform-independent function so it can be unit tested. +QString preferredHardwareVcodec(const QStringList &hardwareCodecs, + const QString &softwareVcodec, + uint vendorId, + bool is10bit); + +#endif // GPUINFO_H diff --git a/src/main.cpp b/src/main.cpp index ec41823b3b..64cf2ce278 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,6 +18,7 @@ #include "ConsoleAppender.h" #include "FileAppender.h" #include "Logger.h" +#include "gpuinfo.h" #include "mainwindow.h" #include "settings.h" @@ -423,6 +424,30 @@ int main(int argc, char **argv) Application a(argc, argv); int result = EXIT_SUCCESS; +#ifdef Q_OS_WIN + // Direct the user-selected physical GPU to be used by the Qt RHI (Direct3D) + // preview/UI via QT_D3D_ADAPTER_INDEX and by the FFmpeg hardware decoder via + // MLT_AVFORMAT_HWACCEL_DEVICE. These must be set before the RHI is initialized (the + // adapter index is read lazily when the first scene-graph window is created). In + // release builds the watchdog child process, and the melt export subprocess, inherit + // this environment. Values already set by the user take precedence. The adapter index + // is resolved live from the GPU's stable vendor+device identity because some drivers + // enumerate adapters in an unstable order across runs. See gpuinfo.cpp and Settings. + { + const uint gpuVendorId = Settings.gpuAdapterVendorId(); + if (gpuVendorId != 0) { + const int gpuAdapterIndex = gpuAdapterIndexFor(gpuVendorId, + Settings.gpuAdapterDeviceId()); + if (gpuAdapterIndex >= 0) { + const QByteArray index = QByteArray::number(gpuAdapterIndex); + if (!qEnvironmentVariableIsSet("QT_D3D_ADAPTER_INDEX")) + ::qputenv("QT_D3D_ADAPTER_INDEX", index); + if (!qEnvironmentVariableIsSet("MLT_AVFORMAT_HWACCEL_DEVICE")) + ::qputenv("MLT_AVFORMAT_HWACCEL_DEVICE", index); + } + } + } +#endif #ifdef QT_DEBUG ::qputenv(kWatchdogEnvVar, "1"); #endif diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9936d84a8c..4911f8069f 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -47,6 +47,7 @@ #include "docks/recentdock.h" #include "docks/subtitlesdock.h" #include "docks/timelinedock.h" +#include "gpuinfo.h" #include "hdrpreviewwindow.h" #include "jobqueue.h" #include "jobs/screencapturejob.h" @@ -1614,6 +1615,43 @@ void MainWindow::setupSettingsMenu() ui->menuDrawingMethod = 0; #endif + // Setup the Graphics Adapter (GPU) selection menu. This is currently only + // populated where index-based GPU selection is supported (Windows D3D RHI); + // enumerateGpuAdapters() returns empty elsewhere and the menu is hidden. + { + const QList adapters = enumerateGpuAdapters(); + if (adapters.isEmpty()) { + delete ui->menuGpuAdapter; + ui->menuGpuAdapter = nullptr; + } else { + QActionGroup *gpuGroup = new QActionGroup(this); + const uint currentVendor = Settings.gpuAdapterVendorId(); + const uint currentDevice = Settings.gpuAdapterDeviceId(); + QAction *autoAction = ui->menuGpuAdapter->addAction(tr("Automatic")); + autoAction->setCheckable(true); + autoAction->setProperty("vendorId", 0u); + autoAction->setProperty("deviceId", 0u); + autoAction->setChecked(currentVendor == 0); + gpuGroup->addAction(autoAction); + for (const GpuAdapterInfo &gpu : adapters) { + QAction *action = ui->menuGpuAdapter->addAction(gpu.name); + action->setCheckable(true); + action->setProperty("vendorId", gpu.vendorId); + action->setProperty("deviceId", gpu.deviceId); + action->setChecked(currentVendor == gpu.vendorId && currentDevice == gpu.deviceId); + gpuGroup->addAction(action); + LOG_INFO() << "GPU adapter" << gpu.index << gpu.name + << QString::asprintf("vendor=0x%04X device=0x%04X", + gpu.vendorId, + gpu.deviceId); + } + connect(gpuGroup, + SIGNAL(triggered(QAction *)), + this, + SLOT(onGpuAdapterTriggered(QAction *))); + } + } + // Setup the job priority actions group = new QActionGroup(this); group->addAction(ui->actionJobPriorityLow); @@ -5345,6 +5383,25 @@ void MainWindow::onDrawingMethodTriggered(QAction *action) } #endif +void MainWindow::onGpuAdapterTriggered(QAction *action) +{ + Settings.setGpuAdapterVendorId(action->property("vendorId").toUInt()); + Settings.setGpuAdapterDeviceId(action->property("deviceId").toUInt()); + QMessageBox dialog(QMessageBox::Information, + qApp->applicationName(), + tr("You must restart Shotcut to change the graphics adapter.\n" + "Do you want to restart now?"), + QMessageBox::No | QMessageBox::Yes, + this); + dialog.setDefaultButton(QMessageBox::Yes); + dialog.setEscapeButton(QMessageBox::No); + dialog.setWindowModality(QmlApplication::dialogModality()); + if (dialog.exec() == QMessageBox::Yes) { + m_exitCode = EXIT_RESTART; + QApplication::closeAllWindows(); + } +} + void MainWindow::on_actionResources_triggered() { ResourceDialog dialog(this); diff --git a/src/mainwindow.h b/src/mainwindow.h index f37c45624f..e855fe2268 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -346,6 +346,7 @@ private slots: #if !defined(Q_OS_MAC) void onDrawingMethodTriggered(QAction *); #endif + void onGpuAdapterTriggered(QAction *); void on_actionResources_triggered(); void on_actionApplicationLog_triggered(); void on_actionClose_triggered(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index b340cd2981..7f09ef8b99 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -196,6 +196,11 @@ + + + Graphics Adapter + + Job Priority @@ -314,6 +319,7 @@ + diff --git a/src/settings.cpp b/src/settings.cpp index f52c3d7ab9..466afc1568 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1360,6 +1360,30 @@ void ShotcutSettings::setDrawMethod(int i) settings.setValue("opengl", i); } +uint ShotcutSettings::gpuAdapterVendorId() const +{ + // PCI vendor id of the selected GPU (0x10DE NVIDIA, 0x1002 AMD, 0x8086 Intel). + // 0 means Automatic / system default. The vendor+device pair is the stable identity + // of the chosen GPU; the live DXGI adapter index is resolved from it at startup. + return settings.value("player/gpuAdapterVendorId", 0).toUInt(); +} + +void ShotcutSettings::setGpuAdapterVendorId(uint id) +{ + settings.setValue("player/gpuAdapterVendorId", id); +} + +uint ShotcutSettings::gpuAdapterDeviceId() const +{ + // PCI device id of the selected GPU; pairs with the vendor id to identify it. + return settings.value("player/gpuAdapterDeviceId", 0).toUInt(); +} + +void ShotcutSettings::setGpuAdapterDeviceId(uint id) +{ + settings.setValue("player/gpuAdapterDeviceId", id); +} + bool ShotcutSettings::safeMode() const { return settings.value("safeMode", false).toBool(); diff --git a/src/settings.h b/src/settings.h index f7a68fc66c..d73e43434f 100644 --- a/src/settings.h +++ b/src/settings.h @@ -302,6 +302,10 @@ class ShotcutSettings : public QObject // general continued int drawMethod() const; void setDrawMethod(int); + uint gpuAdapterVendorId() const; + void setGpuAdapterVendorId(uint); + uint gpuAdapterDeviceId() const; + void setGpuAdapterDeviceId(uint); bool safeMode() const; void setSafeMode(bool value); bool noUpgrade() const; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000000..1aadb88969 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,12 @@ +find_package(Qt6 6.4 REQUIRED COMPONENTS Core Test) + +add_executable(test_gpuinfo + test_gpuinfo.cpp + ${CMAKE_SOURCE_DIR}/src/gpuinfo.cpp +) +target_include_directories(test_gpuinfo PRIVATE ${CMAKE_SOURCE_DIR}/src) +target_link_libraries(test_gpuinfo PRIVATE Qt6::Core Qt6::Test) +if(WIN32) + target_link_libraries(test_gpuinfo PRIVATE dxgi dxguid) +endif() +add_test(NAME gpuinfo COMMAND test_gpuinfo) diff --git a/tests/test_gpuinfo.cpp b/tests/test_gpuinfo.cpp new file mode 100644 index 0000000000..65e68b8efb --- /dev/null +++ b/tests/test_gpuinfo.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2026 Meltytech, LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gpuinfo.h" + +#include +#include + +class TestGpuInfo : public QObject +{ + Q_OBJECT + +private slots: + // ---- preferredHardwareVcodec (pure logic) ---------------------------------- + + void prefersVendorFamily() + { + const QStringList hw{"h264_nvenc", "h264_amf", "hevc_nvenc", "hevc_amf"}; + QCOMPARE(preferredHardwareVcodec(hw, "libx264", kGpuVendorNvidia, false), + QString("h264_nvenc")); + QCOMPARE(preferredHardwareVcodec(hw, "libx265", kGpuVendorNvidia, false), + QString("hevc_nvenc")); + QCOMPARE(preferredHardwareVcodec(hw, "libx264", kGpuVendorAmd, false), + QString("h264_amf")); + QCOMPARE(preferredHardwareVcodec(hw, "libx265", kGpuVendorAmd, false), + QString("hevc_amf")); + } + + void prefersQsvForIntel() + { + const QStringList hw{"h264_nvenc", "h264_qsv"}; + QCOMPARE(preferredHardwareVcodec(hw, "libx264", kGpuVendorIntel, false), + QString("h264_qsv")); + } + + void orderIndependent() + { + // The NVIDIA encoder must win even when the AMD one comes first in the list. + const QStringList hw{"h264_amf", "h264_nvenc"}; + QCOMPARE(preferredHardwareVcodec(hw, "libx264", kGpuVendorNvidia, false), + QString("h264_nvenc")); + } + + void fallsBackToFirstWhenFamilyMissing() + { + // NVIDIA selected but only an AMF encoder is available -> use it. + const QStringList hw{"h264_amf"}; + QCOMPARE(preferredHardwareVcodec(hw, "libx264", kGpuVendorNvidia, false), + QString("h264_amf")); + } + + void fallsBackToFirstWhenAutomatic() + { + // vendorId 0 (Automatic) -> first type-compatible encoder. + const QStringList hw{"h264_amf", "h264_nvenc"}; + QCOMPARE(preferredHardwareVcodec(hw, "libx264", 0u, false), QString("h264_amf")); + } + + void matchesCodecType() + { + const QStringList hw{"h264_nvenc", "hevc_nvenc", "av1_nvenc", "vp9_qsv"}; + QCOMPARE(preferredHardwareVcodec(hw, "libsvtav1", kGpuVendorNvidia, false), + QString("av1_nvenc")); + // No vp9 NVENC exists -> fall back to the first vp9 encoder. + QCOMPARE(preferredHardwareVcodec(hw, "libvpx-vp9", kGpuVendorNvidia, false), + QString("vp9_qsv")); + } + + void skipsH264HardwareFor10bit() + { + // No H.264 hardware encoder supports 10-bit, so it must not be selected; HEVC + // hardware encoders are unaffected (they do support 10-bit). + const QStringList hw{"h264_nvenc", "hevc_nvenc"}; + QVERIFY(preferredHardwareVcodec(hw, "libx264", kGpuVendorNvidia, true).isEmpty()); + QCOMPARE(preferredHardwareVcodec(hw, "libx265", kGpuVendorNvidia, true), + QString("hevc_nvenc")); + } + + void returnsEmptyWhenNoneCompatible() + { + const QStringList hw{"h264_nvenc", "hevc_nvenc"}; + QVERIFY(preferredHardwareVcodec(hw, "libvpx-vp9", kGpuVendorNvidia, false).isEmpty()); + QVERIFY(preferredHardwareVcodec(QStringList(), "libx264", kGpuVendorNvidia, false).isEmpty()); + } + + // ---- enumerateGpuAdapters / gpuAdapterIndexFor (hardware dependent) --------- + // These assert invariants only; on a machine/CI without a usable GPU the list is + // empty and the loops are simply skipped. + + void enumerationInvariants() + { + const QList adapters = enumerateGpuAdapters(); + QSet identities; + for (const GpuAdapterInfo &g : adapters) { + QVERIFY2(g.index >= 0, "adapter index must be non-negative"); + QVERIFY2(g.vendorId != 0, "adapter vendorId must be set"); + QVERIFY2(!g.name.isEmpty(), "adapter name must be set"); + // De-duplication: each physical GPU appears at most once. + const QString id = QStringLiteral("%1:%2").arg(g.vendorId).arg(g.deviceId); + QVERIFY2(!identities.contains(id), "adapters must be de-duplicated by identity"); + identities.insert(id); + } + } + + void indexResolution() + { + const QList adapters = enumerateGpuAdapters(); + for (const GpuAdapterInfo &g : adapters) { + // A present GPU resolves to a valid live index... + QVERIFY(gpuAdapterIndexFor(g.vendorId, g.deviceId) >= 0); + } + // ...an absent GPU and the "automatic" sentinel resolve to -1. + QCOMPARE(gpuAdapterIndexFor(0xFFFFu, 0xFFFFu), -1); + QCOMPARE(gpuAdapterIndexFor(0u, 0u), -1); + } +}; + +QTEST_MAIN(TestGpuInfo) +#include "test_gpuinfo.moc"