1
//! The graphics-check dialog (`SysDialogType::GpuCheck`).
2
//!
3
//! For apps that NEED working video acceleration. It is a thin, honest face
4
//! on machinery that already exists: the shell's GL probe (`query_gpu_info`,
5
//! published through [`crate::appenv::gpu_status`]) says what the RENDERER
6
//! got, and the dll's provisioning check
7
//! (`VideoStartupCheck::run`/`remediate`, reached through
8
//! [`crate::appenv::gpu_provision_hooks`]) says whether hardware decode is
9
//! ready, whether the machine is SAFE TO REBOOT, and what a one-click repair
10
//! would run.
11
//!
12
//! Consent is the whole design: `run()` is inspection only and happens on a
13
//! worker thread when the dialog opens; `remediate()` — which elevates via
14
//! pkexec and can install drivers or repair an unbootable kernel — runs ONLY
15
//! after the user has read the exact command list and pressed the button.
16
//! When the remediation needs a reboot, or when the CURRENT boot path is
17
//! already unsafe, the dialog says so before anything is applied.
18

            
19
use alloc::string::{String, ToString};
20
use alloc::vec::Vec;
21

            
22
use azul_core::callbacks::{LayoutCallbackInfo, LayoutCallbackType, Update};
23
use azul_core::dom::Dom;
24
use azul_core::refany::RefAny;
25
use azul_core::task::ThreadId;
26
use azul_css::AzString;
27

            
28
use super::{cpu_dialog_window, style};
29
use crate::appenv::{GpuProvisionOutcome, GpuProvisionReport, GpuStatus};
30
use crate::callbacks::CallbackInfo;
31
use azul_core::task::ThreadReceiver;
32
use crate::thread::{
33
    Thread, ThreadCallbackType, ThreadReceiveMsg, ThreadSender, ThreadWriteBackMsg,
34
    WriteBackCallbackType,
35
};
36
use crate::widgets::button::{Button, ButtonOnClickCallbackType};
37
use crate::widgets::progressbar::ProgressBar;
38

            
39
/// Where the dialog is in the inspect → consent → apply story.
40
#[derive(Debug, Clone)]
41
pub enum GpuPhase {
42
    /// The readiness check is running on a worker thread.
43
    Checking,
44
    /// The check came back; waiting for the user.
45
    Report(GpuProvisionReport),
46
    /// No shell published provisioning hooks (a build without the video
47
    /// stack, or a headless run) — the GL section is still shown.
48
    Unavailable(String),
49
    /// The user consented; the remediation is running (pkexec prompt).
50
    /// Carries REAL progress: commands finished / total, and the command
51
    /// currently running.
52
    Applying {
53
        /// Commands finished so far.
54
        done: usize,
55
        /// Commands the plan set out to run.
56
        total: usize,
57
        /// The command running right now.
58
        step: String,
59
    },
60
    /// The remediation finished.
61
    Applied(GpuProvisionOutcome),
62
}
63

            
64
/// Shared dialog state (window ctx + worker writebacks).
65
#[derive(Debug, Clone)]
66
pub struct GpuDialogState {
67
    /// Current phase.
68
    pub phase: GpuPhase,
69
}
70

            
71
/// Opens the dialog and starts the (inspection-only) readiness check.
72
pub fn open(info: &mut CallbackInfo) {
73
    let state = RefAny::new(GpuDialogState {
74
        phase: GpuPhase::Checking,
75
    });
76
    info.add_thread(
77
        ThreadId::unique(),
78
        Thread::create(
79
            RefAny::new(CheckTask),
80
            state.clone(),
81
            check_worker as ThreadCallbackType,
82
        ),
83
    );
84
    info.create_window(cpu_dialog_window(
85
        "Graphics Check",
86
        (560.0, 560.0),
87
        dialog_layout as LayoutCallbackType,
88
        state,
89
    ));
90
}
91

            
92
struct CheckTask;
93
struct NewPhase(GpuPhase);
94

            
95
/// Background: the provisioning readiness check. INSPECTION ONLY — it
96
/// dlopen's codec libraries and reads kernel/driver state, changes nothing.
97
extern "C" fn check_worker(mut _init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
98
    let phase = match crate::appenv::gpu_provision_hooks() {
99
        Some(hooks) => GpuPhase::Report((hooks.check)()),
100
        None => GpuPhase::Unavailable(
101
            "This build has no driver-provisioning support, so only the \
102
             renderer's own report is available."
103
                .to_owned(),
104
        ),
105
    };
106
    let _ = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
107
        apply_phase as WriteBackCallbackType,
108
        RefAny::new(NewPhase(phase)),
109
    )));
110
}
111

            
112
/// Background: the CONSENTED remediation. Side-effecting — driver install
113
/// and/or kernel repair through pkexec.
114
extern "C" fn remediate_worker(mut _init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
115
    let phase = match crate::appenv::gpu_provision_hooks() {
116
        Some(hooks) => {
117
            // One writeback per command, so the bar moves for real. (The
118
            // main-thread drain reads the queue until it is EMPTY — see the
119
            // `every_writeback_a_worker_sends_reaches_the_main_thread` law;
120
            // before that fix everything after the first step was dropped.)
121
            let mut on_step = |done: usize, total: usize, step: &str| {
122
                let _ = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
123
                    apply_phase as WriteBackCallbackType,
124
                    RefAny::new(NewPhase(GpuPhase::Applying {
125
                        done,
126
                        total,
127
                        step: step.to_owned(),
128
                    })),
129
                )));
130
            };
131
            GpuPhase::Applied((hooks.remediate)(&mut on_step))
132
        }
133
        None => GpuPhase::Applied(GpuProvisionOutcome {
134
            ok: false,
135
            reboot_required: false,
136
            message: "provisioning hooks disappeared between check and apply".to_owned(),
137
        }),
138
    };
139
    let _ = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
140
        apply_phase as WriteBackCallbackType,
141
        RefAny::new(NewPhase(phase)),
142
    )));
143
}
144

            
145
/// Main thread: move the shared state to the worker's phase.
146
extern "C" fn apply_phase(mut state: RefAny, mut msg: RefAny, _info: CallbackInfo) -> Update {
147
    let Some(new_phase) = msg.downcast_ref::<NewPhase>().map(|p| p.0.clone()) else {
148
        return Update::DoNothing;
149
    };
150
    if let Some(mut s) = state.downcast_mut::<GpuDialogState>() {
151
        s.phase = new_phase;
152
    }
153
    Update::RefreshDomAllWindows
154
}
155

            
156
// --- buttons ---------------------------------------------------------------
157

            
158
/// The consent button. Everything before this point was inspection.
159
extern "C" fn on_repair_now(mut state: RefAny, mut info: CallbackInfo) -> Update {
160
    {
161
        let Some(mut s) = state.downcast_mut::<GpuDialogState>() else {
162
            return Update::DoNothing;
163
        };
164
        let GpuPhase::Report(report) = &s.phase else {
165
            return Update::DoNothing;
166
        };
167
        if !report.can_remediate {
168
            return Update::DoNothing;
169
        }
170
        s.phase = GpuPhase::Applying {
171
            done: 0,
172
            total: 0,
173
            step: String::new(),
174
        };
175
    }
176
    info.add_thread(
177
        ThreadId::unique(),
178
        Thread::create(
179
            RefAny::new(CheckTask),
180
            state.clone(),
181
            remediate_worker as ThreadCallbackType,
182
        ),
183
    );
184
    Update::RefreshDomAllWindows
185
}
186

            
187
extern "C" fn on_close(mut _state: RefAny, mut info: CallbackInfo) -> Update {
188
    info.close_window();
189
    Update::DoNothing
190
}
191

            
192
// --- layout ----------------------------------------------------------------
193

            
194
extern "C" fn dialog_layout(_data: RefAny, info: LayoutCallbackInfo) -> Dom {
195
    let Some(mut ctx) = info.get_ctx().into_option() else {
196
        return Dom::create_body();
197
    };
198
    let phase = match ctx.downcast_ref::<GpuDialogState>() {
199
        Some(s) => s.phase.clone(),
200
        None => return Dom::create_body(),
201
    };
202
    drop(ctx);
203
    let state = info.get_ctx().into_option().unwrap_or_else(|| RefAny::new(()));
204

            
205
    use azul_css::props::{
206
        basic::pixel::PixelValue,
207
        layout::{LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop},
208
    };
209
    let pad = style(vec![
210
        azul_css::props::property::CssProperty::padding_left(LayoutPaddingLeft {
211
            inner: PixelValue::px(16.0),
212
        }),
213
        azul_css::props::property::CssProperty::padding_right(LayoutPaddingRight {
214
            inner: PixelValue::px(16.0),
215
        }),
216
        azul_css::props::property::CssProperty::padding_top(LayoutPaddingTop {
217
            inner: PixelValue::px(16.0),
218
        }),
219
        azul_css::props::property::CssProperty::padding_bottom(LayoutPaddingBottom {
220
            inner: PixelValue::px(16.0),
221
        }),
222
    ]);
223

            
224
    let mut children: Vec<Dom> = vec![Dom::create_h2_with_text("Graphics check")];
225
    let mut buttons: Vec<(&str, ButtonOnClickCallbackType, RefAny)> = Vec::new();
226

            
227
    match &phase {
228
        GpuPhase::Checking => {
229
            children.push(Dom::create_p_with_text(
230
                "Checking the graphics drivers and the boot path...",
231
            ));
232
        }
233
        GpuPhase::Report(report) => {
234
            children.push(Dom::create_p_with_text(report.summary.as_str()));
235
            children.push(Dom::create_p_with_text(alloc::format!(
236
                "Hardware video decode: {}",
237
                yes_no(report.hw_decode_ready)
238
            )));
239
            children.push(Dom::create_p_with_text(alloc::format!(
240
                "Safe to reboot: {}",
241
                yes_no(report.boot_safe)
242
            )));
243
            if !report.boot_safe {
244
                children.push(Dom::create_p_with_text(
245
                    "WARNING: as things stand, the next reboot may not reach a \
246
                     usable desktop. Apply the repair below BEFORE rebooting.",
247
                ));
248
            }
249
            children.extend(detail_lines(&report.detail));
250
            if report.can_remediate {
251
                children.push(Dom::create_p_with_text(
252
                    "The repair runs the commands listed above and will ask for \
253
                     your password.",
254
                ));
255
                if report.needs_reboot {
256
                    children.push(Dom::create_p_with_text(
257
                        "It takes effect after a restart - nothing reboots on its own.",
258
                    ));
259
                }
260
                buttons.push((
261
                    "Repair now",
262
                    on_repair_now as ButtonOnClickCallbackType,
263
                    state.clone(),
264
                ));
265
            }
266
        }
267
        GpuPhase::Unavailable(reason) => {
268
            children.push(Dom::create_p_with_text(reason.as_str()));
269
        }
270
        GpuPhase::Applying { done, total, step } => {
271
            children.push(Dom::create_p_with_text(
272
                "Applying... you may be asked for your password. Do not close \
273
                 this window.",
274
            ));
275
            // A real fraction or nothing: an indeterminate bar drawn as if it
276
            // measured something is worse than no bar. `total == 0` is the
277
            // window between consent and the first command report.
278
            if *total > 0 {
279
                #[allow(clippy::cast_precision_loss)]
280
                let percent = (*done as f32 / *total as f32) * 100.0;
281
                children.push(ProgressBar::create(percent).dom());
282
                children.push(Dom::create_p_with_text(alloc::format!(
283
                    "Step {} of {}: {}",
284
                    done + 1,
285
                    total,
286
                    step
287
                )));
288
            } else {
289
                children.push(Dom::create_p_with_text("Starting..."));
290
            }
291
        }
292
        GpuPhase::Applied(outcome) => {
293
            children.push(Dom::create_p_with_text(if outcome.ok {
294
                "The repair finished."
295
            } else {
296
                "The repair did not finish."
297
            }));
298
            children.push(Dom::create_p_with_text(outcome.message.as_str()));
299
            if outcome.reboot_required {
300
                children.push(Dom::create_p_with_text(
301
                    "Restart the machine to finish - the change is staged, not live.",
302
                ));
303
            }
304
        }
305
    }
306

            
307
    children.extend(gl_section(crate::appenv::gpu_status().as_ref()));
308
    buttons.push(("Close", on_close as ButtonOnClickCallbackType, state));
309
    children.push(button_row(buttons));
310

            
311
    Dom::create_body()
312
        .with_css_props(pad)
313
        .with_children(children.into())
314
}
315

            
316
/// What the RENDERER actually got, as opposed to what the machine could do.
317
/// Always shown: it is the answer to "why is this app slow/soft-rendered".
318
fn gl_section(status: Option<&GpuStatus>) -> Vec<Dom> {
319
    let mut out = vec![Dom::create_h2_with_text("This window's renderer")];
320
    match status {
321
        Some(s) if s.ok => {
322
            out.push(Dom::create_p_with_text(alloc::format!(
323
                "GPU rendering, on {} ({})",
324
                s.renderer, s.vendor
325
            )));
326
            out.push(Dom::create_p_with_text(alloc::format!(
327
                "OpenGL {} - GLSL {}",
328
                s.version, s.glsl_version
329
            )));
330
        }
331
        Some(s) => {
332
            out.push(Dom::create_p_with_text(
333
                "CPU rendering - the GPU path was rejected.",
334
            ));
335
            if !s.renderer.is_empty() {
336
                out.push(Dom::create_p_with_text(alloc::format!(
337
                    "Detected: {} ({}) - OpenGL {}",
338
                    s.renderer, s.vendor, s.version
339
                )));
340
            }
341
            out.push(Dom::create_p_with_text(alloc::format!("Why: {}", s.verdict)));
342
        }
343
        None => out.push(Dom::create_p_with_text(
344
            "CPU rendering - no GPU probe ran in this session.",
345
        )),
346
    }
347
    out
348
}
349

            
350
/// The provisioning report's multi-line detail, one paragraph per line, so
351
/// the exact command list the user is consenting to stays readable.
352
fn detail_lines(detail: &str) -> Vec<Dom> {
353
    detail
354
        .lines()
355
        .filter(|l| !l.trim().is_empty())
356
        .map(Dom::create_p_with_text)
357
        .collect()
358
}
359

            
360
const fn yes_no(v: bool) -> &'static str {
361
    if v {
362
        "yes"
363
    } else {
364
        "no"
365
    }
366
}
367

            
368
/// Buttons on one line (same shape as the other dialogs' rows).
369
fn button_row(buttons: Vec<(&str, ButtonOnClickCallbackType, RefAny)>) -> Dom {
370
    let children: Vec<Dom> = buttons
371
        .into_iter()
372
        .map(|(label, cb, state)| {
373
            Button::create(AzString::from(label))
374
                .with_on_click(state, cb)
375
                .dom()
376
        })
377
        .collect();
378
    Dom::create_div().with_children(children.into())
379
}