Issue #136: app.gpu {wait:false} still says why the GPU is off; Settings shows "GPU: starting…" while the device is created

Review follow-up: with GPU rendering off (preference, environment, earlier failure) the
no-wait reply now reports available=false and the reason at once, without creating the
device. The Performance tab no longer calls gpu::available() while the device is still
being created, which could block the UI thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
OneSlips
2026-10-25 05:15:13 +10:00
co-authored by Claude Opus 5.5
parent 6376b1cebf
commit 32e55afee8
3 changed files with 24 additions and 11 deletions
+13 -6
View File
@@ -404,7 +404,12 @@ fn snapshot_defaults_to_cpu_without_gpu_environment_overrides() {
let script = tmp("default-cpu.jsonl");
std::fs::write(
&script,
format!("{}\n{}\n", json!({"method": "engine.execute", "params": {"command": "app.gpu"}}), json!({"method": "ui.screenshot"})),
format!(
"{}\n{}\n{}\n",
json!({"method": "engine.execute", "params": {"command": "app.gpu", "params": {"wait": false}}}),
json!({"method": "engine.execute", "params": {"command": "app.gpu"}}),
json!({"method": "ui.screenshot"})
),
)
.unwrap();
let image = tmp("default-cpu.png");
@@ -418,11 +423,13 @@ fn snapshot_defaults_to_cpu_without_gpu_environment_overrides() {
assert!(output.status.success(), "status {}: {}", output.status, String::from_utf8_lossy(&output.stderr));
let replies: Vec<Value> = String::from_utf8_lossy(&output.stdout).lines().map(|line| serde_json::from_str(line).unwrap()).collect();
assert!(replies.iter().all(|reply| reply["ok"] == true), "{replies:?}");
let gpu = &replies[0]["result"];
assert_eq!(gpu["enabled"], false, "{gpu}");
assert_eq!(gpu["available"], false, "{gpu}");
assert_eq!(gpu["adapter"], Value::Null, "no device is created: {gpu}");
assert!(gpu["reason"].as_str().is_some_and(|reason| reason.contains("preference")), "{gpu}");
// `wait: false` (what the UI sends) still says why the GPU is off: no device is being created
for gpu in [&replies[0]["result"], &replies[1]["result"]] {
assert_eq!(gpu["enabled"], false, "{gpu}");
assert_eq!(gpu["available"], false, "{gpu}");
assert_eq!(gpu["adapter"], Value::Null, "no device is created: {gpu}");
assert!(gpu["reason"].as_str().is_some_and(|reason| reason.contains("preference")), "{gpu}");
}
let decoded = lightcraft_codecs::decode(&std::fs::read(image).unwrap(), Default::default()).unwrap();
assert_eq!((decoded.width, decoded.height), (480, 320));
}
+7 -4
View File
@@ -226,18 +226,21 @@ pub fn specs() -> Vec<CommandSpec> {
"versions": ph.versions.iter().map(|v| json!({"name": v.name, "created": v.created})).collect::<Vec<_>>(),
}))
}),
cmd!(query "app.gpu", "GPU Rendering", [], None, "{enabled?: bool, wait?: bool} — allow/forbid GPU rendering (CPU fallback; LIGHTCRAFT_GPU=0 forbids it for the process); returns {enabled, available, adapter, reason (why the GPU is off), lastFallback (latest render redone on the CPU, and why)}. wait: false never waits for a device still being created (available, adapter and reason are then null)", always, |_, p| {
cmd!(query "app.gpu", "GPU Rendering", [], None, "{enabled?: bool, wait?: bool} — allow/forbid GPU rendering (CPU fallback; LIGHTCRAFT_GPU=0 forbids it for the process); returns {enabled, available, adapter, reason (why the GPU is off), lastFallback (latest render redone on the CPU, and why)}. wait: false never waits for a device still being created (available, adapter and reason are then null; with GPU rendering off they say so at once)", always, |_, p| {
if let Some(on) = p.get("enabled").and_then(Value::as_bool) {
lightcraft_gpu::set_enabled(on);
}
// the UI applies the setting on its first frame: it must not wait seconds for the
// device there (issue #136; `warm_up` creates it off the UI thread)
if p.get("wait").and_then(Value::as_bool) == Some(false) && !lightcraft_gpu::ready() {
// off (preference, environment): known at once, and asking the adapter would
// create the device; on: still being created, so nothing is known yet
let on = lightcraft_gpu::enabled();
return Ok(json!({
"enabled": lightcraft_gpu::enabled(),
"available": null,
"enabled": on,
"available": if on { Value::Null } else { Value::Bool(false) },
"adapter": null,
"reason": null,
"reason": if on { None } else { lightcraft_gpu::unavailable_reason() },
"lastFallback": lightcraft_gpu::last_fallback(),
}));
}
+4 -1
View File
@@ -417,7 +417,10 @@ fn performance_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
use lightcraft_engine::gpu;
heading(ui, t, crate::i18n::tr("Rendering"));
check(ui, "settings.gpu", &mut app.ui.settings.gpu, "Use the GPU for rendering");
let status = if !gpu::available() {
// `available` would wait for a device still being created (issue #136)
let status = if gpu::enabled() && !gpu::ready() {
"GPU: starting…".to_string()
} else if !gpu::available() {
match gpu::unavailable_reason() {
Some(why) => crate::i18n::tr_format!("Rendering on the CPU: {why}", why = why),
None => "No usable GPU found: rendering on the CPU".to_string(),