1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
//! `dispatch_redraw`: the re-entrancy-guarded redraw entry that drives the
//! device-lost recovery state machine before delegating to `run_redraw`.
//!
//! Holds the adapter-LUID probe and the `RecoveryState` match arms; recovery
//! retry mechanics live in `recovery.rs`, the actual pipeline in `redraw.rs`.
use std::time::{Duration, Instant};
use slate_platform::{Window, WindowId};
use super::super::guards::reset_borrow_order;
use super::super::state::AppState;
use super::super::types::{
ADAPTER_PROBE_MIN_INTERVAL_MS, AppSignal, DeviceLossReason, RECOVERY_COOLDOWN_MS,
RECOVERY_FLAP_GUARD_SECS, RecoveryState,
};
impl AppState {
/// Full redraw dispatch with device-lost recovery wrapper + re-entrancy guard.
///
/// Returns `AppSignal::RequestQuit` if recovery exceeds `RECOVERY_MAX_ATTEMPTS`.
/// Returns `AppSignal::None` if the window is unknown (race with destroy).
pub fn dispatch_redraw(&self, window_id: WindowId) -> AppSignal {
// Snapshot fields needed for early logging before the per-window borrow.
let (pre_rendering, pre_device_lost) = {
let guard = self.windows.borrow();
match guard.get(&window_id) {
Some(win) => {
let r = win.rendering.get();
let dl = win
.renderer
.borrow()
.as_ref()
.map(|r| r.is_device_lost())
.unwrap_or(false);
(r, dl)
}
None => return AppSignal::None,
}
};
log::trace!(
target: "slate::device_lost",
"dispatch_redraw entry window={:?}: rendering={pre_rendering} device_lost={pre_device_lost}",
window_id
);
// Re-entrancy guard — if a redraw is already in flight for this window, skip.
{
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
if win.rendering.get() {
return AppSignal::None;
}
win.rendering.set(true);
} else {
return AppSignal::None;
}
}
// RAII guard clears rendering flag on scope exit (panic-safe).
// We hold the flag on the WindowState Cell directly.
struct RenderingCellGuard<'a>(&'a std::cell::Cell<bool>);
impl Drop for RenderingCellGuard<'_> {
fn drop(&mut self) {
self.0.set(false);
}
}
let _rendering_guard = {
let guard = self.windows.borrow();
guard.get(&window_id).map(|win| {
// SAFETY: The Cell reference lifetime is bounded by AppState
// lifetime, which outlives this scope. We need a raw-pointer
// bridge because the borrow guard would alias. Use the guard
// pattern via a raw pointer converted back to a reference with
// the AppState lifetime.
//
// Actually: we set the flag above, and we clear it below after
// all dispatch is done. The guard approach needs the Cell to
// stay alive. Since AppState is Rc'd and lives for the entire
// scope, this is safe. We approximate with a simple cleanup at
// the end of the function instead.
let _ = win; // acknowledged
})
};
reset_borrow_order();
// Skip if not initialized.
{
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
if win.renderer.borrow().is_none() {
win.rendering.set(false);
return AppSignal::None;
}
} else {
return AppSignal::None;
}
}
// Adapter-LUID probe: detect cross-monitor drag onto a different
// physical adapter and mark device lost so the recovery state machine
// re-picks an adapter matching the window's current monitor.
//
// Gated on NotLost + !skip_draws — during active recovery the old LUID
// would re-trigger on every retry step and could trip the flap guard.
// Throttled to ADAPTER_PROBE_MIN_INTERVAL_MS to absorb burst during drag.
// No-op on non-Windows (current_monitor_luid returns None).
{
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
let healthy = matches!(*win.recovery_state.borrow(), RecoveryState::NotLost)
&& !win.skip_draws.get();
let now = Instant::now();
let recently_probed = win
.last_adapter_check_at
.get()
.map(|t| {
now.duration_since(t) < Duration::from_millis(ADAPTER_PROBE_MIN_INTERVAL_MS)
})
.unwrap_or(false);
if healthy && !recently_probed {
win.last_adapter_check_at.set(Some(now));
let window_luid = win.window.current_monitor_luid();
let adapter_luid = win
.renderer
.borrow()
.as_ref()
.and_then(|r| r.current_adapter_luid());
if let (Some(w), Some(a)) = (window_luid, adapter_luid)
&& w != a
{
log::info!(
target: "slate::device_lost",
"adapter LUID mismatch window={:?}: window={:#018x} renderer={:#018x} — marking device-lost",
window_id, w, a
);
if let Some(r) = win.renderer.borrow().as_ref() {
r.mark_device_potentially_lost();
}
}
}
}
}
// Check device_lost and drive the state machine.
let device_lost = {
let guard = self.windows.borrow();
guard
.get(&window_id)
.and_then(|win| win.renderer.borrow().as_ref().map(|r| r.is_device_lost()))
.unwrap_or(false)
};
let state_snapshot = {
let guard = self.windows.borrow();
guard
.get(&window_id)
.map(|win| win.recovery_state.borrow().clone())
};
let Some(state_snapshot) = state_snapshot else {
// Window disappeared.
return AppSignal::None;
};
log::trace!(
target: "slate::device_lost",
"dispatch_redraw match window={:?}: state={:?} device_lost={device_lost}",
window_id, &state_snapshot
);
let result = match state_snapshot {
RecoveryState::NotLost if device_lost => {
let reason = self.classify_loss_reason(window_id);
let now = Instant::now();
let in_size_move = {
let guard = self.windows.borrow();
guard
.get(&window_id)
.map(|w| w.window.in_size_move())
.unwrap_or(false)
};
if in_size_move {
log::info!(target: "slate::device_lost",
"device-lost during modal size/move — deferring (reason={:?})", reason);
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
*win.recovery_state.borrow_mut() = RecoveryState::DeferredUntilStable {
detected_at: now,
reason,
};
}
let guard2 = self.windows.borrow();
if let Some(win) = guard2.get(&window_id) {
win.rendering.set(false);
}
return AppSignal::None;
}
// Reason-aware flap guard: only WgpuCallback losses count.
if reason == DeviceLossReason::WgpuCallback {
let prev = {
let guard = self.windows.borrow();
guard
.get(&window_id)
.and_then(|w| w.last_wgpu_callback_loss_at.get())
};
if let Some(prev) = prev {
let elapsed = now.duration_since(prev);
if elapsed <= Duration::from_secs(RECOVERY_FLAP_GUARD_SECS) {
log::error!(target: "slate::device_lost",
"device-lost re-fired {}ms after prior WgpuCallback (guard={}s) — giving up",
elapsed.as_millis(), RECOVERY_FLAP_GUARD_SECS);
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
*win.recovery_state.borrow_mut() = RecoveryState::GiveUp { reason };
win.last_wgpu_callback_loss_at.set(Some(now));
}
let guard2 = self.windows.borrow();
if let Some(win) = guard2.get(&window_id) {
win.rendering.set(false);
}
return AppSignal::RequestQuit;
}
}
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
win.last_wgpu_callback_loss_at.set(Some(now));
}
}
log::info!(target: "slate::device_lost",
"device loss detected (reason={:?}), entering cooldown", reason);
{
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
*win.recovery_state.borrow_mut() = RecoveryState::DetectedLost {
detected_at: now,
reason,
};
win.window.request_redraw();
}
}
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
win.rendering.set(false);
}
return AppSignal::None;
}
RecoveryState::DetectedLost {
detected_at,
reason,
} => {
let reason = self.maybe_upgrade_reason(window_id, reason);
{
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
*win.recovery_state.borrow_mut() = RecoveryState::CooldownGate {
since: detected_at,
reason,
};
win.window.request_redraw();
}
}
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
win.rendering.set(false);
}
return AppSignal::None;
}
RecoveryState::CooldownGate { since, reason } => {
let reason = self.maybe_upgrade_reason(window_id, reason);
if since.elapsed() < Duration::from_millis(RECOVERY_COOLDOWN_MS) {
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
*win.recovery_state.borrow_mut() =
RecoveryState::CooldownGate { since, reason };
win.window.request_redraw();
}
let guard2 = self.windows.borrow();
if let Some(win) = guard2.get(&window_id) {
win.rendering.set(false);
}
return AppSignal::None;
}
log::info!(target: "slate::device_lost",
"cooldown elapsed, starting retry (reason={:?})", reason);
{
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
*win.recovery_state.borrow_mut() = RecoveryState::Retrying {
attempt: 0,
last_attempt_at: Instant::now(),
reason,
};
}
}
let sig = self.execute_recovery_step(window_id);
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
win.rendering.set(false);
}
return sig;
}
RecoveryState::Retrying { reason, .. } => {
let _ = self.maybe_upgrade_reason(window_id, reason);
let sig = self.execute_recovery_step(window_id);
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
win.rendering.set(false);
}
return sig;
}
RecoveryState::DeferredUntilStable { reason, .. } => {
let _ = self.maybe_upgrade_reason(window_id, reason);
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
win.rendering.set(false);
}
return AppSignal::None;
}
RecoveryState::Recovered { .. } => {
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
*win.recovery_state.borrow_mut() = RecoveryState::NotLost;
}
// Fall through to normal redraw.
AppSignal::None
}
RecoveryState::GiveUp { .. } => {
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
win.rendering.set(false);
}
return AppSignal::RequestQuit;
}
RecoveryState::NotLost => {
// Fall through to normal redraw.
AppSignal::None
}
};
let _ = result; // All fall-through arms return None; run the actual redraw.
self.run_redraw(window_id);
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window_id) {
win.rendering.set(false);
}
AppSignal::None
}
}