fix(renderer): rescue failed Linux Auto presentation per launch
Probe Qt's default backend in a disposable graphics-only child before runtime and the main window. After proven failure, allow one software presentation attempt without persisting a renderer preference. Preserve explicit backend policy and saved-GPU validation. Bound and reap children, keep deep links in the parent, and avoid blocked diagnostic pipes. Cover healthy/failing decisions, overrides, missing executables, paths with spaces and watchdog cleanup in the passing Qt suite. cx
This commit is contained in:
7 files changed
+363
-13
No files matched your search
Generated
+1
@@ -4582,6 +4582,7 @@ dependencies = [
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlencoding",
|
||||
|
||||
@@ -125,6 +125,7 @@ qbz-player = { path = "../qbz-player" }
|
||||
# (StreamExt over the response byte stream).
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
futures-util = { workspace = true }
|
||||
tempfile = "3"
|
||||
async-trait = "0.1"
|
||||
zeroize = "1"
|
||||
|
||||
|
||||
@@ -110,13 +110,13 @@ extern "C" const char *qbz_qt_vulkan_devices_json()
|
||||
// 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()
|
||||
static int presentationPreflight(bool autoBackend)
|
||||
{
|
||||
if (qobject_cast<QGuiApplication *>(QCoreApplication::instance()) == nullptr)
|
||||
return 71;
|
||||
|
||||
QQuickWindow window;
|
||||
window.setTitle(QStringLiteral("QBZ GPU preflight"));
|
||||
window.setTitle(QStringLiteral("QBZ graphics preflight"));
|
||||
window.setFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowDoesNotAcceptFocus);
|
||||
window.resize(2, 2);
|
||||
window.setColor(Qt::transparent);
|
||||
@@ -139,8 +139,14 @@ extern "C" int qbz_qt_vulkan_preflight_window()
|
||||
QObject::connect(&pulse, &QTimer::timeout, &window, &QQuickWindow::update);
|
||||
QObject::connect(&window, &QQuickWindow::frameSwapped, &loop, [&] {
|
||||
++frames;
|
||||
if (frames == 1)
|
||||
if (autoBackend && frames >= 3) {
|
||||
// Pump successive native presentations, not just context creation.
|
||||
// Auto has no fixed grace sleep on the healthy startup path.
|
||||
result = 0;
|
||||
loop.quit();
|
||||
} else if (!autoBackend && frames == 1) {
|
||||
settle.start(700);
|
||||
}
|
||||
window.update();
|
||||
});
|
||||
QObject::connect(&window, &QQuickWindow::sceneGraphError, &loop,
|
||||
@@ -173,3 +179,15 @@ extern "C" int qbz_qt_vulkan_preflight_window()
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C" int qbz_qt_vulkan_preflight_window()
|
||||
{
|
||||
return presentationPreflight(false);
|
||||
}
|
||||
|
||||
// Uses Qt's actual default (OpenGL on Linux), or software selected BEFORE
|
||||
// QGuiApplication. No Vulkan inventory or multiple-adapter requirement.
|
||||
extern "C" int qbz_qt_auto_preflight_window()
|
||||
{
|
||||
return presentationPreflight(true);
|
||||
}
|
||||
@@ -3548,6 +3548,15 @@ fn apply_renderer_preference() {
|
||||
// backend must be undone even when this one is being overridden by env.
|
||||
let reverted = renderer_qt::revert_if_previous_launch_died();
|
||||
|
||||
// Whitespace is not a backend choice. Qt itself does not trim every env
|
||||
// entry; normalize these before either the child or parent constructs Qt.
|
||||
#[cfg(target_os = "linux")]
|
||||
for name in ["QSG_RHI_BACKEND", "QT_QUICK_BACKEND"] {
|
||||
if std::env::var_os(name).is_some() && !qt_backend_env_overrides(name) {
|
||||
std::env::remove_var(name);
|
||||
}
|
||||
}
|
||||
|
||||
if qt_backend_env_overrides("QSG_RHI_BACKEND") || qt_backend_env_overrides("QT_QUICK_BACKEND") {
|
||||
log::info!("[renderer] explicit Qt backend env present; leaving the choice to it");
|
||||
return;
|
||||
@@ -3766,6 +3775,14 @@ fn main() {
|
||||
// Before even the disposable GPU-preflight QGuiApplication. A child
|
||||
// spawned later inherits the resolved factor and takes the same path.
|
||||
apply_interface_scale_preference();
|
||||
#[cfg(target_os = "linux")]
|
||||
if renderer_qt::auto_preflight::child_requested() {
|
||||
let app = QGuiApplication::new();
|
||||
let result = renderer_qt::auto_preflight::run_child();
|
||||
drop(app);
|
||||
log::logger().flush();
|
||||
std::process::exit(result);
|
||||
}
|
||||
// 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
|
||||
@@ -3788,6 +3805,14 @@ fn main() {
|
||||
// user-visible. The child uses its own Wayland connection; fatal protocol
|
||||
// errors cannot take this parent down.
|
||||
renderer_qt::preflight_saved_gpu_at_boot();
|
||||
#[cfg(target_os = "linux")]
|
||||
apply_renderer_preference();
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Err(reason) = renderer_qt::auto_preflight::at_boot() {
|
||||
log::error!("[renderer] startup stopped before runtime/Qt: {reason}");
|
||||
log::logger().flush();
|
||||
std::process::exit(78);
|
||||
}
|
||||
// Same decision, different primitive: a per-session named mutex arbitrates
|
||||
// and a named pipe carries the handoff. `acquire_or_raise` has already
|
||||
// forwarded this launch's deep link (or a bare PRESENT) by the time it
|
||||
@@ -3905,6 +3930,7 @@ fn main() {
|
||||
// slot: QGuiApplication must first install the platform integration so we
|
||||
// can enumerate Qt's real QVulkanInstance, but the selection env must land
|
||||
// before the first QQuickWindow/QRhi is constructed.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
apply_renderer_preference();
|
||||
apply_scroll_physics();
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
//! Linux managed Auto: presentation proof before the parent constructs Qt.
|
||||
//! Children never run auth, audio, IPC ownership or navigation recovery.
|
||||
|
||||
use super::*;
|
||||
use std::process::{Command, Output};
|
||||
|
||||
const CHILD_ENV: &str = "QBZ_INTERNAL_AUTO_PREFLIGHT_CHILD";
|
||||
const PROOF: &str = "QBZ_AUTO_PREFLIGHT_OK";
|
||||
static SOFTWARE_NOTICE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn child_requested() -> bool {
|
||||
std::env::var(CHILD_ENV).is_ok_and(|value| value == "1")
|
||||
}
|
||||
|
||||
pub fn run_child() -> i32 {
|
||||
std::env::remove_var(CHILD_ENV);
|
||||
// SAFETY: main calls only on the child's GUI thread, after constructing
|
||||
// its sole QGuiApplication. C++ owns and destroys the temporary window.
|
||||
let result = unsafe { qbz_qt_auto_preflight_window() };
|
||||
if result == 0 {
|
||||
use std::io::Write;
|
||||
let mut stdout = std::io::stdout().lock();
|
||||
let _ = writeln!(stdout, "{PROOF}");
|
||||
let _ = stdout.flush();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn take_software_notice() -> bool {
|
||||
SOFTWARE_NOTICE.swap(false, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
fn external_backend(qsg: &str, quick: &str, qbz: &str) -> bool {
|
||||
!qsg.trim().is_empty()
|
||||
|| !quick.trim().is_empty()
|
||||
|| (!qbz.trim().is_empty() && !qbz.trim().eq_ignore_ascii_case("auto"))
|
||||
}
|
||||
|
||||
fn command_for(executable: &std::path::Path, software: bool) -> Command {
|
||||
// Deliberately no parent arguments: a deep link belongs to the parent.
|
||||
// PRIME/ICD and QPA selection are inherited exactly as the parent uses them.
|
||||
let mut command = Command::new(executable);
|
||||
command.env(CHILD_ENV, "1");
|
||||
if software {
|
||||
command.env("QT_QUICK_BACKEND", "software");
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
fn proof(result: &(Output, bool)) -> Result<(), String> {
|
||||
let (output, timed_out) = result;
|
||||
if *timed_out {
|
||||
return Err("child exceeded the 8-second watchdog (killed and reaped)".into());
|
||||
}
|
||||
if output.status.success()
|
||||
&& String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.any(|line| line == PROOF)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
"child status {}; presentation not proven; {}",
|
||||
output.status,
|
||||
preflight_output_tail(output)
|
||||
))
|
||||
}
|
||||
|
||||
/// At most Auto then software. A spawn/monitoring failure is an infrastructure
|
||||
/// error, not proof of bad graphics; report it and stop without rewriting prefs.
|
||||
fn decide(mut probe: impl FnMut(bool) -> Result<(Output, bool), String>) -> Result<bool, String> {
|
||||
match proof(&probe(false)?) {
|
||||
Ok(()) => Ok(false),
|
||||
Err(reason) => {
|
||||
log::warn!("[renderer] Auto presentation failed: {reason}; trying software once");
|
||||
proof(&probe(true)?)
|
||||
.map_err(|reason| format!("software presentation also failed: {reason}"))?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run after saved-GPU validation and apply_renderer_preference, but before
|
||||
/// runtime/audio/QGuiApplication. Backend envs now include a forced saved
|
||||
/// renderer, so that policy takes precedence too. No Vulkan enumeration here.
|
||||
pub fn at_boot() -> Result<(), String> {
|
||||
let env = |name| std::env::var(name).unwrap_or_default();
|
||||
if external_backend(
|
||||
&env("QSG_RHI_BACKEND"),
|
||||
&env("QT_QUICK_BACKEND"),
|
||||
&env("QBZ_RENDERER"),
|
||||
) {
|
||||
log::info!("[renderer] explicit backend policy preserved; managed Auto probe skipped");
|
||||
return Ok(());
|
||||
}
|
||||
if PREFLIGHT_APPROVED_GPU
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.is_some()
|
||||
{
|
||||
// The parent will use this exact Vulkan identity, already presented.
|
||||
return Ok(());
|
||||
}
|
||||
if matches!(
|
||||
env("QT_QPA_PLATFORM").trim().split(':').next(),
|
||||
Some("offscreen" | "minimal" | "vnc")
|
||||
) {
|
||||
log::info!("[renderer] non-desktop QPA; managed presentation probe skipped");
|
||||
return Ok(());
|
||||
}
|
||||
let executable =
|
||||
std::env::current_exe().map_err(|e| format!("cannot resolve executable: {e}"))?;
|
||||
let start = std::time::Instant::now();
|
||||
let software = decide(|software| {
|
||||
let started = std::time::Instant::now();
|
||||
let result = run_preflight_child(
|
||||
&mut command_for(&executable, software),
|
||||
GPU_PREFLIGHT_PARENT_TIMEOUT,
|
||||
);
|
||||
log::info!(
|
||||
"[renderer] {} presentation probe completed in {} ms",
|
||||
if software { "software" } else { "Auto" },
|
||||
started.elapsed().as_millis()
|
||||
);
|
||||
result
|
||||
})?;
|
||||
if software {
|
||||
// This launch only. No sentinel or preference changes, so a recovered
|
||||
// driver gets another Auto attempt on the next ordinary launch.
|
||||
std::env::set_var("QT_QUICK_BACKEND", "software");
|
||||
SOFTWARE_NOTICE.store(true, Ordering::SeqCst);
|
||||
log::warn!(
|
||||
"[renderer] software presentation confirmed; rescue for this launch only ({} ms total)",
|
||||
start.elapsed().as_millis()
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"[renderer] Auto presentation confirmed; Qt default backend retained ({} ms total)",
|
||||
start.elapsed().as_millis()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
fn outcome(code: i32, marker: bool, timeout: bool) -> (Output, bool) {
|
||||
(
|
||||
Output {
|
||||
status: std::process::ExitStatus::from_raw(code),
|
||||
stdout: if marker {
|
||||
format!("{PROOF}\n").into_bytes()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
stderr: Vec::new(),
|
||||
},
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_health_failure_and_retry_are_launch_local_and_finite() {
|
||||
let mut attempts = Vec::new();
|
||||
assert!(!decide(|soft| {
|
||||
attempts.push(soft);
|
||||
Ok(outcome(0, true, false))
|
||||
})
|
||||
.unwrap());
|
||||
assert_eq!(attempts, [false]);
|
||||
for (code, marker, timeout) in [(6, false, false), (0, false, false), (0, true, true)] {
|
||||
attempts.clear();
|
||||
assert!(decide(|soft| {
|
||||
attempts.push(soft);
|
||||
Ok(if soft {
|
||||
outcome(0, true, false)
|
||||
} else {
|
||||
outcome(code, marker, timeout)
|
||||
})
|
||||
})
|
||||
.unwrap());
|
||||
assert_eq!(attempts, [false, true]);
|
||||
}
|
||||
attempts.clear();
|
||||
assert!(decide(|soft| {
|
||||
attempts.push(soft);
|
||||
Ok(outcome(6, false, false))
|
||||
})
|
||||
.is_err());
|
||||
assert_eq!(attempts, [false, true]);
|
||||
assert!(!decide(|_| Ok(outcome(0, true, false))).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_policy_ignores_empty_and_whitespace_values() {
|
||||
for value in ["", " ", "\t"] {
|
||||
assert!(!external_backend(value, value, value));
|
||||
assert!(!external_backend(value, value, " Auto "));
|
||||
}
|
||||
for (qsg, quick, qbz) in [("opengl", "", ""), ("", " software ", ""), ("", "", "gpu")] {
|
||||
assert!(external_backend(qsg, quick, qbz));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_spawn_failure_is_diagnostic_without_a_second_child() {
|
||||
let mut attempts = 0;
|
||||
let result = decide(|_| {
|
||||
attempts += 1;
|
||||
Err("fixture spawn failure".into())
|
||||
});
|
||||
assert_eq!(attempts, 1);
|
||||
assert!(result.unwrap_err().contains("spawn failure"));
|
||||
let mut missing = command_for(std::path::Path::new("/no such qbz executable"), false);
|
||||
assert!(run_preflight_child(&mut missing, std::time::Duration::from_millis(80)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_paths_args_logs_and_watchdog_are_isolated() {
|
||||
use std::os::unix::fs::symlink;
|
||||
let dir = tempfile::Builder::new()
|
||||
.prefix("qbz preflight ")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let executable = dir.path().join("fixture with spaces");
|
||||
symlink("/bin/sh", &executable).unwrap();
|
||||
let mut command = command_for(&executable, false);
|
||||
assert_eq!(
|
||||
command.get_args().count(),
|
||||
0,
|
||||
"parent links never reach the graphics child"
|
||||
);
|
||||
command.args(["-c", "test -z \"$XDG_ACTIVATION_TOKEN\" && test -z \"$DESKTOP_STARTUP_ID\" || exit 9; head -c 131072 /dev/zero >&2; echo QBZ_AUTO_PREFLIGHT_OK"]);
|
||||
let result = run_preflight_child(&mut command, std::time::Duration::from_secs(2)).unwrap();
|
||||
assert!(proof(&result).is_ok());
|
||||
assert_eq!(result.0.stderr.len(), 65536);
|
||||
|
||||
// exec replaces the fixture shell: there is exactly one owned child,
|
||||
// just as the production command directly starts the QBZ executable.
|
||||
let pid_path = dir.path().join("pid");
|
||||
let mut command = command_for(&executable, false);
|
||||
command
|
||||
.env("FIXTURE_PID", &pid_path)
|
||||
.args(["-c", "echo $$ > \"$FIXTURE_PID\"; exec sleep 30"]);
|
||||
let start = std::time::Instant::now();
|
||||
let result =
|
||||
run_preflight_child(&mut command, std::time::Duration::from_millis(80)).unwrap();
|
||||
assert!(result.1);
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(1));
|
||||
let pid = std::fs::read_to_string(pid_path).unwrap();
|
||||
assert!(
|
||||
!std::path::Path::new(&format!("/proc/{}", pid.trim())).exists(),
|
||||
"timed-out child must be reaped"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -336,8 +336,14 @@ struct QtGpuJson {
|
||||
unsafe extern "C" {
|
||||
fn qbz_qt_vulkan_devices_json() -> *const std::ffi::c_char;
|
||||
fn qbz_qt_vulkan_preflight_window() -> i32;
|
||||
#[cfg(target_os = "linux")]
|
||||
fn qbz_qt_auto_preflight_window() -> i32;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "renderer_preflight_linux.rs"]
|
||||
pub mod auto_preflight;
|
||||
|
||||
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";
|
||||
@@ -700,26 +706,43 @@ fn approved_identity_from(output: &std::process::Output) -> Option<String> {
|
||||
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)
|
||||
let mut command = std::process::Command::new(executable);
|
||||
command
|
||||
.env(GPU_PREFLIGHT_CHILD_ENV, "1")
|
||||
.env(GPU_PREFLIGHT_IDENTITY_ENV, identity)
|
||||
.env(GPU_PREFLIGHT_NAME_ENV, name)
|
||||
.env(GPU_PREFLIGHT_NAME_ENV, name);
|
||||
run_preflight_child(&mut command, GPU_PREFLIGHT_PARENT_TIMEOUT)
|
||||
}
|
||||
|
||||
/// Drain into anonymous temporary files rather than undrained pipes: verbose
|
||||
/// driver diagnostics must not block the child before it can report success.
|
||||
/// No worker survives cancellation; kill + wait reaps the one child we own.
|
||||
fn run_preflight_child(
|
||||
command: &mut std::process::Command,
|
||||
budget: std::time::Duration,
|
||||
) -> Result<(std::process::Output, bool), String> {
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
let mut stdout = tempfile::tempfile().map_err(|e| format!("child stdout: {e}"))?;
|
||||
let mut stderr = tempfile::tempfile().map_err(|e| format!("child stderr: {e}"))?;
|
||||
let out = stdout.try_clone().map_err(|e| e.to_string())?;
|
||||
let err = stderr.try_clone().map_err(|e| e.to_string())?;
|
||||
let started = std::time::Instant::now();
|
||||
let mut child = command
|
||||
// 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())
|
||||
.stdout(out)
|
||||
.stderr(err)
|
||||
.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 => {
|
||||
Ok(None) if started.elapsed() < budget => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(25));
|
||||
}
|
||||
Ok(None) => {
|
||||
@@ -734,9 +757,24 @@ fn spawn_gpu_preflight(name: &str, identity: &str) -> Result<(std::process::Outp
|
||||
}
|
||||
}
|
||||
}
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
let status = child
|
||||
.wait()
|
||||
.map_err(|error| format!("cannot collect child result: {error}"))?;
|
||||
fn tail(file: &mut std::fs::File) -> Result<Vec<u8>, String> {
|
||||
let length = file.metadata().map_err(|e| e.to_string())?.len();
|
||||
file.seek(SeekFrom::Start(length.saturating_sub(65536)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut bytes = Vec::new();
|
||||
file.take(65536)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(bytes)
|
||||
}
|
||||
let output = std::process::Output {
|
||||
status,
|
||||
stdout: tail(&mut stdout)?,
|
||||
stderr: tail(&mut stderr)?,
|
||||
};
|
||||
Ok((output, timed_out))
|
||||
}
|
||||
|
||||
@@ -863,8 +901,9 @@ pub fn apply_gpu_preference() {
|
||||
// A GPU pick needs Vulkan (see below), so it cannot also honour an explicit
|
||||
// `software`/`opengl` choice — the more specific, explicitly-chosen
|
||||
// RENDERER wins and this says so rather than quietly overriding it.
|
||||
if let Some(forced) =
|
||||
std::env::var_os("QSG_RHI_BACKEND").or_else(|| std::env::var_os("QT_QUICK_BACKEND"))
|
||||
if let Some(forced) = ["QSG_RHI_BACKEND", "QT_QUICK_BACKEND"]
|
||||
.into_iter()
|
||||
.find(|name| nonempty_env(name))
|
||||
{
|
||||
log::info!(
|
||||
"[qbz-qt] gpu: the renderer row already forced {forced:?}; a GPU pick needs \
|
||||
|
||||
@@ -1167,6 +1167,12 @@ impl qbz_shell::QbzShell {
|
||||
"Selected GPU is unavailable in this display session — using Auto",
|
||||
));
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if crate::renderer_qt::auto_preflight::take_software_notice() {
|
||||
crate::toast_qt::warning(qbz_i18n::t(
|
||||
"Renderer switched automatically — the previous renderer failed to start",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// The renderer probe's one-shot report (see the declaration).
|
||||
|
||||
Reference in new issue
Block a user