fix(qt): preflight preferred GPU presentation
cx
This commit is contained in:
17 files changed
+975
-187
No files matched your search
Generated
-10
@@ -199,15 +199,6 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ash"
|
|
||||||
version = "0.38.0+1.3.281"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
|
|
||||||
dependencies = [
|
|
||||||
"libloading 0.8.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ashpd"
|
name = "ashpd"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -4517,7 +4508,6 @@ name = "qbz-qt"
|
|||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"ash",
|
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"cc",
|
"cc",
|
||||||
"cxx",
|
"cxx",
|
||||||
|
|||||||
@@ -4696,6 +4696,9 @@ msgstr "integriert"
|
|||||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||||
msgstr "Bevorzugte GPU geändert — QBZ neu starten, um die Änderung zu übernehmen"
|
msgstr "Bevorzugte GPU geändert — QBZ neu starten, um die Änderung zu übernehmen"
|
||||||
|
|
||||||
|
msgid "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
msgstr "Die ausgewählte GPU ist in dieser Bildschirmsitzung nicht verfügbar — Auto wird verwendet"
|
||||||
|
|
||||||
msgid "Renderer changed — restart QBZ to apply"
|
msgid "Renderer changed — restart QBZ to apply"
|
||||||
msgstr "Renderer geändert — QBZ neu starten, um es anzuwenden"
|
msgstr "Renderer geändert — QBZ neu starten, um es anzuwenden"
|
||||||
|
|
||||||
|
|||||||
@@ -399,6 +399,9 @@ msgstr "integrated"
|
|||||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||||
msgstr "Preferred GPU changed — restart QBZ to apply"
|
msgstr "Preferred GPU changed — restart QBZ to apply"
|
||||||
|
|
||||||
|
msgid "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
msgstr "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
|
||||||
msgid "Search your library"
|
msgid "Search your library"
|
||||||
msgstr "Search your library"
|
msgstr "Search your library"
|
||||||
|
|
||||||
|
|||||||
@@ -4726,6 +4726,9 @@ msgstr "integrada"
|
|||||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||||
msgstr "GPU preferida cambiada — reinicia QBZ para aplicar"
|
msgstr "GPU preferida cambiada — reinicia QBZ para aplicar"
|
||||||
|
|
||||||
|
msgid "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
msgstr "La GPU seleccionada no está disponible en esta sesión de pantalla — se usará Auto"
|
||||||
|
|
||||||
msgid "Renderer changed — restart QBZ to apply"
|
msgid "Renderer changed — restart QBZ to apply"
|
||||||
msgstr "Cambiaste el renderizador — reinicia QBZ para aplicarlo"
|
msgstr "Cambiaste el renderizador — reinicia QBZ para aplicarlo"
|
||||||
|
|
||||||
|
|||||||
@@ -4696,6 +4696,9 @@ msgstr "intégré"
|
|||||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||||
msgstr "GPU préféré modifié — redémarrez QBZ pour appliquer"
|
msgstr "GPU préféré modifié — redémarrez QBZ pour appliquer"
|
||||||
|
|
||||||
|
msgid "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
msgstr "Le GPU sélectionné n’est pas disponible dans cette session d’affichage — utilisation du mode Auto"
|
||||||
|
|
||||||
msgid "Renderer changed — restart QBZ to apply"
|
msgid "Renderer changed — restart QBZ to apply"
|
||||||
msgstr "Moteur de rendu modifié — redémarrez QBZ pour l'appliquer"
|
msgstr "Moteur de rendu modifié — redémarrez QBZ pour l'appliquer"
|
||||||
|
|
||||||
|
|||||||
@@ -4682,6 +4682,9 @@ msgstr "内蔵"
|
|||||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||||
msgstr "優先GPUを変更しました — 適用するには QBZ を再起動してください"
|
msgstr "優先GPUを変更しました — 適用するには QBZ を再起動してください"
|
||||||
|
|
||||||
|
msgid "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
msgstr "選択したGPUはこのディスプレイセッションでは利用できないため、自動設定を使用します"
|
||||||
|
|
||||||
msgid "Renderer changed — restart QBZ to apply"
|
msgid "Renderer changed — restart QBZ to apply"
|
||||||
msgstr "レンダラーを変更しました — 適用するには QBZ を再起動してください"
|
msgstr "レンダラーを変更しました — 適用するには QBZ を再起動してください"
|
||||||
|
|
||||||
|
|||||||
@@ -4708,6 +4708,9 @@ msgstr "geïntegreerd"
|
|||||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||||
msgstr "Voorkeurs-GPU gewijzigd — herstart QBZ om toe te passen"
|
msgstr "Voorkeurs-GPU gewijzigd — herstart QBZ om toe te passen"
|
||||||
|
|
||||||
|
msgid "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
msgstr "De geselecteerde GPU is niet beschikbaar in deze beeldschermsessie — Auto wordt gebruikt"
|
||||||
|
|
||||||
msgid "Renderer changed — restart QBZ to apply"
|
msgid "Renderer changed — restart QBZ to apply"
|
||||||
msgstr "Renderer gewijzigd — start QBZ opnieuw om toe te passen"
|
msgstr "Renderer gewijzigd — start QBZ opnieuw om toe te passen"
|
||||||
|
|
||||||
|
|||||||
@@ -4696,6 +4696,9 @@ msgstr "integrada"
|
|||||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||||
msgstr "GPU preferida alterada — reinicie o QBZ para aplicar"
|
msgstr "GPU preferida alterada — reinicie o QBZ para aplicar"
|
||||||
|
|
||||||
|
msgid "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
msgstr "A GPU selecionada não está disponível nesta sessão gráfica — será usado Auto"
|
||||||
|
|
||||||
msgid "Renderer changed — restart QBZ to apply"
|
msgid "Renderer changed — restart QBZ to apply"
|
||||||
msgstr "Renderizador alterado — reinicie o QBZ para aplicar"
|
msgstr "Renderizador alterado — reinicie o QBZ para aplicar"
|
||||||
|
|
||||||
|
|||||||
@@ -4712,6 +4712,9 @@ msgstr "встроенный"
|
|||||||
msgid "Preferred GPU changed — restart QBZ to apply"
|
msgid "Preferred GPU changed — restart QBZ to apply"
|
||||||
msgstr "Предпочитаемый GPU изменён — перезапустите QBZ, чтобы применить"
|
msgstr "Предпочитаемый GPU изменён — перезапустите QBZ, чтобы применить"
|
||||||
|
|
||||||
|
msgid "Selected GPU is unavailable in this display session — using Auto"
|
||||||
|
msgstr "Выбранный GPU недоступен в этом сеансе отображения — используется авто"
|
||||||
|
|
||||||
msgid "Renderer changed — restart QBZ to apply"
|
msgid "Renderer changed — restart QBZ to apply"
|
||||||
msgstr "Отрисовщик изменён — перезапустите QBZ, чтобы применить"
|
msgstr "Отрисовщик изменён — перезапустите QBZ, чтобы применить"
|
||||||
|
|
||||||
|
|||||||
@@ -50,14 +50,6 @@ qbz-app = { path = "../qbz-app" }
|
|||||||
qbz-audio = { path = "../qbz-audio" }
|
qbz-audio = { path = "../qbz-audio" }
|
||||||
# The HiFi Wizard's frontend-agnostic core; Qt owns the presentation adapter.
|
# The HiFi Wizard's frontend-agnostic core; Qt owns the presentation adapter.
|
||||||
qbz-dac-wizard-core = { path = "../qbz-dac-wizard-core" }
|
qbz-dac-wizard-core = { path = "../qbz-dac-wizard-core" }
|
||||||
# Vulkan enumeration for the Preferred-GPU row — real adapter NAMES, MODELS and
|
|
||||||
# types, in the exact order `QT_VK_PHYSICAL_DEVICE_INDEX` counts them (both are
|
|
||||||
# `vkEnumeratePhysicalDevices`). This is the Qt analogue of what the reference
|
|
||||||
# does with `wgpu::Instance::enumerate_adapters`.
|
|
||||||
#
|
|
||||||
# `ash` is a thin FFI binding that dlopens libvulkan at RUNTIME: no Vulkan SDK
|
|
||||||
# at build time, and a box without libvulkan simply enumerates nothing.
|
|
||||||
ash = "0.38"
|
|
||||||
qbz-qobuz = { path = "../qbz-qobuz" }
|
qbz-qobuz = { path = "../qbz-qobuz" }
|
||||||
# Cross-provider "Open Music Link" resolver. The crate is frontend-agnostic;
|
# Cross-provider "Open Music Link" resolver. The crate is frontend-agnostic;
|
||||||
# src/link_resolver_qt.rs supplies the live Qobuz search + Qt navigation seams.
|
# src/link_resolver_qt.rs supplies the live Qobuz search + Qt navigation seams.
|
||||||
|
|||||||
+17
-5
@@ -321,6 +321,11 @@ fn build_rhi_items() {
|
|||||||
// font through QFontDatabase before Main.qml constructs any text.
|
// font through QFontDatabase before Main.qml constructs any text.
|
||||||
println!("cargo:rerun-if-changed=cxx/font_fallback.cpp");
|
println!("cargo:rerun-if-changed=cxx/font_fallback.cpp");
|
||||||
cc.file("cxx/font_fallback.cpp");
|
cc.file("cxx/font_fallback.cpp");
|
||||||
|
// Plain helper, no QObject/moc: enumerates the exact QVulkanInstance that
|
||||||
|
// Qt Quick will give QRhi. A raw Vulkan instance can see a different order
|
||||||
|
// when hybrid-GPU implicit layers are active.
|
||||||
|
println!("cargo:rerun-if-changed=cxx/qt_vulkan_probe.cpp");
|
||||||
|
cc.file("cxx/qt_vulkan_probe.cpp");
|
||||||
for p in qtbuild.include_paths() {
|
for p in qtbuild.include_paths() {
|
||||||
cc.include(p);
|
cc.include(p);
|
||||||
}
|
}
|
||||||
@@ -337,10 +342,13 @@ fn build_rhi_items() {
|
|||||||
let libs = qtbuild.qmake_query("QT_INSTALL_LIBS");
|
let libs = qtbuild.qmake_query("QT_INSTALL_LIBS");
|
||||||
cc.flag(&format!("-F{}", libs.trim()));
|
cc.flag(&format!("-F{}", libs.trim()));
|
||||||
for module in ["QtCore", "QtGui", "QtQml", "QtQuick"] {
|
for module in ["QtCore", "QtGui", "QtQml", "QtQuick"] {
|
||||||
let private = format!(
|
let version_root = format!("{}/{module}.framework/Headers/{version}", libs.trim());
|
||||||
"{}/{module}.framework/Headers/{version}/{module}",
|
if Path::new(&version_root).is_dir() {
|
||||||
libs.trim()
|
// Private Qt headers include siblings as
|
||||||
);
|
// <QtGui/private/foo_p.h>; that needs the version root.
|
||||||
|
cc.include(&version_root);
|
||||||
|
}
|
||||||
|
let private = format!("{version_root}/{module}");
|
||||||
if Path::new(&private).is_dir() {
|
if Path::new(&private).is_dir() {
|
||||||
cc.include(private);
|
cc.include(private);
|
||||||
}
|
}
|
||||||
@@ -348,7 +356,11 @@ fn build_rhi_items() {
|
|||||||
} else {
|
} else {
|
||||||
let headers = qtbuild.qmake_query("QT_INSTALL_HEADERS");
|
let headers = qtbuild.qmake_query("QT_INSTALL_HEADERS");
|
||||||
for module in ["QtCore", "QtGui", "QtQml", "QtQuick"] {
|
for module in ["QtCore", "QtGui", "QtQml", "QtQuick"] {
|
||||||
let private = format!("{headers}/{module}/{version}/{module}");
|
let version_root = format!("{headers}/{module}/{version}");
|
||||||
|
if Path::new(&version_root).is_dir() {
|
||||||
|
cc.include(&version_root);
|
||||||
|
}
|
||||||
|
let private = format!("{version_root}/{module}");
|
||||||
if Path::new(&private).is_dir() {
|
if Path::new(&private).is_dir() {
|
||||||
cc.include(private);
|
cc.include(private);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#include <QtCore/QByteArray>
|
||||||
|
#include <QtCore/QCoreApplication>
|
||||||
|
#include <QtCore/QEventLoop>
|
||||||
|
#include <QtCore/QJsonArray>
|
||||||
|
#include <QtCore/QJsonDocument>
|
||||||
|
#include <QtCore/QJsonObject>
|
||||||
|
#include <QtCore/QTimer>
|
||||||
|
#include <QtCore/QVector>
|
||||||
|
#include <QtCore/QVersionNumber>
|
||||||
|
#include <QtGui/QGuiApplication>
|
||||||
|
#include <QtGui/qtguiglobal.h>
|
||||||
|
#include <QtQuick/QQuickWindow>
|
||||||
|
|
||||||
|
#if QT_CONFIG(vulkan) && __has_include(<vulkan/vulkan.h>)
|
||||||
|
#include <QtGui/QVulkanFunctions>
|
||||||
|
#include <private/qvulkandefaultinstance_p.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QByteArray serializedDevices("[]");
|
||||||
|
|
||||||
|
#if QT_CONFIG(vulkan) && __has_include(<vulkan/vulkan.h>)
|
||||||
|
QByteArray deviceUuid(QVulkanFunctions *functions, VkPhysicalDevice device,
|
||||||
|
const QVulkanInstance *instance)
|
||||||
|
{
|
||||||
|
if (instance->apiVersion() < QVersionNumber(1, 1))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
VkPhysicalDeviceIDProperties id{};
|
||||||
|
id.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES;
|
||||||
|
VkPhysicalDeviceProperties2 properties{};
|
||||||
|
properties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
||||||
|
properties.pNext = &id;
|
||||||
|
functions->vkGetPhysicalDeviceProperties2(device, &properties);
|
||||||
|
|
||||||
|
bool any = false;
|
||||||
|
for (const auto byte : id.deviceUUID)
|
||||||
|
any = any || byte != 0;
|
||||||
|
if (!any)
|
||||||
|
return {};
|
||||||
|
return QByteArray(reinterpret_cast<const char *>(id.deviceUUID), VK_UUID_SIZE).toHex();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Enumerate through the very QVulkanInstance Qt Quick will later hand to
|
||||||
|
// QRhi. This is intentionally not a second raw vkCreateInstance: implicit
|
||||||
|
// layers (notably hybrid-GPU layers) may expose a different order depending
|
||||||
|
// on the instance extensions and platform integration. Returning Qt's exact
|
||||||
|
// order makes QT_VK_PHYSICAL_DEVICE_INDEX an identity translation instead of
|
||||||
|
// a guess.
|
||||||
|
extern "C" const char *qbz_qt_vulkan_devices_json()
|
||||||
|
{
|
||||||
|
serializedDevices = "[]";
|
||||||
|
|
||||||
|
#if QT_CONFIG(vulkan) && __has_include(<vulkan/vulkan.h>)
|
||||||
|
// QVulkanDefaultInstance needs the platform integration installed by
|
||||||
|
// QGuiApplication. Rust calls this helper after constructing it and before
|
||||||
|
// constructing the first QQuickWindow.
|
||||||
|
if (QCoreApplication::instance() == nullptr)
|
||||||
|
return serializedDevices.constData();
|
||||||
|
|
||||||
|
QVulkanInstance *instance = QVulkanDefaultInstance::instance();
|
||||||
|
if (instance == nullptr || !instance->isValid())
|
||||||
|
return serializedDevices.constData();
|
||||||
|
|
||||||
|
QVulkanFunctions *functions = instance->functions();
|
||||||
|
if (functions == nullptr)
|
||||||
|
return serializedDevices.constData();
|
||||||
|
|
||||||
|
uint32_t count = 0;
|
||||||
|
if (functions->vkEnumeratePhysicalDevices(instance->vkInstance(), &count, nullptr)
|
||||||
|
!= VK_SUCCESS || count == 0) {
|
||||||
|
return serializedDevices.constData();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<VkPhysicalDevice> devices(static_cast<qsizetype>(count));
|
||||||
|
if (functions->vkEnumeratePhysicalDevices(instance->vkInstance(), &count, devices.data())
|
||||||
|
!= VK_SUCCESS) {
|
||||||
|
return serializedDevices.constData();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray result;
|
||||||
|
for (uint32_t index = 0; index < count; ++index) {
|
||||||
|
VkPhysicalDeviceProperties properties{};
|
||||||
|
functions->vkGetPhysicalDeviceProperties(devices.at(index), &properties);
|
||||||
|
|
||||||
|
QJsonObject item;
|
||||||
|
item.insert(QStringLiteral("index"), static_cast<int>(index));
|
||||||
|
item.insert(QStringLiteral("name"),
|
||||||
|
QString::fromUtf8(properties.deviceName).trimmed());
|
||||||
|
item.insert(QStringLiteral("vendor"), static_cast<qint64>(properties.vendorID));
|
||||||
|
item.insert(QStringLiteral("device"), static_cast<qint64>(properties.deviceID));
|
||||||
|
item.insert(QStringLiteral("type"), static_cast<int>(properties.deviceType));
|
||||||
|
item.insert(QStringLiteral("uuid"),
|
||||||
|
QString::fromLatin1(deviceUuid(functions, devices.at(index), instance)));
|
||||||
|
result.append(item);
|
||||||
|
}
|
||||||
|
serializedDevices = QJsonDocument(result).toJson(QJsonDocument::Compact);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return serializedDevices.constData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exercise the exact path the real shell needs, but inside the disposable
|
||||||
|
// preflight child: a Qt Quick Vulkan swapchain has to present actual frames to
|
||||||
|
// the active QPA compositor. Merely creating VkDevice succeeds on the owner's
|
||||||
|
// unsupported Intel-render -> NVIDIA/KWin path; the fatal failure only arrives
|
||||||
|
// when Wayland imports the first DMA-BUF. If that protocol error kills this
|
||||||
|
// process, the parent remains alive and falls back to Auto.
|
||||||
|
extern "C" int qbz_qt_vulkan_preflight_window()
|
||||||
|
{
|
||||||
|
if (qobject_cast<QGuiApplication *>(QCoreApplication::instance()) == nullptr)
|
||||||
|
return 71;
|
||||||
|
|
||||||
|
QQuickWindow window;
|
||||||
|
window.setTitle(QStringLiteral("QBZ GPU preflight"));
|
||||||
|
window.setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowDoesNotAcceptFocus);
|
||||||
|
window.resize(2, 2);
|
||||||
|
window.setColor(Qt::transparent);
|
||||||
|
// Keep the native surface effectively invisible while still requiring a
|
||||||
|
// real buffer commit/import. An offscreen window would miss the exact
|
||||||
|
// Wayland failure this probe exists to catch.
|
||||||
|
window.setOpacity(0.01);
|
||||||
|
|
||||||
|
QEventLoop loop;
|
||||||
|
QTimer pulse;
|
||||||
|
QTimer settle;
|
||||||
|
QTimer deadline;
|
||||||
|
pulse.setInterval(16);
|
||||||
|
settle.setSingleShot(true);
|
||||||
|
deadline.setSingleShot(true);
|
||||||
|
|
||||||
|
int frames = 0;
|
||||||
|
int result = 76; // hard deadline / no usable presentation
|
||||||
|
|
||||||
|
QObject::connect(&pulse, &QTimer::timeout, &window, &QQuickWindow::update);
|
||||||
|
QObject::connect(&window, &QQuickWindow::frameSwapped, &loop, [&] {
|
||||||
|
++frames;
|
||||||
|
if (frames == 1)
|
||||||
|
settle.start(700);
|
||||||
|
window.update();
|
||||||
|
});
|
||||||
|
QObject::connect(&window, &QQuickWindow::sceneGraphError, &loop,
|
||||||
|
[&](QQuickWindow::SceneGraphError, const QString &) {
|
||||||
|
result = 74;
|
||||||
|
loop.quit();
|
||||||
|
});
|
||||||
|
QObject::connect(&settle, &QTimer::timeout, &loop, [&] {
|
||||||
|
// Two swaps plus a grace interval keep the Wayland connection pumping
|
||||||
|
// long enough for an asynchronous compositor import error to arrive.
|
||||||
|
result = frames >= 2 ? 0 : 75;
|
||||||
|
loop.quit();
|
||||||
|
});
|
||||||
|
QObject::connect(&deadline, &QTimer::timeout, &loop, [&] {
|
||||||
|
result = 76;
|
||||||
|
loop.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.show();
|
||||||
|
window.update();
|
||||||
|
pulse.start();
|
||||||
|
deadline.start(5000);
|
||||||
|
loop.exec();
|
||||||
|
|
||||||
|
pulse.stop();
|
||||||
|
settle.stop();
|
||||||
|
deadline.stop();
|
||||||
|
window.hide();
|
||||||
|
window.releaseResources();
|
||||||
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -47,8 +47,9 @@
|
|||||||
//! not a port.
|
//! not a port.
|
||||||
//!
|
//!
|
||||||
//! REPLACED, not dropped — the Qt truths that stand where a GDK/GSK row stood:
|
//! REPLACED, not dropped — the Qt truths that stand where a GDK/GSK row stood:
|
||||||
//! `Renderer (Qt)` (saved pref vs the resolved RHI api), `GPU Adapters` (the
|
//! `Renderer (Qt)` (saved pref vs the resolved RHI api), `GPU Adapters` (Qt's
|
||||||
//! Vulkan enumeration `QT_VK_PHYSICAL_DEVICE_INDEX` counts), `GPU Tier`, and the
|
//! own Vulkan-device order, which `QT_VK_PHYSICAL_DEVICE_INDEX` addresses),
|
||||||
|
//! `GPU Tier`, and the
|
||||||
//! `QSG_RHI_BACKEND` / `QT_QUICK_BACKEND` / `QT_VK_PHYSICAL_DEVICE_INDEX`
|
//! `QSG_RHI_BACKEND` / `QT_QUICK_BACKEND` / `QT_VK_PHYSICAL_DEVICE_INDEX`
|
||||||
//! environment rows — which this binary SETS itself (`renderer_qt.rs:491`), so
|
//! environment rows — which this binary SETS itself (`renderer_qt.rs:491`), so
|
||||||
//! they are the most load-bearing env vars it has.
|
//! they are the most load-bearing env vars it has.
|
||||||
@@ -795,8 +796,9 @@ fn build_graphics_rows(g: &Gathered) -> Vec<Row> {
|
|||||||
// resolved at startup (`renderer_qt::active_api`).
|
// resolved at startup (`renderer_qt::active_api`).
|
||||||
row("Renderer (Qt)", &g.renderer_saved, &g.renderer_runtime, 0),
|
row("Renderer (Qt)", &g.renderer_saved, &g.renderer_runtime, 0),
|
||||||
// Stands where the reference's `renderer_decision_summary()` adapter
|
// Stands where the reference's `renderer_decision_summary()` adapter
|
||||||
// string does. NOT byte-identical: this is the Vulkan enumeration in
|
// string does. NOT byte-identical: this is the actual Qt-owned Vulkan
|
||||||
// `QT_VK_PHYSICAL_DEVICE_INDEX` order, not wgpu's adapter list.
|
// instance order addressed by `QT_VK_PHYSICAL_DEVICE_INDEX`, not
|
||||||
|
// wgpu's or an independently-created Vulkan instance's adapter list.
|
||||||
row("GPU Adapters", "—", &g.gpu_adapters, 0),
|
row("GPU Adapters", "—", &g.gpu_adapters, 0),
|
||||||
// Shaders + the dynamic background need this; false means the scenes
|
// Shaders + the dynamic background need this; false means the scenes
|
||||||
// stay hidden and reduce-motion is forced on, which is exactly the
|
// stay hidden and reduce-motion is forced on, which is exactly the
|
||||||
|
|||||||
@@ -3268,12 +3268,28 @@ pub(crate) fn arm_hard_exit_watchdog(source: &'static str) {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
qbz_log::install("info");
|
qbz_log::install("info");
|
||||||
|
// Internal, disposable graphics child. It must branch before the normal
|
||||||
|
// instance lock, navigation crash-chain, runtime and audio thread: a GPU
|
||||||
|
// whose DMA-BUFs the active Wayland compositor rejects is expected to kill
|
||||||
|
// THIS process, while the waiting parent remains healthy and falls back to
|
||||||
|
// Auto.
|
||||||
|
if renderer_qt::gpu_preflight_child_requested() {
|
||||||
|
let app = QGuiApplication::new();
|
||||||
|
let result = renderer_qt::run_gpu_preflight_child();
|
||||||
|
drop(app);
|
||||||
|
log::logger().flush();
|
||||||
|
std::process::exit(result);
|
||||||
|
}
|
||||||
deep_link_qt::capture_argv();
|
deep_link_qt::capture_argv();
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if !single_instance_qt::acquire_or_raise() {
|
if !single_instance_qt::acquire_or_raise() {
|
||||||
log::info!("[qbz-qt] another instance owns the session bus name; exiting");
|
log::info!("[qbz-qt] another instance owns the session bus name; exiting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Validate a persisted explicit GPU before constructing anything costly or
|
||||||
|
// user-visible. The child uses its own Wayland connection; fatal protocol
|
||||||
|
// errors cannot take this parent down.
|
||||||
|
renderer_qt::preflight_saved_gpu_at_boot();
|
||||||
// Link anchor for the hand-written QAbstractListModel. Its QML singleton
|
// Link anchor for the hand-written QAbstractListModel. Its QML singleton
|
||||||
// registration itself runs at QCoreApplication startup.
|
// registration itself runs at QCoreApplication startup.
|
||||||
local_tracks_model_qt::register_qml_model();
|
local_tracks_model_qt::register_qml_model();
|
||||||
@@ -3367,15 +3383,16 @@ fn main() {
|
|||||||
|
|
||||||
let _ = APP.set(runtime);
|
let _ = APP.set(runtime);
|
||||||
|
|
||||||
// All three must run before QGuiApplication: the renderer envs are read
|
// Renderer policy stays before QGuiApplication. Scroll policy only needs
|
||||||
// when the backend is chosen, the GPU envs when the graphics context is
|
// to precede Flickable construction. Preferred GPU has a narrower safe
|
||||||
// created, and the wheel policy when Flickables are constructed. Setting
|
// slot: QGuiApplication must first install the platform integration so we
|
||||||
// any of them afterwards is a silent no-op.
|
// can enumerate Qt's real QVulkanInstance, but the selection env must land
|
||||||
|
// before the first QQuickWindow/QRhi is constructed.
|
||||||
apply_renderer_preference();
|
apply_renderer_preference();
|
||||||
renderer_qt::apply_gpu_preference();
|
|
||||||
apply_scroll_physics();
|
apply_scroll_physics();
|
||||||
|
|
||||||
let mut app = QGuiApplication::new();
|
let mut app = QGuiApplication::new();
|
||||||
|
renderer_qt::apply_gpu_preference();
|
||||||
let mut engine = QQmlApplicationEngine::new();
|
let mut engine = QQmlApplicationEngine::new();
|
||||||
|
|
||||||
// ── THE APP TYPEFACE, AND WHY IT IS SET RIGHT HERE ────────────────────
|
// ── THE APP TYPEFACE, AND WHY IT IS SET RIGHT HERE ────────────────────
|
||||||
|
|||||||
+693
-130
@@ -188,12 +188,13 @@ fn clear_sentinel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is a sentinel currently armed? Lets the liveness report stay quiet on the
|
/// Is either graphics startup sentinel currently armed? Lets the liveness
|
||||||
/// runs where there is nothing to protect — notably the offscreen smoke, which
|
/// report stay quiet on runs where there is nothing to protect — notably the
|
||||||
/// presents no frames by design and would otherwise log a scary warning on
|
/// offscreen smoke, which presents no frames by design and would otherwise log
|
||||||
/// every gate.
|
/// a scary warning on every gate.
|
||||||
pub fn sentinel_armed() -> bool {
|
pub fn sentinel_armed() -> bool {
|
||||||
sentinel_path().map(|p| p.exists()).unwrap_or(false)
|
sentinel_path().map(|p| p.exists()).unwrap_or(false)
|
||||||
|
|| gpu_sentinel_path().map(|p| p.exists()).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The choice the armed sentinel was protecting, if one survived a launch.
|
/// The choice the armed sentinel was protecting, if one survived a launch.
|
||||||
@@ -204,10 +205,10 @@ fn armed_choice() -> Option<String> {
|
|||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called at the TOP of `apply_renderer_preference()`, before anything is
|
/// Called before GPU preflight (and again, harmlessly, at the top of
|
||||||
/// forced. If a previous forced launch left the sentinel armed it never
|
/// `apply_renderer_preference()`), before anything is forced. If a previous
|
||||||
/// reached liveness, so the choice is reverted to "auto" and this launch runs
|
/// forced launch left the sentinel armed it never reached liveness, so the
|
||||||
/// on Qt's default.
|
/// choice is reverted to "auto" and this launch runs on Qt's default.
|
||||||
///
|
///
|
||||||
/// Returns true when a revert happened — the caller then ignores the persisted
|
/// Returns true when a revert happened — the caller then ignores the persisted
|
||||||
/// pref for this launch (it has just been rewritten to "auto" anyway).
|
/// pref for this launch (it has just been rewritten to "auto" anyway).
|
||||||
@@ -249,11 +250,20 @@ pub fn disarm_on_liveness(frames: i32) {
|
|||||||
// Only claim a disarm when something WAS armed. Saying "sentinel disarmed"
|
// Only claim a disarm when something WAS armed. Saying "sentinel disarmed"
|
||||||
// on every ordinary launch trains the reader to skim past the line, which
|
// on every ordinary launch trains the reader to skim past the line, which
|
||||||
// is precisely the run where it carries information.
|
// is precisely the run where it carries information.
|
||||||
if sentinel_armed() {
|
let renderer_armed = sentinel_path().map(|p| p.exists()).unwrap_or(false);
|
||||||
log::info!("[qbz-qt] renderer: startup sentinel disarmed ({frames} frames rendered)");
|
let gpu_armed = gpu_sentinel_path().map(|p| p.exists()).unwrap_or(false);
|
||||||
|
if renderer_armed || gpu_armed {
|
||||||
|
let protected = match (renderer_armed, gpu_armed) {
|
||||||
|
(true, true) => "renderer + GPU",
|
||||||
|
(true, false) => "renderer",
|
||||||
|
(false, true) => "GPU",
|
||||||
|
(false, false) => unreachable!(),
|
||||||
|
};
|
||||||
|
log::info!("[qbz-qt] {protected}: startup sentinel disarmed ({frames} frames rendered)");
|
||||||
clear_sentinel();
|
clear_sentinel();
|
||||||
|
clear_gpu_sentinel();
|
||||||
} else {
|
} else {
|
||||||
log::debug!("[qbz-qt] renderer: liveness confirmed ({frames} frames), nothing armed");
|
log::debug!("[qbz-qt] graphics: liveness confirmed ({frames} frames), nothing armed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,38 +272,39 @@ pub fn disarm_on_liveness(frames: i32) {
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
//
|
//
|
||||||
// Scoping: `qbz-nix-docs/qt-frontend/2026-08-02-gpu-selection-scoping.md`.
|
// Scoping: `qbz-nix-docs/qt-frontend/2026-08-02-gpu-selection-scoping.md`.
|
||||||
// Qt's own documentation is blunt: QRhi "will choose the system default GPU
|
// Qt exposes no public QRhi adapter-selection API. On Linux its supported
|
||||||
// adapter … No further adapter configurability is provided at this time." So
|
// escape hatch is `QT_VK_PHYSICAL_DEVICE_INDEX`, read when the Vulkan QRhi is
|
||||||
// there is no API — only the driver's env vars, and only on some platforms.
|
// created. The index is process-local and MUST be resolved afresh at boot.
|
||||||
//
|
//
|
||||||
// | platform | selectable | how |
|
// | platform | selectable | how |
|
||||||
// |---|---|---|
|
// |---|---|---|
|
||||||
// | Linux (OpenGL, this port's default) | YES | `DRI_PRIME`, or the NVIDIA PRIME pair |
|
// | Linux | YES | Vulkan + `QT_VK_PHYSICAL_DEVICE_INDEX` |
|
||||||
// | Windows D3D | yes, when that port lands | `QT_D3D_ADAPTER_INDEX` |
|
// | Windows D3D | yes, when that port lands | `QT_D3D_ADAPTER_INDEX` |
|
||||||
// | macOS Metal | **NO** | QRhi hardcodes `MTLCreateSystemDefaultDevice` |
|
// | macOS Metal | **NO** | QRhi hardcodes `MTLCreateSystemDefaultDevice` |
|
||||||
//
|
//
|
||||||
// So: implement Linux, HIDE the row on macOS. A row that cannot do anything is
|
// WHY A RAW VULKAN PROBE IS WRONG. On the owner's Intel + NVIDIA hybrid, an
|
||||||
// worse than no row — and until today this one was worse still, because it
|
// independently-created VkInstance enumerated Intel=0/NVIDIA=1 while Qt's own
|
||||||
// silently DROPPED every selection that was not "Auto"
|
// QVulkanInstance enumerated NVIDIA=0/Intel=1 in the same environment. Hybrid
|
||||||
// (`settings_qt.rs`, the old `"gpu-power"` arm persisted only index 0).
|
// implicit layers can react to instance/platform details; "both call
|
||||||
|
// vkEnumeratePhysicalDevices" does not make the indices interchangeable. The
|
||||||
|
// old code fed the raw index to Qt, selected the opposite GPU, and Wayland
|
||||||
|
// killed the connection when cross-device dmabuf import failed.
|
||||||
//
|
//
|
||||||
// CLASS KEYS, NEVER NAMES. The pref lives in the ui_prefs.json shared with the
|
// `cxx/qt_vulkan_probe.cpp` enumerates the actual QVulkanDefaultInstance that
|
||||||
// shipping Slint build, which accepts `"auto" | "integrated" | "discrete"` and
|
// Qt Quick later gives QRhi. It runs after QGuiApplication installs the
|
||||||
// falls back to Auto for anything it cannot parse. Writing an adapter NAME
|
// platform integration and before the first QQuickWindow exists. Preferences
|
||||||
// here would silently degrade the Slint side — and its wgpu/Vulkan names would
|
// store a stable Vulkan UUID beside the legacy display name; an index is never
|
||||||
// not string-match what we read from sysfs anyway.
|
// persisted.
|
||||||
|
|
||||||
/// One physical GPU, as Vulkan enumerates it.
|
/// One physical GPU in this launch's Qt-owned Vulkan instance.
|
||||||
///
|
|
||||||
/// `index` is the position in `vkEnumeratePhysicalDevices` — the SAME number
|
|
||||||
/// `QT_VK_PHYSICAL_DEVICE_INDEX` selects by, because Qt calls the same
|
|
||||||
/// function. That identity is the whole reason this row can work at all.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct GpuInfo {
|
pub struct GpuInfo {
|
||||||
|
/// Exact `QT_VK_PHYSICAL_DEVICE_INDEX` for this process only.
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
/// The model, verbatim from the driver: "NVIDIA GeForce RTX 4070 Laptop
|
|
||||||
/// GPU", "Intel(R) Arc(tm) Graphics (MTL)".
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub vendor_id: u32,
|
||||||
|
pub device_id: u32,
|
||||||
|
pub identity: String,
|
||||||
pub discrete: bool,
|
pub discrete: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,26 +321,126 @@ impl GpuInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerate the real GPUs, once per process.
|
#[derive(serde::Deserialize)]
|
||||||
///
|
struct QtGpuJson {
|
||||||
/// Empty when libvulkan is absent, no ICD is installed, or the instance cannot
|
index: u32,
|
||||||
/// be created — every one of which means "we cannot honour a GPU choice", so
|
name: String,
|
||||||
/// the row collapses to Auto instead of offering hardware that may not exist.
|
vendor: u32,
|
||||||
/// That is the defect this replaces: a CLASS dropdown offers "Discrete GPU" on
|
device: u32,
|
||||||
/// a machine that has none.
|
#[serde(rename = "type")]
|
||||||
|
device_type: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn qbz_qt_vulkan_devices_json() -> *const std::ffi::c_char;
|
||||||
|
fn qbz_qt_vulkan_preflight_window() -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GPU_PREFLIGHT_CHILD_ENV: &str = "QBZ_INTERNAL_GPU_PREFLIGHT_CHILD";
|
||||||
|
const GPU_PREFLIGHT_IDENTITY_ENV: &str = "QBZ_INTERNAL_GPU_PREFLIGHT_IDENTITY";
|
||||||
|
const GPU_PREFLIGHT_NAME_ENV: &str = "QBZ_INTERNAL_GPU_PREFLIGHT_NAME";
|
||||||
|
const GPU_PREFLIGHT_NOT_FOUND: i32 = 41;
|
||||||
|
const GPU_PREFLIGHT_NOT_SELECTABLE: i32 = 42;
|
||||||
|
const GPU_PREFLIGHT_PARENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(8);
|
||||||
|
|
||||||
|
/// The parent only authorizes a forced GPU after a disposable child proved
|
||||||
|
/// that this exact identity can present Qt Quick frames to the active
|
||||||
|
/// compositor. Kept in-process: a success from an older topology/driver must
|
||||||
|
/// never become a permanent allow-list.
|
||||||
|
static PREFLIGHT_APPROVED_GPU: Mutex<Option<String>> = Mutex::new(None);
|
||||||
|
static PREFLIGHT_FALLBACK_NOTICE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
fn stable_gpu_identity(raw: &QtGpuJson) -> String {
|
||||||
|
let uuid = raw.uuid.trim().to_ascii_lowercase();
|
||||||
|
if uuid.len() == 32
|
||||||
|
&& uuid.bytes().all(|b| b.is_ascii_hexdigit())
|
||||||
|
&& uuid.bytes().any(|b| b != b'0')
|
||||||
|
{
|
||||||
|
format!("vk:{uuid}")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"pci:{:04x}:{:04x}:{}",
|
||||||
|
raw.vendor,
|
||||||
|
raw.device,
|
||||||
|
raw.name.trim().to_ascii_lowercase()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumerate through Qt's own default Vulkan instance. The C++ side refuses to
|
||||||
|
/// run before QGuiApplication exists and returns `[]` on unsupported or broken
|
||||||
|
/// Vulkan setups, so inventory can never become a startup gate.
|
||||||
|
fn enumerate_qt_gpus() -> Vec<GpuInfo> {
|
||||||
|
if !cfg!(target_os = "linux") {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
// SAFETY: the C++ function returns a pointer into a process-static
|
||||||
|
// QByteArray. This call runs on the GUI thread and copies the bytes before
|
||||||
|
// returning.
|
||||||
|
let ptr = unsafe { qbz_qt_vulkan_devices_json() };
|
||||||
|
if ptr.is_null() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let json = unsafe { std::ffi::CStr::from_ptr(ptr) }.to_string_lossy();
|
||||||
|
let raw: Vec<QtGpuJson> = match serde_json::from_str(&json) {
|
||||||
|
Ok(list) => list,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("[qbz-qt] gpu: Qt Vulkan inventory was invalid: {e}");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
gpu_inventory_from_raw(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gpu_inventory_from_raw(raw: Vec<QtGpuJson>) -> Vec<GpuInfo> {
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let mut out = Vec::with_capacity(raw.len());
|
||||||
|
for gpu in raw {
|
||||||
|
let name = gpu.name.trim().to_string();
|
||||||
|
// VkPhysicalDeviceType::CPU is lavapipe/llvmpipe, not another GPU the
|
||||||
|
// Preferred-GPU control should offer.
|
||||||
|
if name.is_empty() || gpu.device_type == 4 || !seen.insert(gpu.index) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(GpuInfo {
|
||||||
|
index: gpu.index,
|
||||||
|
name,
|
||||||
|
vendor_id: gpu.vendor,
|
||||||
|
device_id: gpu.device,
|
||||||
|
identity: stable_gpu_identity(&gpu),
|
||||||
|
discrete: gpu.device_type == 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.sort_by_key(|gpu| gpu.index);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Real hardware adapters in the exact order this process's Qt Vulkan backend
|
||||||
|
/// sees them. Empty is supported: Auto can still use OpenGL or software.
|
||||||
pub fn gpus() -> &'static [GpuInfo] {
|
pub fn gpus() -> &'static [GpuInfo] {
|
||||||
static GPUS: std::sync::OnceLock<Vec<GpuInfo>> = std::sync::OnceLock::new();
|
static GPUS: std::sync::OnceLock<Vec<GpuInfo>> = std::sync::OnceLock::new();
|
||||||
GPUS.get_or_init(|| {
|
GPUS.get_or_init(|| {
|
||||||
let list = enumerate_vulkan_gpus();
|
let list = enumerate_qt_gpus();
|
||||||
if list.is_empty() {
|
if list.is_empty() {
|
||||||
log::info!("[qbz-qt] gpu: no Vulkan devices enumerated — the selector stays on Auto");
|
|
||||||
} else {
|
|
||||||
for g in &list {
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"[qbz-qt] gpu: device {} '{}' ({})",
|
"[qbz-qt] gpu: Qt exposed no selectable Vulkan hardware — Auto remains in control"
|
||||||
g.index,
|
);
|
||||||
g.name,
|
} else {
|
||||||
if g.discrete { "discrete" } else { "integrated" }
|
for gpu in &list {
|
||||||
|
log::info!(
|
||||||
|
"[qbz-qt] gpu: Qt device {} '{}' ({}, vendor={:04x} device={:04x})",
|
||||||
|
gpu.index,
|
||||||
|
gpu.name,
|
||||||
|
if gpu.discrete {
|
||||||
|
"discrete"
|
||||||
|
} else {
|
||||||
|
"integrated"
|
||||||
|
},
|
||||||
|
gpu.vendor_id,
|
||||||
|
gpu.device_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,77 +448,127 @@ pub fn gpus() -> &'static [GpuInfo] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enumerate_vulkan_gpus() -> Vec<GpuInfo> {
|
/// True only in the disposable subprocess spawned by
|
||||||
use ash::vk;
|
/// [`preflight_saved_gpu_at_boot`]. This is checked at the top of `main`,
|
||||||
// SAFETY: `Entry::load` dlopens libvulkan; every call below is a plain
|
/// before single-instance ownership, navigation sentinels, audio or runtime
|
||||||
// enumeration against the instance we create and destroy here. Nothing
|
/// construction.
|
||||||
// escapes this function but owned `String`s and plain numbers, and the
|
pub fn gpu_preflight_child_requested() -> bool {
|
||||||
// instance is destroyed on every path.
|
std::env::var_os(GPU_PREFLIGHT_CHILD_ENV)
|
||||||
unsafe {
|
.is_some_and(|value| value.to_string_lossy().trim() == "1")
|
||||||
let Ok(entry) = ash::Entry::load() else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let app_info = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_0);
|
|
||||||
let create_info = vk::InstanceCreateInfo::default().application_info(&app_info);
|
|
||||||
let Ok(instance) = entry.create_instance(&create_info, None) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let devices = instance.enumerate_physical_devices().unwrap_or_default();
|
|
||||||
let mut out = Vec::with_capacity(devices.len());
|
|
||||||
for (i, pd) in devices.iter().enumerate() {
|
|
||||||
let props = instance.get_physical_device_properties(*pd);
|
|
||||||
let name = props
|
|
||||||
.device_name_as_c_str()
|
|
||||||
.ok()
|
|
||||||
.and_then(|s| s.to_str().ok())
|
|
||||||
.unwrap_or("")
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
if name.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.push(GpuInfo {
|
|
||||||
index: i as u32,
|
|
||||||
name,
|
|
||||||
discrete: props.device_type == vk::PhysicalDeviceType::DISCRETE_GPU,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
instance.destroy_instance(None);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The GPU whose index Vulkan would pick with no override — Qt's own default
|
/// Run the child half after its minimal QGuiApplication exists. The C++ helper
|
||||||
/// is device 0. Selecting it needs no coupling at all.
|
/// creates a transparent 2x2 QQuickWindow and keeps swapping long enough for
|
||||||
fn default_gpu_index() -> u32 {
|
/// Wayland to accept or fatally reject its DMA-BUFs.
|
||||||
0
|
pub fn run_gpu_preflight_child() -> i32 {
|
||||||
|
let identity = std::env::var(GPU_PREFLIGHT_IDENTITY_ENV).unwrap_or_default();
|
||||||
|
let name = std::env::var(GPU_PREFLIGHT_NAME_ENV).unwrap_or_default();
|
||||||
|
std::env::remove_var(GPU_PREFLIGHT_CHILD_ENV);
|
||||||
|
std::env::remove_var(GPU_PREFLIGHT_IDENTITY_ENV);
|
||||||
|
std::env::remove_var(GPU_PREFLIGHT_NAME_ENV);
|
||||||
|
|
||||||
|
if !cfg!(target_os = "linux") {
|
||||||
|
return GPU_PREFLIGHT_NOT_SELECTABLE;
|
||||||
|
}
|
||||||
|
let list = gpus();
|
||||||
|
if list.len() < 2 {
|
||||||
|
log::info!("[qbz-qt] gpu preflight: fewer than two selectable devices -> Auto");
|
||||||
|
return GPU_PREFLIGHT_NOT_SELECTABLE;
|
||||||
|
}
|
||||||
|
let Some(gpu) = resolve_gpu_in(list, &name, &identity) else {
|
||||||
|
log::warn!("[qbz-qt] gpu preflight: requested device '{name}' ({identity}) is not present");
|
||||||
|
return GPU_PREFLIGHT_NOT_FOUND;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::env::set_var("QSG_RHI_BACKEND", "vulkan");
|
||||||
|
std::env::set_var("QT_VK_PHYSICAL_DEVICE_INDEX", gpu.index.to_string());
|
||||||
|
log::info!(
|
||||||
|
"[qbz-qt] gpu preflight: probing '{}' ({}) at Qt index {}",
|
||||||
|
gpu.name,
|
||||||
|
gpu.identity,
|
||||||
|
gpu.index
|
||||||
|
);
|
||||||
|
// SAFETY: the helper is called on the child's GUI thread, after its sole
|
||||||
|
// QGuiApplication and before any other QQuickWindow. It owns all temporary
|
||||||
|
// Qt objects and returns only after destroying the probe window.
|
||||||
|
let result = unsafe { qbz_qt_vulkan_preflight_window() };
|
||||||
|
if result == 0 {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut stdout = std::io::stdout().lock();
|
||||||
|
let _ = writeln!(stdout, "QBZ_GPU_PREFLIGHT_OK={}", gpu.identity);
|
||||||
|
let _ = stdout.flush();
|
||||||
|
log::info!(
|
||||||
|
"[qbz-qt] gpu preflight: '{}' presented frames successfully",
|
||||||
|
gpu.name
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"[qbz-qt] gpu preflight: '{}' did not prove presentation (code {result})",
|
||||||
|
gpu.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a persisted `gpu_power` value to a device. Accepts a NAME (what
|
/// Resolve a persisted GPU pair to a device. Stable identity wins; without one,
|
||||||
/// this row and the reference both store) and the legacy CLASS keys the older
|
/// accept the model name older Qt builds store and the legacy class keys.
|
||||||
/// builds and the Slint app write, so a shared pref never reads as garbage.
|
|
||||||
///
|
///
|
||||||
/// `None` = Auto, or a value that matches no present device — which is exactly
|
/// `None` = Auto, or a value that matches no present device — which is exactly
|
||||||
/// the case that used to let you select a GPU that does not exist.
|
/// the case that used to let you select a GPU that does not exist.
|
||||||
pub fn resolve_gpu(pref: &str) -> Option<&'static GpuInfo> {
|
fn unique_gpu_matching(
|
||||||
|
list: &[GpuInfo],
|
||||||
|
mut predicate: impl FnMut(&GpuInfo) -> bool,
|
||||||
|
) -> Option<&GpuInfo> {
|
||||||
|
let mut matches = list.iter().filter(|gpu| predicate(gpu));
|
||||||
|
let first = matches.next()?;
|
||||||
|
matches.next().is_none().then_some(first)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_gpu_in<'a>(list: &'a [GpuInfo], pref: &str, identity: &str) -> Option<&'a GpuInfo> {
|
||||||
let pref = pref.trim();
|
let pref = pref.trim();
|
||||||
if pref.is_empty() || pref == "auto" {
|
if pref.is_empty() || pref == "auto" {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let list = gpus();
|
let identity = identity.trim();
|
||||||
if let Some(hit) = list.iter().find(|g| g.name == pref) {
|
if !identity.is_empty() {
|
||||||
|
if let Some(hit) = unique_gpu_matching(list, |gpu| gpu.identity == identity) {
|
||||||
|
// A downgraded/older build may update `gpu_power` without knowing
|
||||||
|
// about `gpu_identity`. If the readable key clearly names another
|
||||||
|
// device (or class), treat the identity as stale. Otherwise the
|
||||||
|
// UUID remains authoritative across driver model-name changes.
|
||||||
|
let names_another = list.iter().any(|gpu| gpu.name == pref) && hit.name != pref;
|
||||||
|
let class_disagrees =
|
||||||
|
matches!(pref, "discrete" | "integrated") && (hit.discrete != (pref == "discrete"));
|
||||||
|
if !names_another && !class_disagrees {
|
||||||
|
return Some(hit);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Never fall back from a missing UUID to a same-model device: that
|
||||||
|
// could be the other of two identical eGPUs.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(hit) = unique_gpu_matching(list, |gpu| gpu.name == pref) {
|
||||||
return Some(hit);
|
return Some(hit);
|
||||||
}
|
}
|
||||||
// Legacy class keys — resolve to a device that ACTUALLY EXISTS, or to
|
// Legacy class keys — resolve to a device that ACTUALLY EXISTS, or to
|
||||||
// nothing. "discrete" on a single-GPU laptop resolves to None, and that is
|
// nothing. "discrete" on a single-GPU laptop resolves to None, and that is
|
||||||
// the point.
|
// the point.
|
||||||
match pref {
|
match pref {
|
||||||
"discrete" => list.iter().find(|g| g.discrete),
|
"discrete" => unique_gpu_matching(list, |gpu| gpu.discrete),
|
||||||
"integrated" => list.iter().find(|g| !g.discrete),
|
"integrated" => unique_gpu_matching(list, |gpu| !gpu.discrete),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resolve_saved_gpu() -> Option<&'static GpuInfo> {
|
||||||
|
resolve_gpu_in(
|
||||||
|
gpus(),
|
||||||
|
&crate::settings_qt::pref_str("gpu_power", "auto"),
|
||||||
|
&crate::settings_qt::pref_str("gpu_identity", ""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Can this machine actually honour a Preferred-GPU choice?
|
/// Can this machine actually honour a Preferred-GPU choice?
|
||||||
///
|
///
|
||||||
/// Two conditions, and BOTH are the owner's point that a control must never
|
/// Two conditions, and BOTH are the owner's point that a control must never
|
||||||
@@ -420,30 +581,283 @@ pub fn gpu_selectable() -> bool {
|
|||||||
cfg!(target_os = "linux") && gpus().len() > 1
|
cfg!(target_os = "linux") && gpus().len() > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the persisted Preferred GPU. Called from `apply_renderer_preference`'s
|
// A GPU override can fail before QML exists (bad adapter, presentation, or a
|
||||||
/// slot — BEFORE `QGuiApplication`, because an env set after the graphics
|
// cross-device dmabuf import). Keep this separate from `qt_renderer_attempt`
|
||||||
/// context exists is a silent no-op (scoping trap 2).
|
// so a device failure never rewrites the renderer row and vice versa.
|
||||||
///
|
fn gpu_sentinel_path() -> Option<std::path::PathBuf> {
|
||||||
/// Deliberately class-level, exactly like the reference: it nudges the driver
|
Some(dirs::data_dir()?.join("qbz").join("qt_gpu_attempt"))
|
||||||
/// toward the integrated or the discrete GPU rather than binding one adapter.
|
|
||||||
/// Slint has the same limitation (documented F7) and so does Chromium
|
|
||||||
/// (`--force_high_performance_gpu`) — this is the ceiling of the platform, not
|
|
||||||
/// a shortcut.
|
|
||||||
pub fn apply_gpu_preference() {
|
|
||||||
if !gpu_selectable() {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// An explicit env always wins — someone who exported one of these meant it,
|
|
||||||
// and it is the escape hatch when this row guesses wrong.
|
fn clear_gpu_sentinel() {
|
||||||
for taken in [
|
if let Some(path) = gpu_sentinel_path() {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn armed_gpu_choice() -> Option<String> {
|
||||||
|
std::fs::read_to_string(gpu_sentinel_path()?)
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arm_gpu_sentinel(gpu: &GpuInfo) {
|
||||||
|
let Some(path) = gpu_sentinel_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
let marker = format!("{}|{}|qt-index={}", gpu.identity, gpu.name, gpu.index);
|
||||||
|
match std::fs::write(&path, marker) {
|
||||||
|
Ok(()) => log::info!("[qbz-qt] gpu: startup sentinel armed for '{}'", gpu.name),
|
||||||
|
Err(e) => log::warn!("[qbz-qt] gpu: could not arm the startup sentinel: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revert_if_previous_gpu_launch_died() -> bool {
|
||||||
|
let Some(choice) = armed_gpu_choice() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
clear_gpu_sentinel();
|
||||||
|
log::warn!(
|
||||||
|
"[qbz-qt] gpu: the previous launch forced '{choice}' and never reached liveness — \
|
||||||
|
reverting Preferred GPU to Auto"
|
||||||
|
);
|
||||||
|
crate::settings_qt::save_gpu_preference(None);
|
||||||
|
PREFLIGHT_FALLBACK_NOTICE.store(true, Ordering::SeqCst);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nonempty_env(name: &str) -> bool {
|
||||||
|
std::env::var_os(name).is_some_and(|value| !value.to_string_lossy().trim().is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gpu_override_env() -> Option<&'static str> {
|
||||||
|
[
|
||||||
"QT_VK_PHYSICAL_DEVICE_INDEX",
|
"QT_VK_PHYSICAL_DEVICE_INDEX",
|
||||||
"DRI_PRIME",
|
"DRI_PRIME",
|
||||||
"__NV_PRIME_RENDER_OFFLOAD",
|
"__NV_PRIME_RENDER_OFFLOAD",
|
||||||
] {
|
"__VK_LAYER_NV_optimus",
|
||||||
if std::env::var_os(taken).is_some() {
|
"MESA_VK_DEVICE_SELECT",
|
||||||
log::info!("[qbz-qt] gpu: {taken} already set; leaving the choice to it");
|
"VK_DRIVER_FILES",
|
||||||
|
"VK_ICD_FILENAMES",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.find(|name| nonempty_env(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mirror `main::apply_renderer_preference` just far enough to know whether a
|
||||||
|
/// saved GPU would be ignored anyway. A software/OpenGL renderer choice wins
|
||||||
|
/// over Preferred GPU, so probing Vulkan in that case would be both wasteful
|
||||||
|
/// and misleading.
|
||||||
|
fn renderer_blocks_gpu_preflight() -> bool {
|
||||||
|
if nonempty_env("QSG_RHI_BACKEND") || nonempty_env("QT_QUICK_BACKEND") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let from_env = std::env::var("QBZ_RENDERER")
|
||||||
|
.ok()
|
||||||
|
.map(|value| value.trim().to_ascii_lowercase())
|
||||||
|
.filter(|value| !value.is_empty() && value != "auto");
|
||||||
|
let choice = from_env.unwrap_or_else(|| crate::settings_qt::pref_str("renderer", "auto"));
|
||||||
|
renderer_choice_blocks_gpu(&choice)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderer_choice_blocks_gpu(choice: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
choice.trim().to_ascii_lowercase().as_str(),
|
||||||
|
"software" | "cpu" | "soft" | "gl" | "gles" | "femtovg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preflight_output_tail(output: &std::process::Output) -> String {
|
||||||
|
let text = format!(
|
||||||
|
"{}\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
String::from_utf8_lossy(&output.stdout)
|
||||||
|
);
|
||||||
|
let lines: Vec<&str> = text
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.collect();
|
||||||
|
let tail = lines[lines.len().saturating_sub(6)..].join(" | ");
|
||||||
|
tail.chars().take(1200).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approved_identity_from_stdout(stdout: &[u8]) -> Option<String> {
|
||||||
|
String::from_utf8_lossy(stdout)
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("QBZ_GPU_PREFLIGHT_OK="))
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|identity| !identity.is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approved_identity_from(output: &std::process::Output) -> Option<String> {
|
||||||
|
approved_identity_from_stdout(&output.stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_gpu_preflight(name: &str, identity: &str) -> Result<(std::process::Output, bool), String> {
|
||||||
|
let executable = std::env::current_exe()
|
||||||
|
.map_err(|error| format!("cannot resolve current executable: {error}"))?;
|
||||||
|
let mut child = std::process::Command::new(executable)
|
||||||
|
.env(GPU_PREFLIGHT_CHILD_ENV, "1")
|
||||||
|
.env(GPU_PREFLIGHT_IDENTITY_ENV, identity)
|
||||||
|
.env(GPU_PREFLIGHT_NAME_ENV, name)
|
||||||
|
// An activation token is single-use. The invisible probe must never
|
||||||
|
// consume the token intended to focus the real main window.
|
||||||
|
.env_remove("XDG_ACTIVATION_TOKEN")
|
||||||
|
.env_remove("DESKTOP_STARTUP_ID")
|
||||||
|
.stdin(std::process::Stdio::null())
|
||||||
|
.stdout(std::process::Stdio::piped())
|
||||||
|
.stderr(std::process::Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|error| format!("cannot spawn child: {error}"))?;
|
||||||
|
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
let mut timed_out = false;
|
||||||
|
loop {
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(_)) => break,
|
||||||
|
Ok(None) if started.elapsed() < GPU_PREFLIGHT_PARENT_TIMEOUT => {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(25));
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
timed_out = true;
|
||||||
|
let _ = child.kill();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
return Err(format!("cannot monitor child: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let output = child
|
||||||
|
.wait_with_output()
|
||||||
|
.map_err(|error| format!("cannot collect child result: {error}"))?;
|
||||||
|
Ok((output, timed_out))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_preflight_approved(identity: Option<String>) {
|
||||||
|
let mut slot = PREFLIGHT_APPROVED_GPU
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
|
*slot = identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preflight_approved(identity: &str) -> bool {
|
||||||
|
PREFLIGHT_APPROVED_GPU
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner())
|
||||||
|
.as_deref()
|
||||||
|
== Some(identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject_saved_gpu(reason: &str) {
|
||||||
|
set_preflight_approved(None);
|
||||||
|
clear_gpu_sentinel();
|
||||||
|
crate::settings_qt::save_gpu_preference(None);
|
||||||
|
PREFLIGHT_FALLBACK_NOTICE.store(true, Ordering::SeqCst);
|
||||||
|
log::warn!(
|
||||||
|
"[qbz-qt] gpu preflight: {reason}; Preferred GPU reverted to Auto before main startup"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-shot UI notice consumed after QbzShell registers its Qt-thread hop.
|
||||||
|
pub fn take_preflight_fallback_notice() -> bool {
|
||||||
|
PREFLIGHT_FALLBACK_NOTICE.swap(false, Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate an explicit saved GPU in a disposable process before this process
|
||||||
|
/// constructs its runtime, audio thread or QGuiApplication. A fatal Wayland
|
||||||
|
/// DMA-BUF import error can then kill only the child; the parent persists Auto
|
||||||
|
/// and continues through the normal safe startup.
|
||||||
|
pub fn preflight_saved_gpu_at_boot() {
|
||||||
|
set_preflight_approved(None);
|
||||||
|
if !cfg!(target_os = "linux") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Renderer recovery may turn a previously forced software/OpenGL choice
|
||||||
|
// into Auto. Do it before deciding whether a Vulkan GPU probe is relevant.
|
||||||
|
let _ = revert_if_previous_launch_died();
|
||||||
|
if revert_if_previous_gpu_launch_died() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(name) = gpu_override_env() {
|
||||||
|
log::info!(
|
||||||
|
"[qbz-qt] gpu preflight: {name} is explicitly set; leaving external policy untouched"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if renderer_blocks_gpu_preflight() {
|
||||||
|
log::info!(
|
||||||
|
"[qbz-qt] gpu preflight: explicit software/OpenGL renderer wins; no GPU probe needed"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = crate::settings_qt::pref_str("gpu_power", "auto");
|
||||||
|
let identity = crate::settings_qt::pref_str("gpu_identity", "");
|
||||||
|
if name.trim().is_empty() || name == "auto" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"[qbz-qt] gpu preflight: validating saved device '{name}' ({identity}) in an isolated child"
|
||||||
|
);
|
||||||
|
let (output, timed_out) = match spawn_gpu_preflight(&name, &identity) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(error) => {
|
||||||
|
reject_saved_gpu(&error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if timed_out {
|
||||||
|
reject_saved_gpu("child exceeded the 8-second watchdog");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if output.status.success() {
|
||||||
|
if let Some(approved) = approved_identity_from(&output) {
|
||||||
|
log::info!(
|
||||||
|
"[qbz-qt] gpu preflight: presentation approved for {approved}; continuing main startup"
|
||||||
|
);
|
||||||
|
set_preflight_approved(Some(approved));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject_saved_gpu("child exited successfully without an identity proof");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = output.status.code();
|
||||||
|
let detail = preflight_output_tail(&output);
|
||||||
|
if code == Some(GPU_PREFLIGHT_NOT_SELECTABLE) {
|
||||||
|
reject_saved_gpu("this environment exposes fewer than two selectable GPUs");
|
||||||
|
} else if code == Some(GPU_PREFLIGHT_NOT_FOUND) {
|
||||||
|
reject_saved_gpu("the saved GPU identity is no longer present");
|
||||||
|
} else if detail.is_empty() {
|
||||||
|
reject_saved_gpu(&format!("child failed with status {:?}", output.status));
|
||||||
|
} else {
|
||||||
|
reject_saved_gpu(&format!(
|
||||||
|
"child failed with status {:?}; last output: {detail}",
|
||||||
|
output.status
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply Preferred GPU after QGuiApplication installs the platform integration
|
||||||
|
/// and before QQmlApplicationEngine can construct a QQuickWindow. This is the
|
||||||
|
/// only interval where we can enumerate Qt's actual VkInstance and still set
|
||||||
|
/// the environment QRhi consumes.
|
||||||
|
pub fn apply_gpu_preference() {
|
||||||
|
let reverted = revert_if_previous_gpu_launch_died();
|
||||||
|
|
||||||
|
// An explicit env always wins — someone who exported one of these meant it,
|
||||||
|
// including vendor/ICD selectors that can change the inventory itself.
|
||||||
|
if let Some(taken) = gpu_override_env() {
|
||||||
|
log::info!("[qbz-qt] gpu: {taken} already set; leaving the choice to it");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// `apply_renderer_preference()` runs FIRST and may have forced a backend.
|
// `apply_renderer_preference()` runs FIRST and may have forced a backend.
|
||||||
// A GPU pick needs Vulkan (see below), so it cannot also honour an explicit
|
// A GPU pick needs Vulkan (see below), so it cannot also honour an explicit
|
||||||
@@ -458,22 +872,56 @@ pub fn apply_gpu_preference() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let pref = crate::settings_qt::pref_str("gpu_power", "auto");
|
|
||||||
let Some(gpu) = resolve_gpu(&pref) else {
|
let pref = if reverted {
|
||||||
if pref != "auto" && !pref.is_empty() {
|
"auto".to_string()
|
||||||
log::warn!("[qbz-qt] gpu: '{pref}' matches no device present on this machine -> Auto");
|
} else {
|
||||||
}
|
crate::settings_qt::pref_str("gpu_power", "auto")
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
if gpu.index == default_gpu_index() {
|
let identity = if reverted {
|
||||||
// Already the default: selecting it needs no override, and forcing one
|
String::new()
|
||||||
// would buy the Vulkan coupling below for nothing.
|
} else {
|
||||||
|
crate::settings_qt::pref_str("gpu_identity", "")
|
||||||
|
};
|
||||||
|
if pref.trim().is_empty() || pref == "auto" {
|
||||||
|
clear_gpu_sentinel();
|
||||||
|
log::info!("[qbz-qt] gpu: Auto -> Qt/driver environment default (no override)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !gpu_selectable() {
|
||||||
|
clear_gpu_sentinel();
|
||||||
log::info!(
|
log::info!(
|
||||||
"[qbz-qt] gpu: '{}' is device 0 (the default) -> no override",
|
"[qbz-qt] gpu: '{pref}' requested, but this environment exposes fewer than two \
|
||||||
gpu.name
|
selectable GPUs -> Auto"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let Some(gpu) = resolve_gpu_in(gpus(), &pref, &identity) else {
|
||||||
|
clear_gpu_sentinel();
|
||||||
|
log::warn!(
|
||||||
|
"[qbz-qt] gpu: saved device '{pref}' ({identity}) is not present in Qt's inventory -> Auto"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A saved GPU is never allowed to reach the real window merely because it
|
||||||
|
// exists. The disposable child must have presented Qt Quick frames from
|
||||||
|
// this exact stable identity in the current process launch first.
|
||||||
|
if !preflight_approved(&gpu.identity) {
|
||||||
|
reject_saved_gpu(&format!(
|
||||||
|
"no successful presentation proof exists for '{}' ({})",
|
||||||
|
gpu.name, gpu.identity
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate model/class-only preferences once. The model stays for readable
|
||||||
|
// backward compatibility; the UUID becomes authoritative.
|
||||||
|
if pref != gpu.name || identity != gpu.identity {
|
||||||
|
crate::settings_qt::save_gpu_preference(Some(gpu));
|
||||||
|
log::info!("[qbz-qt] gpu: migrated Preferred GPU to stable identity");
|
||||||
|
}
|
||||||
|
|
||||||
// THE VULKAN COUPLING, and why it is the only honest route.
|
// THE VULKAN COUPLING, and why it is the only honest route.
|
||||||
//
|
//
|
||||||
// MEASURED 2026-08-11 on the Intel-Arc + RTX-4070 hybrid (Wayland,
|
// MEASURED 2026-08-11 on the Intel-Arc + RTX-4070 hybrid (Wayland,
|
||||||
@@ -492,15 +940,17 @@ pub fn apply_gpu_preference() {
|
|||||||
// is what killed the NPB Large visualiser when this row first shipped the
|
// is what killed the NPB Large visualiser when this row first shipped the
|
||||||
// PRIME envs. `QT_OPENGL=desktop` does not rescue it (measured).
|
// PRIME envs. `QT_OPENGL=desktop` does not rescue it (measured).
|
||||||
//
|
//
|
||||||
// Vulkan has none of that problem: the same .qsb already carries SPIR-V,
|
// Vulkan has none of that problem: the same .qsb already carries SPIR-V.
|
||||||
// and `QT_VK_PHYSICAL_DEVICE_INDEX` counts the SAME order this module
|
// Every explicit device — including Qt index 0 — forces Vulkan, otherwise
|
||||||
// enumerated. So picking a non-default GPU implies the Vulkan backend —
|
// an OpenGL default could silently use another adapter. The index below
|
||||||
// stated in the row's own description, never silent.
|
// came from Qt's own persistent VkInstance for this launch.
|
||||||
|
arm_gpu_sentinel(gpu);
|
||||||
std::env::set_var("QSG_RHI_BACKEND", "vulkan");
|
std::env::set_var("QSG_RHI_BACKEND", "vulkan");
|
||||||
std::env::set_var("QT_VK_PHYSICAL_DEVICE_INDEX", gpu.index.to_string());
|
std::env::set_var("QT_VK_PHYSICAL_DEVICE_INDEX", gpu.index.to_string());
|
||||||
log::info!(
|
log::info!(
|
||||||
"[qbz-qt] gpu: '{}' -> QSG_RHI_BACKEND=vulkan QT_VK_PHYSICAL_DEVICE_INDEX={}",
|
"[qbz-qt] gpu: '{}' ({}) -> QSG_RHI_BACKEND=vulkan QT_VK_PHYSICAL_DEVICE_INDEX={}",
|
||||||
gpu.name,
|
gpu.name,
|
||||||
|
gpu.identity,
|
||||||
gpu.index
|
gpu.index
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -553,4 +1003,117 @@ mod tests {
|
|||||||
assert!(reduce_motion(true));
|
assert!(reduce_motion(true));
|
||||||
GPU_TIER.store(true, Ordering::SeqCst);
|
GPU_TIER.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn raw_gpu(index: u32, name: &str, device_type: u32, uuid: &str) -> QtGpuJson {
|
||||||
|
QtGpuJson {
|
||||||
|
index,
|
||||||
|
name: name.to_string(),
|
||||||
|
vendor: 0x10de,
|
||||||
|
device: 0x2860,
|
||||||
|
device_type,
|
||||||
|
uuid: uuid.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn qt_inventory_keeps_qt_indices_and_filters_cpu_adapters() {
|
||||||
|
let list = gpu_inventory_from_raw(vec![
|
||||||
|
raw_gpu(2, "lavapipe", 4, ""),
|
||||||
|
raw_gpu(1, "Intel Arc", 1, "8680557d080000000002000000000000"),
|
||||||
|
raw_gpu(0, "NVIDIA RTX", 2, "4e8f268581e1cbdcac12c7c15fa6624e"),
|
||||||
|
]);
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
assert_eq!(list[0].index, 0);
|
||||||
|
assert_eq!(list[0].name, "NVIDIA RTX");
|
||||||
|
assert_eq!(list[1].index, 1);
|
||||||
|
assert_eq!(list[1].name, "Intel Arc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stable_identity_wins_over_duplicate_model_names_and_index_changes() {
|
||||||
|
let first = GpuInfo {
|
||||||
|
index: 1,
|
||||||
|
name: "Same GPU".into(),
|
||||||
|
vendor_id: 1,
|
||||||
|
device_id: 2,
|
||||||
|
identity: "vk:first".into(),
|
||||||
|
discrete: true,
|
||||||
|
};
|
||||||
|
let wanted = GpuInfo {
|
||||||
|
index: 0,
|
||||||
|
name: "Same GPU".into(),
|
||||||
|
vendor_id: 1,
|
||||||
|
device_id: 2,
|
||||||
|
identity: "vk:wanted".into(),
|
||||||
|
discrete: true,
|
||||||
|
};
|
||||||
|
let list = vec![first, wanted];
|
||||||
|
assert_eq!(
|
||||||
|
resolve_gpu_in(&list, "Same GPU", "vk:wanted").map(|gpu| gpu.index),
|
||||||
|
Some(0)
|
||||||
|
);
|
||||||
|
assert!(resolve_gpu_in(&list, "Same GPU", "vk:missing").is_none());
|
||||||
|
assert!(
|
||||||
|
resolve_gpu_in(&list, "Same GPU", "").is_none(),
|
||||||
|
"a legacy name may not guess between identical devices"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
resolve_gpu_in(&list, "discrete", "").is_none(),
|
||||||
|
"a legacy class may not guess between multiple discrete GPUs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn newer_readable_choice_can_replace_identity_left_by_an_older_build() {
|
||||||
|
let integrated = GpuInfo {
|
||||||
|
index: 0,
|
||||||
|
name: "Integrated".into(),
|
||||||
|
vendor_id: 1,
|
||||||
|
device_id: 1,
|
||||||
|
identity: "vk:integrated".into(),
|
||||||
|
discrete: false,
|
||||||
|
};
|
||||||
|
let discrete = GpuInfo {
|
||||||
|
index: 1,
|
||||||
|
name: "Discrete".into(),
|
||||||
|
vendor_id: 2,
|
||||||
|
device_id: 2,
|
||||||
|
identity: "vk:discrete".into(),
|
||||||
|
discrete: true,
|
||||||
|
};
|
||||||
|
let list = vec![integrated, discrete];
|
||||||
|
assert_eq!(
|
||||||
|
resolve_gpu_in(&list, "Discrete", "vk:integrated").map(|gpu| gpu.index),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uuid_fallback_is_deterministic_when_driver_exposes_none() {
|
||||||
|
let raw = raw_gpu(0, " Example GPU ", 2, "00000000000000000000000000000000");
|
||||||
|
assert_eq!(stable_gpu_identity(&raw), "pci:10de:2860:example gpu");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn child_success_marker_requires_an_exact_nonempty_identity() {
|
||||||
|
assert_eq!(
|
||||||
|
approved_identity_from_stdout(
|
||||||
|
b"noise\nQBZ_GPU_PREFLIGHT_OK=vk:device-uuid\nmore noise\n"
|
||||||
|
)
|
||||||
|
.as_deref(),
|
||||||
|
Some("vk:device-uuid")
|
||||||
|
);
|
||||||
|
assert!(approved_identity_from_stdout(b"QBZ_GPU_PREFLIGHT_OK= \n").is_none());
|
||||||
|
assert!(approved_identity_from_stdout(b"prefix QBZ_GPU_PREFLIGHT_OK=vk:x\n").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_renderer_choices_that_preclude_vulkan_block_the_probe() {
|
||||||
|
for choice in ["software", "cpu", "soft", "gl", "gles", "femtovg"] {
|
||||||
|
assert!(renderer_choice_blocks_gpu(choice), "{choice}");
|
||||||
|
}
|
||||||
|
for choice in ["auto", "wgpu", "gpu", "hardware", "hw", "unknown"] {
|
||||||
|
assert!(!renderer_choice_blocks_gpu(choice), "{choice}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1321,6 +1321,20 @@ pub fn save_pref(key: &str, value: serde_json::Value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Persist Preferred GPU as one atomic pair: the readable model name retained
|
||||||
|
/// for older builds plus the stable Vulkan identity used by the Qt launcher.
|
||||||
|
/// The process-local Qt index is deliberately never written.
|
||||||
|
pub(crate) fn save_gpu_preference(gpu: Option<&crate::renderer_qt::GpuInfo>) {
|
||||||
|
let (name, identity) = gpu
|
||||||
|
.map(|gpu| (gpu.name.as_str(), gpu.identity.as_str()))
|
||||||
|
.unwrap_or(("auto", ""));
|
||||||
|
update_prefs(|doc| {
|
||||||
|
doc.insert("gpu_power".to_string(), serde_json::json!(name));
|
||||||
|
doc.insert("gpu_identity".to_string(), serde_json::json!(identity));
|
||||||
|
true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Tray store (qbz_app::settings::tray — per-user tray_settings.db, the SAME
|
// Tray store (qbz_app::settings::tray — per-user tray_settings.db, the SAME
|
||||||
// file the Slint tray_settings.rs glue writes).
|
// file the Slint tray_settings.rs glue writes).
|
||||||
static TRAY: OnceLock<qbz_app::settings::tray::TraySettingsState> = OnceLock::new();
|
static TRAY: OnceLock<qbz_app::settings::tray::TraySettingsState> = OnceLock::new();
|
||||||
@@ -1563,11 +1577,11 @@ fn gpu_power_choice() -> (Vec<String>, i32) {
|
|||||||
let gpus = crate::renderer_qt::gpus();
|
let gpus = crate::renderer_qt::gpus();
|
||||||
let mut opts = vec![qbz_i18n::t("Auto (recommended)")];
|
let mut opts = vec![qbz_i18n::t("Auto (recommended)")];
|
||||||
opts.extend(gpus.iter().map(|g| g.label()));
|
opts.extend(gpus.iter().map(|g| g.label()));
|
||||||
// Index 0 is Auto, so a device sits at its enumeration index + 1. An
|
// Index 0 is Auto; UI positions are independent of Qt's process-local
|
||||||
// unmatched pref (a name from another machine, or a legacy class key with
|
// Vulkan indices (which can contain gaps when a CPU adapter is filtered).
|
||||||
// no such device) resolves to Auto.
|
let index = crate::renderer_qt::resolve_saved_gpu()
|
||||||
let index = crate::renderer_qt::resolve_gpu(&pref_str("gpu_power", "auto"))
|
.and_then(|selected| gpus.iter().position(|gpu| gpu.identity == selected.identity))
|
||||||
.map(|g| g.index as i32 + 1)
|
.map(|position| position as i32 + 1)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
(opts, index)
|
(opts, index)
|
||||||
}
|
}
|
||||||
@@ -2943,23 +2957,17 @@ pub async fn settings_select(runtime: &Arc<AppRuntime<LoggingAdapter>>, key: &st
|
|||||||
crate::toast_qt::info(qbz_i18n::t("Renderer changed — restart QBZ to apply"));
|
crate::toast_qt::info(qbz_i18n::t("Renderer changed — restart QBZ to apply"));
|
||||||
}
|
}
|
||||||
"gpu-power" => {
|
"gpu-power" => {
|
||||||
// Index 0 is Auto; every other index is a REAL enumerated device,
|
// Index 0 is Auto; every other position is a real Qt-enumerated
|
||||||
// stored by NAME — the same shape the reference persists, so the
|
// device. Persist model + stable identity atomically, never the
|
||||||
// shared ui_prefs round-trips between the two apps. The old arm
|
// process-local Qt Vulkan index.
|
||||||
// accepted only index 0 and silently discarded the rest
|
|
||||||
// (PARITY-DEBT #83).
|
|
||||||
let gpus = crate::renderer_qt::gpus();
|
let gpus = crate::renderer_qt::gpus();
|
||||||
let value = if index <= 0 {
|
let selected = if index <= 0 {
|
||||||
"auto".to_string()
|
None
|
||||||
} else {
|
} else {
|
||||||
match gpus.get((index - 1) as usize) {
|
gpus.get((index - 1) as usize)
|
||||||
Some(g) => g.name.clone(),
|
|
||||||
// The list moved under the user (a GPU was unplugged, or
|
|
||||||
// the document is stale) — Auto rather than a wrong device.
|
|
||||||
None => "auto".to_string(),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
save_pref("gpu_power", serde_json::json!(value));
|
save_gpu_preference(selected);
|
||||||
|
let value = selected.map(|gpu| gpu.name.as_str()).unwrap_or("auto");
|
||||||
log::info!("[qbz-qt] gpu_power -> {value} (restart to apply)");
|
log::info!("[qbz-qt] gpu_power -> {value} (restart to apply)");
|
||||||
crate::toast_qt::info(qbz_i18n::t("Preferred GPU changed — restart QBZ to apply"));
|
crate::toast_qt::info(qbz_i18n::t("Preferred GPU changed — restart QBZ to apply"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,9 +458,9 @@ pub mod qbz_shell {
|
|||||||
/// with the number of frames the window actually swapped in it.
|
/// with the number of frames the window actually swapped in it.
|
||||||
///
|
///
|
||||||
/// Proof of frames — not merely of a window — is what disarms the
|
/// Proof of frames — not merely of a window — is what disarms the
|
||||||
/// startup sentinel: a backend can create a window and then never
|
/// graphics startup sentinels: a backend or GPU can create a window and
|
||||||
/// present, which looks identical to a healthy start until the user
|
/// then never present, which looks identical to a healthy start until
|
||||||
/// sees a frozen pane.
|
/// the user sees a frozen pane.
|
||||||
#[qinvokable]
|
#[qinvokable]
|
||||||
fn report_frame_liveness(self: Pin<&mut QbzShell>, frames: i32);
|
fn report_frame_liveness(self: Pin<&mut QbzShell>, frames: i32);
|
||||||
|
|
||||||
@@ -1090,6 +1090,11 @@ impl qbz_shell::QbzShell {
|
|||||||
} else {
|
} else {
|
||||||
start_shell_pulse();
|
start_shell_pulse();
|
||||||
}
|
}
|
||||||
|
if crate::renderer_qt::take_preflight_fallback_notice() {
|
||||||
|
crate::toast_qt::warning(qbz_i18n::t(
|
||||||
|
"Selected GPU is unavailable in this display session — using Auto",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The renderer probe's one-shot report (see the declaration).
|
/// The renderer probe's one-shot report (see the declaration).
|
||||||
@@ -1138,11 +1143,11 @@ impl qbz_shell::QbzShell {
|
|||||||
// matters.
|
// matters.
|
||||||
if crate::renderer_qt::sentinel_armed() {
|
if crate::renderer_qt::sentinel_armed() {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"[qbz-qt] renderer: the liveness window closed with NO frames rendered — \
|
"[qbz-qt] graphics: the liveness window closed with NO frames rendered — \
|
||||||
leaving the startup sentinel armed so the next launch reverts to auto"
|
leaving the startup sentinel armed so the next launch reverts to Auto"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log::debug!("[qbz-qt] renderer: no frames in the liveness window (nothing armed)");
|
log::debug!("[qbz-qt] graphics: no frames in the liveness window (nothing armed)");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user