Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/output.rs (84.5K)
1 // SPDX-FileCopyrightText: © 2020 The River Developers
2 // SPDX-License-Identifier: GPL-3.0-only
3
4 use crate::ffi;
5 use crate::server::{Server, WlListener, WlList, wl_signal_add, wl_listener_remove, wl_list_insert, wl_list_remove};
6 use crate::layer_shell::LayerShellOutput;
7 use crate::lock_manager::LockState;
8 use crate::util;
9
10 #[derive(Debug, Clone, Copy, PartialEq)]
11 pub enum OutputStateValue {
12 Enabled,
13 DisabledSoft,
14 DisabledHard,
15 Destroying,
16 }
17
18 #[derive(Debug, Clone, Copy)]
19 pub enum OutputMode {
20 Standard(*mut ffi::wlr_output_mode),
21 Custom {
22 width: i32,
23 height: i32,
24 refresh: i32,
25 },
26 None,
27 }
28
29 #[derive(Debug, Clone, Copy)]
30 pub struct OutputState {
31 pub state: OutputStateValue,
32 pub x: i32,
33 pub y: i32,
34 pub mode: OutputMode,
35 pub scale: f32,
36 pub transform: ffi::wl_output_transform,
37 pub adaptive_sync: bool,
38 pub auto_layout: bool,
39 }
40
41 impl OutputState {
42 pub fn mode_none(&self) -> bool {
43 matches!(self.mode, OutputMode::None)
44 }
45
46 pub unsafe fn from_head_state(state: *const ffi::wlr_output_head_v1_state) -> Self {
47 assert!((*state).enabled);
48 let mode = if !(*state).mode.is_null() {
49 OutputMode::Standard((*state).mode)
50 } else {
51 OutputMode::Custom {
52 width: (*state).custom_mode.width,
53 height: (*state).custom_mode.height,
54 refresh: (*state).custom_mode.refresh,
55 }
56 };
57
58 // Round to nearest 1/120 to ensure the scale is exactly represented
59 // in the fractional-scale-v1 protocol.
60 let scale = ((*state).scale * 120.0).round() / 120.0;
61
62 Self {
63 state: OutputStateValue::Enabled,
64 mode,
65 x: (*state).x,
66 y: (*state).y,
67 scale,
68 transform: (*state).transform,
69 adaptive_sync: (*state).adaptive_sync_enabled,
70 auto_layout: false,
71 }
72 }
73
74 pub unsafe fn dimensions(&self) -> (i32, i32) {
75 let (mut w, mut h) = match self.mode {
76 OutputMode::Standard(mode) => ((*mode).width, (*mode).height),
77 OutputMode::Custom { width, height, .. } => (width, height),
78 OutputMode::None => (0, 0),
79 };
80 if (self.transform as u32) % 2 != 0 {
81 std::mem::swap(&mut w, &mut h);
82 }
83 (
84 ((w as f32) / self.scale) as i32,
85 ((h as f32) / self.scale) as i32,
86 )
87 }
88
89 pub unsafe fn box_layout(&self) -> ffi::wlr_box {
90 let (w, h) = self.dimensions();
91 ffi::wlr_box {
92 x: self.x,
93 y: self.y,
94 width: w,
95 height: h,
96 }
97 }
98
99 pub unsafe fn apply_no_modeset(&self, wlr_state: *mut ffi::wlr_output_state) {
100 ffi::wlr_output_state_set_scale(wlr_state, self.scale);
101 ffi::wlr_output_state_set_transform(wlr_state, self.transform);
102 }
103
104 pub unsafe fn apply_modeset(&self, wlr_state: *mut ffi::wlr_output_state) {
105 let enabled = self.state == OutputStateValue::Enabled;
106 ffi::wlr_output_state_set_enabled(wlr_state, enabled);
107 if !enabled {
108 return;
109 }
110 self.apply_no_modeset(wlr_state);
111 match self.mode {
112 OutputMode::Standard(mode) => {
113 ffi::wlr_output_state_set_mode(wlr_state, mode);
114 }
115 OutputMode::Custom { width, height, refresh } => {
116 ffi::wlr_output_state_set_custom_mode(wlr_state, width, height, refresh);
117 }
118 OutputMode::None => {}
119 }
120 ffi::wlr_output_state_set_adaptive_sync_enabled(wlr_state, self.adaptive_sync);
121 }
122 }
123
124 #[derive(Debug, Clone, Copy, PartialEq)]
125 pub enum LockRenderState {
126 PendingUnlock,
127 Unlocked,
128 PendingBlank,
129 Blanked,
130 PendingLockSurface,
131 LockSurface,
132 }
133
134 #[derive(Debug, Clone, Copy)]
135 pub struct RenderingState {
136 pub tearing: bool,
137 }
138
139 /// One cached window-content reading — see `Output::status_win_samples`.
140 pub struct StatusWinSample {
141 /// The occluding window's slotmap index.
142 pub win: u32,
143 /// The region sampled, in layout px. Part of the key: a segment that
144 /// moves, or a window that slides, is looking at different pixels.
145 pub region: crate::backdrop::Rect,
146 /// When the readback actually ran.
147 pub at: std::time::Instant,
148 /// Commit sequences of the window's surfaces, summed. A window that has
149 /// not committed cannot have changed what it is showing, so this is what
150 /// keeps a still terminal from being re-read four times a second forever.
151 pub seq: u32,
152 /// None when the content could not be read at all.
153 pub sample: Option<crate::backdrop::BackdropSample>,
154 }
155
156 pub struct Output {
157 pub server: *mut Server,
158 pub wlr_output: *mut ffi::wlr_output,
159 pub scene_output: *mut ffi::wlr_scene_output,
160 pub background_rect: *mut ffi::wlr_scene_rect,
161 pub grid_tree: *mut ffi::wlr_scene_tree,
162 /// The grid's backdrop (gap colour, or the Solid spec's colour), kept in
163 /// its own tree under `scene.layers.background_clients` so a client
164 /// background surface paints over it while the cells in `grid_tree` stay
165 /// on top. Positioned in lockstep with `grid_tree`.
166 pub grid_backdrop_tree: *mut ffi::wlr_scene_tree,
167 pub grid_backdrop_rect: *mut ffi::wlr_scene_rect,
168 pub adjust_tree: *mut ffi::wlr_scene_tree,
169 pub adjust_rects: Vec<*mut ffi::wlr_scene_rect>,
170 pub last_adjust_mode: bool,
171 pub object: *mut ffi::wl_resource, // zcce_output_v1 resource
172 pub layer_shell: LayerShellOutput,
173 pub lock_render_state: LockRenderState,
174 pub link: ffi::wl_list,
175 pub link_sent: ffi::wl_list,
176 pub scheduled: OutputState,
177 pub sent: OutputState,
178 pub current: OutputState,
179 pub sent_wl_output: bool,
180 pub rendering_requested: RenderingState,
181 pub rendering_current: RenderingState,
182
183 // Cached grid parameters to avoid redrawing when unchanged
184 /// Camera state at this output's last rendered frame; any difference
185 /// forces a full-output repaint (see the frame chokepoint).
186 pub last_rendered_pan_x: f64,
187 pub last_rendered_pan_y: f64,
188 pub last_rendered_zoom: f64,
189 /// Presentation clock, from the `present` event: when the last frame
190 /// turned into light (CLOCK_MONOTONIC ns, 0 = never), its vblank
191 /// sequence number (0 = the backend has none), and the refresh period.
192 /// `predicted_present_ns` derives the camera's frame clock from these.
193 pub present_when_ns: u64,
194 pub present_seq: u32,
195 pub present_refresh_ns: u64,
196 /// Running count of vblanks skipped between consecutive presents —
197 /// the dropped-frame counter `CCE_FRAME_DEBUG` reports.
198 pub present_dropped: u64,
199 /// Phase of the frame clock's vblank grid (ns within a refresh period),
200 /// locked to the presentation times; `u64::MAX` until the first frame.
201 pub frame_phase_ns: u64,
202 /// What the status-backdrop measurement last ran against: the window
203 /// manager's layout epoch, the camera, and when. It re-runs only when one
204 /// of those moved or `BACKDROP_REFRESH` has passed (for window content
205 /// under a segment), not on every vblank.
206 pub backdrop_epoch: u64,
207 pub backdrop_cam: (f64, f64, f64),
208 pub backdrop_measured_at: Option<std::time::Instant>,
209 /// A tearing page-flip test failed for the current tearing episode; do
210 /// not repeat the atomic TEST_ONLY commit every frame. Cleared when the
211 /// fullscreen client's tearing request goes away.
212 pub tearing_test_failed: bool,
213 /// Darkened by the idle timeout (`IdleManager::set_displays`), so the
214 /// next activity wakes this one and leaves a client-darkened output alone.
215 pub idle_off: bool,
216 pub last_grid_viewport_w: i32,
217 pub last_grid_viewport_h: i32,
218 pub last_grid_zoom: f64,
219 /// The spec the pool was last drawn from; a change (or viewport/zoom
220 /// change) forces a redraw. Pan alone never redraws — it only moves the
221 /// grid tree.
222 pub last_grid_spec: Option<crate::policy::api::BackgroundSpec>,
223 pub grid_rect_pool: Vec<*mut ffi::wlr_scene_rect>,
224 /// Lit-chamfer rims over the grid cells — the same scenefx bevel node
225 /// the windows use, so the grid lines read as raised rails descending
226 /// into each cell through a shaded fillet that wraps the corner arcs.
227 /// Pooled like `grid_rect_pool`, but in their own subtree kept above
228 /// the rects: pool reuse must never stack a rim beneath a
229 /// later-created cell rect.
230 pub grid_bevel_pool: Vec<*mut ffi::wlr_scene_bevel>,
231 pub grid_bevel_tree: *mut ffi::wlr_scene_tree,
232 /// The cell labels' own subtree inside `grid_tree`, raised above the
233 /// cell rects and bevels on every draw: label nodes used to be direct
234 /// children of the grid tree, so any cell rect the pool created after
235 /// them stacked on top and hid them.
236 pub cell_label_tree: *mut ffi::wlr_scene_tree,
237 /// Bevel params the rims were last drawn with (enabled, thickness,
238 /// light x/y/intensity, shade, shoulder as bits) — the spec alone does
239 /// not cover them, and a live config reload must redraw the rims too.
240 pub last_grid_bevel: Option<[u32; 7]>,
241 pub grid_force_redraw_frames: u8,
242 /// Scene nodes for the per-square chess-style labels (overview only), and
243 /// the rasterized glyph buffers behind them. Pooled exactly like
244 /// `grid_rect_pool`: reused across frames, disabled past the live count.
245 /// Each entry remembers the buffer it currently shows, because
246 /// `wlr_scene_buffer_set_buffer` damages the node unconditionally — even
247 /// when handed the buffer already on it — and these are re-walked every
248 /// frame while the overview camera moves.
249 pub cell_label_pool: Vec<(*mut ffi::wlr_scene_buffer, *mut ffi::wlr_buffer)>,
250 pub cell_labels: crate::text::LabelCache,
251 /// Label point size actually in use, so a zoom change can re-rasterize.
252 pub last_label_px: u32,
253 /// Throttle+cache for the window-content half of the backdrop measurement.
254 /// Unlike the grid half, which is arithmetic, this one costs a texture
255 /// readback and a GPU sync, so it is re-taken at most every
256 /// `WIN_SAMPLE_MS` per segment AND only when the window has actually
257 /// committed something since. A window whose content changes faster than
258 /// that is not something the text contrast should be chasing anyway.
259 pub status_win_samples: Vec<StatusWinSample>,
260
261 pub destroy: ffi::wl_listener,
262 pub request_state: ffi::wl_listener,
263 pub frame: ffi::wl_listener,
264 pub present: ffi::wl_listener,
265 }
266
267 unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
268 let output = ffi::wl_resource_get_user_data(resource) as *mut Output;
269 if !output.is_null() {
270 if (*output).object != resource {
271 return;
272 }
273 (*output).object = std::ptr::null_mut();
274 (*output).sent_wl_output = false;
275 }
276 }
277
278 unsafe extern "C" fn output_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
279 ffi::wl_resource_destroy(resource);
280 }
281
282 unsafe extern "C" fn output_set_presentation_mode(
283 _client: *mut ffi::wl_client,
284 resource: *mut ffi::wl_resource,
285 mode: u32,
286 ) {
287 let output = ffi::wl_resource_get_user_data(resource) as *mut Output;
288 if output.is_null() {
289 return;
290 }
291 if !(*(*output).server).wm.ensure_rendering() {
292 return;
293 }
294 match mode {
295 ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC => {
296 (*output).rendering_requested.tearing = false;
297 }
298 ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_ASYNC => {
299 (*output).rendering_requested.tearing = true;
300 }
301 _ => {
302 ffi::wl_resource_post_error(
303 resource,
304 ffi::zcce_output_v1_error_ZCCE_OUTPUT_V1_ERROR_INVALID_PRESENTATION_MODE,
305 b"invalid presentation mode enum value\0".as_ptr() as *const _,
306 );
307 }
308 }
309 }
310
311 static OUTPUT_INTERFACE: ffi::zcce_output_v1_interface = ffi::zcce_output_v1_interface {
312 destroy: Some(output_destroy),
313 set_presentation_mode: Some(output_set_presentation_mode),
314 };
315
316 static INERT_OUTPUT_INTERFACE: ffi::zcce_output_v1_interface = ffi::zcce_output_v1_interface {
317 destroy: Some(output_destroy),
318 set_presentation_mode: None,
319 };
320
321 impl Output {
322 pub unsafe fn make_inert(&mut self) {
323 if !self.object.is_null() {
324 ffi::wl_resource_post_event(self.object, 0); // zcce_output.removed
325 ffi::wl_resource_set_implementation(
326 self.object,
327 &INERT_OUTPUT_INTERFACE as *const _ as *const _,
328 std::ptr::null_mut(),
329 None,
330 );
331 self.layer_shell.make_inert();
332 self.object = std::ptr::null_mut();
333 self.sent_wl_output = false;
334 self.grid_rect_pool.clear();
335 self.grid_bevel_pool.clear();
336 self.grid_bevel_tree = std::ptr::null_mut();
337 self.cell_label_pool.clear();
338 self.cell_label_tree = std::ptr::null_mut();
339 }
340 }
341
342 pub unsafe fn manage_start(&mut self) {
343 match self.scheduled.state {
344 OutputStateValue::Enabled | OutputStateValue::DisabledSoft => {
345 assert!(!self.scheduled.mode_none());
346 let wlr_output = self.wlr_output;
347
348 let self_ptr = self as *mut Output;
349 let layer_shell_ptr = &mut self.layer_shell as *mut LayerShellOutput;
350 (*layer_shell_ptr).manage_start(self_ptr);
351
352 let wm_v1 = (*self.server).wm.object;
353 if !wm_v1.is_null() {
354 let new = self.object.is_null();
355 let output_v1 = if new {
356 let client = ffi::wl_resource_get_client(wm_v1);
357 let res = ffi::wl_resource_create(
358 client,
359 &ffi::zcce_output_v1_interface,
360 ffi::wl_resource_get_version(wm_v1),
361 0,
362 );
363 if res.is_null() {
364 log::error!("out of memory");
365 return;
366 }
367 self.object = res;
368 ffi::wl_resource_set_implementation(
369 res,
370 &OUTPUT_INTERFACE as *const _ as *const _,
371 self as *mut Output as *mut _,
372 Some(handle_destroy_resource),
373 );
374 ffi::wl_resource_post_event(wm_v1, ffi::ZCCE_WINDOW_MANAGER_V1_OUTPUT, res); // zcce_window_manager_v1.output
375 res
376 } else {
377 self.object
378 };
379
380 if !self.sent_wl_output {
381 let global = ffi::river_wlr_output_get_global(wlr_output);
382 if !global.is_null() {
383 let client = ffi::wl_resource_get_client(output_v1);
384 let wl_output_name = ffi::wl_global_get_name(global, client);
385 zcce_output_send_wl_output(output_v1, wl_output_name);
386 self.sent_wl_output = true;
387 }
388 }
389
390 let (scheduled_width, scheduled_height) = self.scheduled.dimensions();
391 let (sent_width, sent_height) = self.sent.dimensions();
392
393 if new || scheduled_width != sent_width || scheduled_height != sent_height {
394 zcce_output_send_dimensions(output_v1, scheduled_width, scheduled_height);
395 }
396 if new || self.scheduled.x != self.sent.x || self.scheduled.y != self.sent.y {
397 zcce_output_send_position(output_v1, self.scheduled.x, self.scheduled.y);
398 }
399 }
400
401 self.sent = self.scheduled;
402
403 wl_list_remove(&mut self.link_sent as *mut ffi::wl_list as *mut WlList);
404 let sent_outputs = &mut (*self.server).wm.sent.outputs as *mut ffi::wl_list as *mut WlList;
405 wl_list_insert((*sent_outputs).prev, &mut self.link_sent as *mut ffi::wl_list as *mut WlList);
406 }
407 OutputStateValue::DisabledHard | OutputStateValue::Destroying => {
408 self.make_inert();
409
410 self.sent = self.scheduled;
411
412 if self.scheduled.state == OutputStateValue::Destroying {
413 assert!(self.wlr_output.is_null());
414
415 if !self.background_rect.is_null() {
416 ffi::wlr_scene_node_destroy(self.background_rect as *mut ffi::wlr_scene_node);
417 self.background_rect = std::ptr::null_mut();
418 }
419
420 if !self.grid_tree.is_null() {
421 ffi::wlr_scene_node_destroy(self.grid_tree as *mut ffi::wlr_scene_node);
422 self.grid_tree = std::ptr::null_mut();
423 }
424 if !self.grid_backdrop_tree.is_null() {
425 ffi::wlr_scene_node_destroy(self.grid_backdrop_tree as *mut ffi::wlr_scene_node);
426 self.grid_backdrop_tree = std::ptr::null_mut();
427 self.grid_backdrop_rect = std::ptr::null_mut();
428 }
429 self.grid_rect_pool.clear();
430 // The bevel and label subtrees died with grid_tree above.
431 self.grid_bevel_pool.clear();
432 self.grid_bevel_tree = std::ptr::null_mut();
433 self.cell_label_pool.clear();
434 self.cell_label_tree = std::ptr::null_mut();
435
436 if !self.adjust_tree.is_null() {
437 ffi::wlr_scene_node_destroy(self.adjust_tree as *mut ffi::wlr_scene_node);
438 self.adjust_tree = std::ptr::null_mut();
439 }
440 self.adjust_rects.clear();
441
442 // remove output from windows fullscreen hint
443 for &window in (*self.server).wm.windows.iter() {
444 if let crate::window::FullscreenRequest::Fullscreen(out) = (*window).wm_scheduled.fullscreen_requested {
445 if out == self as *mut Output {
446 (*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut());
447 }
448 }
449 if (*window).wm_requested.fullscreen == self as *mut Output {
450 (*window).wm_requested.fullscreen = std::ptr::null_mut();
451 }
452 }
453
454 wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut WlList);
455 wl_list_remove(&mut self.link_sent as *mut ffi::wl_list as *mut WlList);
456
457 let _ = Box::from_raw(self as *mut Output);
458 }
459 }
460 }
461 }
462
463 pub unsafe fn create(server: *mut Server, wlr_output: *mut ffi::wlr_output) -> Result<(), &'static str> {
464 let title = format!("river - {}\0", std::ffi::CStr::from_ptr(ffi::river_wlr_output_get_name(wlr_output)).to_string_lossy());
465
466 // Check if output is Wayland/X11 and set application title/app_id
467 if ffi::wlr_output_is_wl(wlr_output) {
468 ffi::wlr_wl_output_set_app_id(wlr_output, "river\0".as_ptr() as *const _);
469 ffi::wlr_wl_output_set_title(wlr_output, title.as_ptr() as *const _);
470 } else if ffi::wlr_output_is_x11(wlr_output) {
471 ffi::wlr_x11_output_set_title(wlr_output, title.as_ptr() as *const _);
472 }
473
474 if !ffi::wlr_output_init_render(wlr_output, (*server).allocator, (*server).renderer) {
475 return Err("Failed to initialize renderer for output");
476 }
477
478 let scene_output = ffi::wlr_scene_output_create((*server).scene.wlr_scene, wlr_output);
479 if scene_output.is_null() {
480 return Err("Failed to create wlr_scene_output");
481 }
482
483 let name_raw = ffi::river_wlr_output_get_name(wlr_output);
484 let name = std::ffi::CStr::from_ptr(name_raw).to_string_lossy();
485 let scale_key = format!("scale_{}", name);
486 let output_scale = (*server).wm.display.get(&scale_key)
487 .map(|&s| s as f32)
488 .unwrap_or((*server).wm.output_scale);
489
490 // The physical size every client's wl_output geometry will carry,
491 // which cce-ui's `units::Metric` measures logical px per mm against.
492 // A configured `size_mm` replaces the EDID figure before the global
493 // exists (wlr_output_layout_add creates it), so no client ever sees
494 // the lie. Logged either way: an output with no size at all leaves
495 // clients on the assumed 96 ppi, and that is worth knowing.
496 let (mut edid_w, mut edid_h) = (0i32, 0i32);
497 ffi::river_wlr_output_get_phys_size(wlr_output, &mut edid_w, &mut edid_h);
498 let configured = (*server).wm.display.get(&format!("mm_w_{}", name))
499 .zip((*server).wm.display.get(&format!("mm_h_{}", name)))
500 .map(|(&w, &h)| (w.round() as i32, h.round() as i32));
501 match configured {
502 Some((w, h)) => {
503 ffi::river_wlr_output_set_phys_size(wlr_output, w, h);
504 log::info!("output {}: physical size {}x{} mm (configured size_mm; EDID said {}x{})", name, w, h, edid_w, edid_h);
505 }
506 None if edid_w > 0 && edid_h > 0 => {
507 log::info!("output {}: physical size {}x{} mm (EDID)", name, edid_w, edid_h);
508 }
509 None => {
510 log::info!("output {}: no physical size — clients assume 96 ppi; set `output {{ {} size_mm=\"WxH\" }}` to measure", name, name);
511 }
512 }
513
514 let initial = OutputState {
515 state: OutputStateValue::DisabledHard,
516 x: 0,
517 y: 0,
518 mode: OutputMode::None,
519 scale: output_scale,
520 transform: ffi::wl_output_transform_WL_OUTPUT_TRANSFORM_NORMAL,
521 adaptive_sync: ffi::river_wlr_output_get_adaptive_sync_status(wlr_output) == ffi::wlr_output_adaptive_sync_status_WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED,
522 auto_layout: true,
523 };
524
525 let output = Box::new(Output {
526 server,
527 wlr_output,
528 scene_output,
529 background_rect: std::ptr::null_mut(),
530 grid_tree: std::ptr::null_mut(),
531 grid_backdrop_tree: std::ptr::null_mut(),
532 grid_backdrop_rect: std::ptr::null_mut(),
533 adjust_tree: std::ptr::null_mut(),
534 adjust_rects: Vec::new(),
535 last_adjust_mode: false,
536 object: std::ptr::null_mut(),
537 layer_shell: LayerShellOutput::default(),
538 lock_render_state: LockRenderState::Blanked,
539 link: std::mem::zeroed(),
540 link_sent: std::mem::zeroed(),
541 scheduled: initial,
542 sent: initial,
543 current: initial,
544 sent_wl_output: false,
545 rendering_requested: RenderingState { tearing: false },
546 rendering_current: RenderingState { tearing: false },
547 last_rendered_pan_x: f64::NAN,
548 last_rendered_pan_y: f64::NAN,
549 last_rendered_zoom: f64::NAN,
550 present_when_ns: 0,
551 present_seq: 0,
552 present_refresh_ns: 0,
553 present_dropped: 0,
554 frame_phase_ns: u64::MAX,
555 backdrop_epoch: u64::MAX,
556 backdrop_cam: (f64::NAN, f64::NAN, f64::NAN),
557 backdrop_measured_at: None,
558 tearing_test_failed: false,
559 idle_off: false,
560 last_grid_viewport_w: 0,
561 last_grid_viewport_h: 0,
562 last_grid_zoom: 0.0,
563 last_grid_spec: None,
564 grid_rect_pool: Vec::new(),
565 grid_bevel_pool: Vec::new(),
566 grid_bevel_tree: std::ptr::null_mut(),
567 cell_label_tree: std::ptr::null_mut(),
568 last_grid_bevel: None,
569 grid_force_redraw_frames: 0,
570 cell_label_pool: Vec::new(),
571 cell_labels: Default::default(),
572 last_label_px: 0,
573 status_win_samples: Vec::new(),
574 destroy: std::mem::zeroed(),
575 request_state: std::mem::zeroed(),
576 frame: std::mem::zeroed(),
577 present: std::mem::zeroed(),
578 });
579
580 let raw = Box::into_raw(output);
581 ffi::river_wlr_output_set_data(wlr_output, raw as *mut std::ffi::c_void);
582
583 let list_head = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
584 let link_custom = &mut (*raw).link as *mut ffi::wl_list as *mut WlList;
585 wl_list_insert((*list_head).prev, link_custom);
586
587 ffi::wl_list_init(&mut (*raw).link_sent);
588
589 // Add event listeners
590 let d_listener = &mut (*raw).destroy as *mut ffi::wl_listener as *mut WlListener;
591 (*d_listener).notify = Some(handle_destroy);
592 wl_signal_add(ffi::river_wlr_output_get_destroy_signal(wlr_output), &mut (*raw).destroy);
593
594 let req_listener = &mut (*raw).request_state as *mut ffi::wl_listener as *mut WlListener;
595 (*req_listener).notify = Some(handle_request_state);
596 wl_signal_add(ffi::river_wlr_output_get_request_state_signal(wlr_output), &mut (*raw).request_state);
597
598 let frame_listener = &mut (*raw).frame as *mut ffi::wl_listener as *mut WlListener;
599 (*frame_listener).notify = Some(handle_frame);
600 wl_signal_add(ffi::river_wlr_output_get_frame_signal(wlr_output), &mut (*raw).frame);
601
602 let pres_listener = &mut (*raw).present as *mut ffi::wl_listener as *mut WlListener;
603 (*pres_listener).notify = Some(handle_present);
604 wl_signal_add(ffi::river_wlr_output_get_present_signal(wlr_output), &mut (*raw).present);
605
606 (*raw).scheduled.state = OutputStateValue::Enabled;
607 let preferred = ffi::wlr_output_preferred_mode(wlr_output);
608 if !preferred.is_null() {
609 (*raw).scheduled.mode = OutputMode::Standard(preferred);
610 } else {
611 (*raw).scheduled.mode = OutputMode::Custom { width: 1280, height: 720, refresh: 0 };
612 }
613
614 (*server).wm.dirty_windowing();
615 Ok(())
616 }
617
618 pub unsafe fn render_and_commit(&mut self) -> Result<(), &'static str> {
619 // Update grid node positions and parameters first, which marks the scene output as damaged if changed
620 self.draw_grid();
621 self.draw_adjust_overlay();
622 // Right after draw_grid, whose geometry this reuses, and BEFORE the
623 // needs-frame early-out: a camera move slides the lattice under a
624 // segment that has no damage of its own, and the bar still has to
625 // hear about it — once the camera has SETTLED. During the motion
626 // itself the measurement is skipped: the sliding lattice changed the
627 // quantized reading on most frames (a push and a bar repaint each),
628 // and a window under a segment moved its sampled region every frame,
629 // defeating the per-(window, region) readback cache — a GPU texture
630 // readback inside the render path, per pan frame. The settle's full
631 // repaint frame runs the measurement with the final camera.
632 //
633 // And not on every frame either: the desktop half of the reading is
634 // pure geometry, which only moves with a transaction (`layout_epoch`)
635 // or the camera; the window-content half is throttled inside
636 // `window_backdrop_sample` at `BACKDROP_REFRESH` anyway. Running the
637 // walk — Vecs, a String per segment, the O(segments × windows) scan,
638 // then `update_status` rebuilding and comparing the whole status
639 // snapshot — every vblank was the largest steady CPU cost of an idle
640 // desktop with anything animating on it.
641 {
642 let wm = &(*self.server).wm;
643 if !wm.viewport_is_active {
644 let cam = (wm.desk_pan_x, wm.desk_pan_y, wm.desk_zoom);
645 let due = self.backdrop_epoch != wm.layout_epoch
646 || self.backdrop_cam != cam
647 || self.backdrop_measured_at.map_or(true, |t| t.elapsed() >= BACKDROP_REFRESH);
648 if due {
649 self.backdrop_epoch = wm.layout_epoch;
650 self.backdrop_cam = cam;
651 self.backdrop_measured_at = Some(std::time::Instant::now());
652 self.measure_status_backdrops();
653 }
654 }
655 }
656
657 // A parked `ccectl screenshot` targeting this output forces a render
658 // even without damage so there is a fresh buffer to read back.
659 let pending_shot = {
660 let wm = &mut (*self.server).wm;
661 if wm
662 .pending_screenshot
663 .as_ref()
664 .map(|s| s.output == self as *mut Output)
665 .unwrap_or(false)
666 {
667 wm.pending_screenshot.take()
668 } else {
669 None
670 }
671 };
672
673 // One chokepoint for every camera-mutation path (wheel zoom, IPC,
674 // keyed actions, edge-pan, the pan animation — whichever of
675 // update_viewport_local or the manage transaction carried it): if
676 // the camera changed since this output last rendered, per-node
677 // damage under-reports the whole-screen relayout (stale slivers of
678 // the previous zoom survive wherever idle content used to be), so
679 // force a full repaint. Must run BEFORE the needs-frame early-out —
680 // a camera change with no other pending damage would otherwise skip
681 // the frame entirely.
682 {
683 // Quantized to screen pixels: a sub-pixel pan moves no node
684 // (see `update_viewport_local`), so it is not a reason to paint.
685 let wm = &(*self.server).wm;
686 let q = wm.desk_zoom * self.current.scale as f64;
687 let cam = ((wm.desk_pan_x * q).round(), (wm.desk_pan_y * q).round(), wm.desk_zoom);
688 if cam != (self.last_rendered_pan_x, self.last_rendered_pan_y, self.last_rendered_zoom) {
689 self.last_rendered_pan_x = cam.0;
690 self.last_rendered_pan_y = cam.1;
691 self.last_rendered_zoom = cam.2;
692 if !self.scene_output.is_null() {
693 ffi::river_scene_output_damage_whole(self.scene_output);
694 }
695 }
696 }
697
698 if pending_shot.is_none() && !ffi::wlr_scene_output_needs_frame(self.scene_output) {
699 return Ok(());
700 }
701
702 // Re-apply scale to all windows whose scale is not 1.0 right before rendering
703 let wm = &(*self.server).wm;
704 for &window in wm.windows.iter() {
705 if !window.is_null() && ((*window).scale != 1.0 || (*window).x11_buffer_scale() != 1.0) {
706 (*window).scale_only_render_finish();
707 }
708 }
709 for &or in wm.override_redirects.iter() {
710 if !or.is_null() {
711 (*or).apply_x11_scale();
712 }
713 }
714
715 // Overview-delay debugging: with `CCE_OVDBG=1` in the compositor's
716 // environment, while /tmp/cce-ovdbg exists (contents = comma-separated
717 // app_id substrings), dump the scene-side truth for matching windows
718 // every rendered frame. Toggle live with
719 // `echo firefox,cce-calendar > /tmp/cce-ovdbg`; `rm` to stop. The env
720 // gate is what keeps a release build from doing an open()+read() of
721 // that path on every frame it ever renders.
722 if ovdbg_enabled() {
723 if let Ok(filter) = std::fs::read_to_string("/tmp/cce-ovdbg") {
724 let pats: Vec<&str> = filter.trim().split(',').filter(|p| !p.is_empty()).collect();
725 for &window in wm.windows.iter() {
726 if window.is_null() || (*window).closed {
727 continue;
728 }
729 let app = (*window).get_app_id_string().unwrap_or_default();
730 if !pats.iter().any(|p| app.contains(p)) {
731 continue;
732 }
733 let now = std::time::SystemTime::now()
734 .duration_since(std::time::UNIX_EPOCH)
735 .unwrap_or_default();
736 eprintln!(
737 "[ovdbg] t={}.{:03} win {} scale={} req=({},{}) box=({},{}) state={:?} hidden={} saved={} tree_en={}",
738 now.as_secs(),
739 now.subsec_millis(),
740 app,
741 (*window).scale,
742 (*window).rendering_requested.x,
743 (*window).rendering_requested.y,
744 (*window).box_geom.x,
745 (*window).box_geom.y,
746 (*window).state,
747 (*window).rendering_requested.hidden,
748 (*window).surfaces.saved,
749 ffi::river_scene_node_get_enabled((*window).tree as *mut ffi::wlr_scene_node),
750 );
751 if let Ok(tag) = std::ffi::CString::new(app) {
752 ffi::river_scene_shadow_dbg((*window).shadow, tag.as_ptr());
753 ffi::river_scene_ovdbg_dump(
754 (*window).tree as *mut ffi::wlr_scene_node,
755 tag.as_ptr(),
756 );
757 }
758 }
759 }
760 }
761
762 let mut state = std::mem::zeroed();
763 ffi::wlr_output_state_init(&mut state);
764
765 self.current.apply_no_modeset(&mut state);
766
767 if !ffi::wlr_scene_output_build_state(self.scene_output, &mut state, std::ptr::null()) {
768 ffi::wlr_output_state_finish(&mut state);
769 return Err("Failed to build scene state");
770 }
771
772 if self.rendering_current.tearing {
773 // The test is an atomic TEST_ONLY commit; once it has said no for
774 // this episode, asking again every frame just taxes the game.
775 if !self.tearing_test_failed {
776 state.tearing_page_flip = true;
777 if !ffi::wlr_output_test_state(self.wlr_output, &state) {
778 state.tearing_page_flip = false;
779 self.tearing_test_failed = true;
780 }
781 }
782 } else {
783 self.tearing_test_failed = false;
784 }
785
786 if !ffi::wlr_output_commit_state(self.wlr_output, &state) {
787 ffi::wlr_output_state_finish(&mut state);
788 return Err("Failed to commit state");
789 }
790
791 // Read the just-committed frame back while the state's buffer is
792 // still alive; encode/notify happen on a worker thread.
793 if let Some(mut shot) = pending_shot {
794 if state.buffer.is_null() {
795 log::warn!("screenshot: output state has no buffer");
796 shot.reply_err("screenshot: output state has no buffer");
797 } else {
798 crate::screenshot::capture_state_buffer(
799 (*self.server).renderer,
800 state.buffer,
801 ffi::river_wlr_output_get_width(self.wlr_output),
802 ffi::river_wlr_output_get_height(self.wlr_output),
803 shot,
804 );
805 }
806 }
807
808 ffi::wlr_output_state_finish(&mut state);
809
810 match (*self.server).lock_manager.state {
811 LockState::Unlocked => {
812 if self.lock_render_state != LockRenderState::Unlocked {
813 self.lock_render_state = LockRenderState::PendingUnlock;
814 }
815 }
816 LockState::Locked => {
817 // Assert normal tree disabled, lock surface rendered
818 }
819 LockState::WaitingForBlank => {
820 if self.lock_render_state != LockRenderState::Blanked {
821 self.lock_render_state = LockRenderState::PendingBlank;
822 }
823 }
824 LockState::WaitingForLockSurfaces => {
825 if let Some(_lock_surf) = (*self.server).lock_manager.lock_surface_from_output(self) {
826 if self.lock_render_state != LockRenderState::LockSurface {
827 self.lock_render_state = LockRenderState::PendingLockSurface;
828 }
829 } else {
830 if self.lock_render_state != LockRenderState::Unlocked {
831 self.lock_render_state = LockRenderState::PendingUnlock;
832 }
833 }
834 }
835 }
836
837 Ok(())
838 }
839
840 pub unsafe fn update_background_color(&mut self) {
841 if !self.background_rect.is_null() {
842 let wm = &(*self.server).wm;
843 let color: [f32; 4] = [
844 (wm.layout.background_r as f64 / u32::MAX as f64) as f32,
845 (wm.layout.background_g as f64 / u32::MAX as f64) as f32,
846 (wm.layout.background_b as f64 / u32::MAX as f64) as f32,
847 (wm.layout.background_a as f64 / u32::MAX as f64) as f32,
848 ];
849 ffi::wlr_scene_rect_set_color(self.background_rect, color.as_ptr());
850 }
851 }
852
853 /// Measure what each status segment on this output is composited over
854 /// and push the result to the bar (see [`crate::backdrop`] for why this
855 /// is geometry rather than a readback).
856 ///
857 /// Runs per frame. The cost is a handful of rect intersections per
858 /// segment; the resend gate is `update_status`'s equality check against
859 /// the last update, which the whole-percent quantization makes stick —
860 /// so a still desktop pushes nothing however many frames go by.
861 pub unsafe fn measure_status_backdrops(&mut self) {
862 let wm = &(*self.server).wm;
863 if wm.status_sender.is_none() {
864 return;
865 }
866
867 let (viewport_w, viewport_h) = self.current.dimensions();
868 let out_rect = crate::backdrop::Rect {
869 x: self.sent.x,
870 y: self.sent.y,
871 w: viewport_w,
872 h: viewport_h,
873 };
874
875 // The opaque ground the grid is drawn onto, so a gap or cell color
876 // carrying alpha resolves against what the screen actually shows.
877 let base = [
878 (wm.layout.background_r as f64 / u32::MAX as f64) as f32,
879 (wm.layout.background_g as f64 / u32::MAX as f64) as f32,
880 (wm.layout.background_b as f64 / u32::MAX as f64) as f32,
881 ];
882
883 let spec = wm.layout.background_spec();
884 let grid = match &spec {
885 crate::policy::api::BackgroundSpec::Grid(g) => g,
886 // No lattice: every segment sits on the flat background color,
887 // which `measure` still reports correctly through an empty frame.
888 _ => {
889 let flat = crate::policy::background::GridFrame {
890 tree_pos: None,
891 period_px_x: 0,
892 period_px_y: 0,
893 period_px_exact_x: 0.0,
894 period_px_exact_y: 0.0,
895 backdrop_w: 0,
896 backdrop_h: 0,
897 cells: None,
898 first_col: 0,
899 first_row: 0,
900 };
901 self.store_backdrops(&flat, crate::policy::api::Rgba([base[0], base[1], base[2], 1.0]), base, out_rect);
902 return;
903 }
904 };
905
906 let frame = crate::policy::background::grid_frame(
907 grid,
908 wm.camera(),
909 viewport_w,
910 viewport_h,
911 self.sent.x,
912 self.sent.y,
913 );
914 let gap = grid.gap_color;
915 self.store_backdrops(&frame, gap, base, out_rect);
916 }
917
918 /// The half of [`Self::measure_status_backdrops`] that walks the windows:
919 /// each status segment on this output measured against `frame`, with any
920 /// window covering part of it sampled for its actual content and folded in.
921 unsafe fn store_backdrops(
922 &mut self,
923 frame: &crate::policy::background::GridFrame,
924 gap: crate::policy::api::Rgba,
925 base: [f32; 3],
926 out_rect: crate::backdrop::Rect,
927 ) {
928 let wm = &(*self.server).wm;
929
930 let visible = |w: *mut crate::window::Window| -> bool {
931 !w.is_null()
932 && !(*w).closed
933 && !(*w).minimized
934 && !matches!((*w).state, crate::window::WindowState::Closing | crate::window::WindowState::Init)
935 };
936 let rect_of = |w: *mut crate::window::Window| crate::backdrop::Rect {
937 x: (*w).box_geom.x,
938 y: (*w).box_geom.y,
939 w: (*w).box_geom.width,
940 h: (*w).box_geom.height,
941 };
942
943 let mut mine: Vec<(String, u8, u8)> = Vec::new();
944 let mut live_keys: Vec<(u32, crate::backdrop::Rect)> = Vec::new();
945 for &seg in wm.windows.iter() {
946 if !visible(seg) || !(*seg).is_status_bar() {
947 continue;
948 }
949 let seg_rect = rect_of(seg);
950 if !seg_rect.intersects(&out_rect) {
951 continue;
952 }
953 let Some(app_id) = (*seg).get_app_id_string() else {
954 continue;
955 };
956
957 let desktop = crate::backdrop::measure(frame, gap, base, seg_rect, false);
958
959 // The window covering the most of this segment, if any. Stacking
960 // order is deliberately not consulted: the compositor's own
961 // hit-test answers with the segment itself (it is on top of
962 // whatever it is asking about), and where two windows both reach
963 // under one segment the larger share is the better guess at what
964 // the text is actually over.
965 let mut best: Option<(*mut crate::window::Window, i64)> = None;
966 for &other in wm.windows.iter() {
967 if other == seg || !visible(other) {
968 continue;
969 }
970 if (*other).is_status_bar() || (*other).is_wallpaper() || (*other).is_grid() {
971 continue;
972 }
973 let area = rect_of(other).intersect_area(&seg_rect);
974 if area > 0 && best.map_or(true, |(_, a)| area > a) {
975 best = Some((other, area));
976 }
977 }
978
979 let sample = match best {
980 None => desktop,
981 Some((win, area)) => {
982 let region = rect_of(win).intersection(&seg_rect).unwrap_or(seg_rect);
983 let key = ((*win).ref_key.index, region);
984 live_keys.push(key);
985 match self.window_backdrop_sample(win, region) {
986 // Blended, not replaced: a window covering half a
987 // segment leaves the other half on the desktop, and
988 // the seam between them is its own legibility problem.
989 Some(w) => {
990 let coverage = area as f32 / (seg_rect.w as f32 * seg_rect.h as f32).max(1.0);
991 crate::backdrop::blend(desktop, w, coverage)
992 }
993 // Unreadable content (no committed buffer yet, an
994 // unsupported read format): the honest answer is still
995 // "unknown", exactly as before this path existed.
996 None => crate::backdrop::UNKNOWN,
997 }
998 }
999 };
1000 mine.push((app_id, sample.luma, sample.spread));
1001 }
1002
1003 // Drop cache entries for segment/window pairs that no longer exist,
1004 // so a closed window or a moved segment cannot pin a stale reading.
1005 self.status_win_samples
1006 .retain(|e| live_keys.iter().any(|(k, r)| *k == e.win && *r == e.region));
1007
1008 {
1009 let mut store = wm.status_backdrops.borrow_mut();
1010 // Replace only this output's segments; another output's entries
1011 // are its own to maintain. Sorted so a reordering of the window
1012 // list cannot, by itself, look like a change worth resending.
1013 store.retain(|(id, _, _)| !mine.iter().any(|(m, _, _)| m == id));
1014 store.extend(mine);
1015 store.sort_by(|a, b| a.0.cmp(&b.0));
1016 }
1017
1018 wm.update_status();
1019 }
1020
1021 /// The window-content half of the backdrop measurement: what `win` is
1022 /// actually showing inside `region` (layout px), or None when it cannot be
1023 /// read.
1024 ///
1025 /// Throttled and cached per (window, region) — this is the one part of the
1026 /// measurement that costs a texture readback and its GPU sync, and it runs
1027 /// inside the render path.
1028 unsafe fn window_backdrop_sample(
1029 &mut self,
1030 win: *mut crate::window::Window,
1031 region: crate::backdrop::Rect,
1032 ) -> Option<crate::backdrop::BackdropSample> {
1033 /// Re-read a window's content at most this often, per segment.
1034 const WIN_SAMPLE_MS: u128 = 250;
1035 /// Refuse to read back more than this many pixels in one sample. A bar
1036 /// strip is naturally short, so this only trips on an implausibly wide
1037 /// segment at a high buffer scale — where reporting "unknown" and
1038 /// wearing the outline beats stalling the render thread.
1039 const MAX_SAMPLE_PX: i64 = 512 * 1024;
1040
1041 let id = (*win).ref_key.index;
1042 let now = std::time::Instant::now();
1043 let seq = Self::surface_content_seq((*win).root_surface());
1044 if let Some(hit) = self
1045 .status_win_samples
1046 .iter()
1047 .find(|e| e.win == id && e.region == region)
1048 {
1049 // Two gates, and the content one is the load-bearing half: a
1050 // window nobody is typing in never gets read a second time.
1051 if hit.seq == seq || now.duration_since(hit.at).as_millis() < WIN_SAMPLE_MS {
1052 return hit.sample;
1053 }
1054 }
1055
1056 let fresh = self.read_window_region(win, region, MAX_SAMPLE_PX);
1057 match self
1058 .status_win_samples
1059 .iter_mut()
1060 .find(|e| e.win == id && e.region == region)
1061 {
1062 Some(slot) => {
1063 slot.at = now;
1064 slot.seq = seq;
1065 slot.sample = fresh;
1066 }
1067 None => self.status_win_samples.push(StatusWinSample {
1068 win: id,
1069 region,
1070 at: now,
1071 seq,
1072 sample: fresh,
1073 }),
1074 }
1075 fresh
1076 }
1077
1078 /// Commit sequences of a surface tree, summed — a cheap "has this window
1079 /// drawn anything new?" key. Walks subsurfaces too, because a toolkit that
1080 /// renders into one can leave the root's own sequence untouched for the
1081 /// life of the window.
1082 unsafe fn surface_content_seq(root: *mut ffi::wlr_surface) -> u32 {
1083 if root.is_null() {
1084 return 0;
1085 }
1086 unsafe extern "C" fn sum_cb(
1087 surface: *mut ffi::wlr_surface,
1088 _sx: std::os::raw::c_int,
1089 _sy: std::os::raw::c_int,
1090 data: *mut std::ffi::c_void,
1091 ) {
1092 let total = &mut *(data as *mut u32);
1093 *total = total.wrapping_add(ffi::river_wlr_surface_current_seq(surface));
1094 }
1095 let mut total: u32 = 0;
1096 ffi::wlr_surface_for_each_surface(
1097 root,
1098 Some(sum_cb),
1099 &mut total as *mut u32 as *mut std::ffi::c_void,
1100 );
1101 total
1102 }
1103
1104 /// Read `region` (layout px) out of a window's committed surfaces and
1105 /// measure it. Subsurfaces are composited in, because a toolkit that puts
1106 /// its content in one would otherwise be measured as its blank root.
1107 unsafe fn read_window_region(
1108 &self,
1109 win: *mut crate::window::Window,
1110 region: crate::backdrop::Rect,
1111 max_px: i64,
1112 ) -> Option<crate::backdrop::BackdropSample> {
1113 let root = (*win).root_surface();
1114 if root.is_null() {
1115 return None;
1116 }
1117 let (mut bw, mut bh) = (0i32, 0i32);
1118 ffi::river_wlr_surface_get_buffer_size(root, &mut bw, &mut bh);
1119 if bw <= 0 || bh <= 0 {
1120 return None;
1121 }
1122 // Two scales stack here: the window's own render scale maps layout px
1123 // to surface-logical px, and the buffer scale maps those to the
1124 // physical pixels a texture read is addressed in.
1125 let logical_w = ffi::river_wlr_surface_get_width(root).max(1);
1126 let buf_scale = bw as f64 / logical_w as f64;
1127 let win_scale = if (*win).scale > 0.0 { (*win).scale } else { 1.0 };
1128 let to_buf = buf_scale / win_scale;
1129
1130 let rx = (((region.x - (*win).box_geom.x) as f64) * to_buf).round() as i32;
1131 let ry = (((region.y - (*win).box_geom.y) as f64) * to_buf).round() as i32;
1132 let rw = ((region.w as f64) * to_buf).round() as i32;
1133 let rh = ((region.h as f64) * to_buf).round() as i32;
1134 if rw <= 0 || rh <= 0 || (rw as i64) * (rh as i64) > max_px {
1135 return None;
1136 }
1137
1138 struct Collect {
1139 list: Vec<(*mut ffi::wlr_surface, i32, i32)>,
1140 }
1141 unsafe extern "C" fn collect_cb(
1142 surface: *mut ffi::wlr_surface,
1143 sx: std::os::raw::c_int,
1144 sy: std::os::raw::c_int,
1145 data: *mut std::ffi::c_void,
1146 ) {
1147 let collect = &mut *(data as *mut Collect);
1148 collect.list.push((surface, sx, sy));
1149 }
1150 let mut collect = Collect { list: Vec::new() };
1151 ffi::wlr_surface_for_each_surface(
1152 root,
1153 Some(collect_cb),
1154 &mut collect as *mut Collect as *mut std::ffi::c_void,
1155 );
1156
1157 let mut canvas = vec![0u8; (rw as usize) * (rh as usize) * 4];
1158 let mut composited = 0usize;
1159 for (surface, sx, sy) in collect.list {
1160 let texture = ffi::wlr_surface_get_texture(surface);
1161 if texture.is_null() {
1162 continue;
1163 }
1164 let (mut sw, mut sh) = (0i32, 0i32);
1165 ffi::river_wlr_surface_get_buffer_size(surface, &mut sw, &mut sh);
1166 if sw <= 0 || sh <= 0 {
1167 continue;
1168 }
1169 // Subsurface offsets are surface-logical; buffers are physical.
1170 let off_x = (sx as f64 * buf_scale).round() as i32;
1171 let off_y = (sy as f64 * buf_scale).round() as i32;
1172 let x0 = off_x.max(rx);
1173 let y0 = off_y.max(ry);
1174 let x1 = (off_x + sw).min(rx + rw);
1175 let y1 = (off_y + sh).min(ry + rh);
1176 if x1 <= x0 || y1 <= y0 {
1177 continue;
1178 }
1179 let src = ffi::wlr_box {
1180 x: x0 - off_x,
1181 y: y0 - off_y,
1182 width: x1 - x0,
1183 height: y1 - y0,
1184 };
1185 let Some((pixels, format)) =
1186 crate::screenshot::read_texture_region(texture, src, x1 - x0, y1 - y0)
1187 else {
1188 continue;
1189 };
1190 let Some(rgba) = crate::screenshot::to_rgba(pixels, format) else { continue };
1191 crate::screenshot::blit(&mut canvas, rw, rh, &rgba, x1 - x0, y1 - y0, x0 - rx, y0 - ry);
1192 composited += 1;
1193 }
1194 if composited == 0 {
1195 return None;
1196 }
1197 crate::backdrop::measure_pixels(&canvas)
1198 }
1199
1200 pub unsafe fn draw_adjust_overlay(&mut self) {
1201 if self.adjust_tree.is_null() {
1202 return;
1203 }
1204
1205 let wm = &(*self.server).wm;
1206 if wm.adjust_position_mode != self.last_adjust_mode {
1207 self.last_adjust_mode = wm.adjust_position_mode;
1208 ffi::wlr_output_schedule_frame(self.wlr_output);
1209 }
1210
1211 if !wm.adjust_position_mode {
1212 ffi::wlr_scene_node_set_enabled(self.adjust_tree as *mut ffi::wlr_scene_node, false);
1213 return;
1214 }
1215
1216 // Enable the overlay tree.
1217 ffi::wlr_scene_node_set_enabled(self.adjust_tree as *mut ffi::wlr_scene_node, true);
1218 ffi::wlr_scene_node_lower_to_bottom(self.adjust_tree as *mut ffi::wlr_scene_node);
1219
1220 let (viewport_w, viewport_h) = self.current.dimensions();
1221 let w = viewport_w;
1222 let h = viewport_h;
1223
1224 // Semicircles size: 120 x 120 (so radius = 60).
1225 let targets = [
1226 // TopLeft (nw): x = 0, y = -60
1227 (0, -60),
1228 // TopCenter (n): x = w/2 - 60, y = -60
1229 (w / 2 - 60, -60),
1230 // TopRight (ne): x = w - 120, y = -60
1231 (w - 120, -60),
1232 // BottomLeft (sw): x = 0, y = h - 60
1233 (0, h - 60),
1234 // BottomCenter (s): x = w/2 - 60, y = h - 60
1235 (w / 2 - 60, h - 60),
1236 // BottomRight (se): x = w - 120, y = h - 60
1237 (w - 120, h - 60),
1238 // Left (w): x = -60, y = h/2 - 60
1239 (-60, h / 2 - 60),
1240 // Right (e): x = w - 60, y = h/2 - 60
1241 (w - 60, h / 2 - 60),
1242 ];
1243
1244 let color: [f32; 4] = [0.4, 0.6, 0.9, 0.5];
1245 let color_ptr = color.as_ptr();
1246
1247 for (idx, &(tx, ty)) in targets.iter().enumerate() {
1248 let rect = if idx < self.adjust_rects.len() {
1249 let node = self.adjust_rects[idx];
1250 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, true);
1251 ffi::wlr_scene_rect_set_size(node, 120, 120);
1252 ffi::wlr_scene_rect_set_color(node, color_ptr);
1253 node
1254 } else {
1255 let node = ffi::wlr_scene_rect_create(self.adjust_tree, 120, 120, color_ptr);
1256 if !node.is_null() {
1257 self.adjust_rects.push(node);
1258 // Make it circular!
1259 ffi::river_scene_rect_set_corner_radius(node, 60);
1260 }
1261 node
1262 };
1263
1264 if !rect.is_null() {
1265 ffi::wlr_scene_node_set_position(rect as *mut ffi::wlr_scene_node, tx, ty);
1266 }
1267 }
1268
1269 // Disable any extra rects in the pool if we somehow have more
1270 for idx in targets.len()..self.adjust_rects.len() {
1271 let node = self.adjust_rects[idx];
1272 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, false);
1273 }
1274 }
1275
1276 /// Draw the desktop background from the policy crate's declarative
1277 /// spec: `Layout::background_spec()` says WHAT to show, and (for the
1278 /// grid) `policy::background::grid_frame` derives this frame's geometry
1279 /// — tree shift, backdrop extent, cell lattice, density fade. This side
1280 /// keeps the scene nodes, the rect reuse pool, and scenefx's fade-inset
1281 /// wire encoding.
1282 pub unsafe fn draw_grid(&mut self) {
1283 if self.grid_tree.is_null() {
1284 return;
1285 }
1286
1287 let wm = &(*self.server).wm;
1288
1289 // Enable the grid tree.
1290 ffi::wlr_scene_node_set_enabled(self.grid_tree as *mut ffi::wlr_scene_node, true);
1291
1292 // Keep the grid tree (cells + rims) at the top of the background layer,
1293 // above the client backgrounds in layers.background_clients.
1294 ffi::wlr_scene_node_raise_to_top(self.grid_tree as *mut ffi::wlr_scene_node);
1295
1296 // The backdrop goes BELOW the client backgrounds: its own tree, placed
1297 // just above this output's base rect (or at the very bottom), so a
1298 // layer-shell Background surface or a wallpaper window replaces the flat
1299 // colour and keeps the cell lattice.
1300 if self.grid_backdrop_tree.is_null() {
1301 self.grid_backdrop_tree = ffi::wlr_scene_tree_create((*self.server).scene.layers.background);
1302 ffi::river_scene_tree_set_desk_offset(self.grid_backdrop_tree, true);
1303 if self.grid_backdrop_tree.is_null() {
1304 return;
1305 }
1306 if !self.background_rect.is_null() {
1307 ffi::wlr_scene_node_lower_to_bottom(self.background_rect as *mut ffi::wlr_scene_node);
1308 ffi::wlr_scene_node_place_above(
1309 self.grid_backdrop_tree as *mut ffi::wlr_scene_node,
1310 self.background_rect as *mut ffi::wlr_scene_node,
1311 );
1312 } else {
1313 ffi::wlr_scene_node_lower_to_bottom(self.grid_backdrop_tree as *mut ffi::wlr_scene_node);
1314 }
1315 }
1316 ffi::wlr_scene_node_set_enabled(self.grid_backdrop_tree as *mut ffi::wlr_scene_node, true);
1317 let backdrop_tree = self.grid_backdrop_tree;
1318 let backdrop_rect = &mut self.grid_backdrop_rect;
1319 let mut set_backdrop = |w: i32, h: i32, color_ptr: *const f32| {
1320 if backdrop_rect.is_null() {
1321 *backdrop_rect = ffi::wlr_scene_rect_create(backdrop_tree, w, h, color_ptr);
1322 } else {
1323 ffi::wlr_scene_rect_set_size(*backdrop_rect, w, h);
1324 ffi::wlr_scene_rect_set_color(*backdrop_rect, color_ptr);
1325 }
1326 };
1327
1328 let (viewport_w, viewport_h) = self.current.dimensions();
1329 let spec = wm.layout.background_spec();
1330 let zoom = crate::policy::background::sanitized_zoom(wm.desk_zoom);
1331
1332 // Spec/viewport/zoom/bevel changes force a redraw of the pools;
1333 // pan alone only moves the grid tree.
1334 let bevel_key = [
1335 wm.layout.bevel_enabled as u32,
1336 wm.layout.bevel_thickness.to_bits(),
1337 wm.layout.bevel_light_x.to_bits(),
1338 wm.layout.bevel_light_y.to_bits(),
1339 wm.layout.bevel_light_intensity.to_bits(),
1340 wm.layout.bevel_shade_intensity.to_bits(),
1341 wm.layout.bevel_shoulder.to_bits(),
1342 ];
1343 let structure_changed = self.last_grid_viewport_w != viewport_w
1344 || self.last_grid_viewport_h != viewport_h
1345 || self.last_grid_zoom != zoom
1346 || self.last_grid_spec.as_ref() != Some(&spec)
1347 || self.last_grid_bevel != Some(bevel_key);
1348 if structure_changed {
1349 self.grid_force_redraw_frames = 3;
1350 // Fallback-grid structure (spec/zoom/viewport) is backdrop
1351 // content in the optimized-blur capture set — same staleness
1352 // rule as the client-grid latch. Not while a camera gesture
1353 // holds the bakes frozen: the zoom restructures the grid every
1354 // frame, and the settle re-bakes once at the end.
1355 if !wm.viewport_is_active {
1356 ffi::river_scene_mark_optimized_blur_dirty((*self.server).scene.wlr_scene);
1357 }
1358 }
1359 let force = self.grid_force_redraw_frames > 0;
1360 if force {
1361 self.grid_force_redraw_frames -= 1;
1362 self.last_grid_viewport_w = viewport_w;
1363 self.last_grid_viewport_h = viewport_h;
1364 self.last_grid_zoom = zoom;
1365 self.last_grid_spec = Some(spec.clone());
1366 self.last_grid_bevel = Some(bevel_key);
1367 }
1368
1369 // The cell rims live in their own subtree kept above every pooled
1370 // rect (incl. the backdrop), so reuse order can never bury one.
1371 if self.grid_bevel_tree.is_null() {
1372 self.grid_bevel_tree = ffi::wlr_scene_tree_create(self.grid_tree);
1373 }
1374 ffi::wlr_scene_node_raise_to_top(self.grid_bevel_tree as *mut ffi::wlr_scene_node);
1375
1376 let grid_tree = self.grid_tree;
1377 if !grid_tree.is_null() {
1378 // Desk content: rendered with the camera's sub-pixel offset.
1379 ffi::river_scene_tree_set_desk_offset(grid_tree, true);
1380 }
1381 let pool = &mut self.grid_rect_pool;
1382 let mut pool_idx = 0;
1383
1384 let layout = &wm.layout;
1385 // The relief lives on the LINES, never the cells (mirroring the
1386 // cce-grid client, which must be able to latch without swapping the
1387 // grid's material): each cell's chamfer box is expanded by the
1388 // half-gap, so the lit wall occupies exactly the half-rail around
1389 // the cell — a crest at the rail centerline descending to the cell
1390 // edge — and neighboring rings abut without overlap. Cell floors
1391 // stay flat.
1392 let bevel_on = layout.bevel_enabled;
1393 // Light normalized exactly like the window bevels — the grid is lit
1394 // by the same lamp.
1395 let (bevel_lx, bevel_ly) = {
1396 let (lx, ly) = (layout.bevel_light_x, layout.bevel_light_y);
1397 let len = (lx * lx + ly * ly).sqrt();
1398 if len > 1e-6 { (lx / len, ly / len) } else { (-0.7071, -0.7071) }
1399 };
1400 let bevel_tree = self.grid_bevel_tree;
1401 let bevel_pool = &mut self.grid_bevel_pool;
1402 let mut bevel_idx = 0;
1403
1404 let mut get_bevel = |w: i32, h: i32, x: i32, y: i32, radius: i32, thickness: f32| {
1405 let bevel = if bevel_idx < bevel_pool.len() {
1406 let node = bevel_pool[bevel_idx];
1407 ffi::wlr_scene_node_set_enabled(&mut (*node).node as *mut ffi::wlr_scene_node, true);
1408 ffi::wlr_scene_bevel_set_size(node, w, h);
1409 node
1410 } else {
1411 let node = ffi::wlr_scene_bevel_create(bevel_tree, w, h, 0, 0.0, layout.bevel_color.as_ptr());
1412 if !node.is_null() {
1413 bevel_pool.push(node);
1414 }
1415 node
1416 };
1417 if !bevel.is_null() {
1418 ffi::wlr_scene_node_set_position(&mut (*bevel).node as *mut ffi::wlr_scene_node, x, y);
1419 ffi::wlr_scene_bevel_set_corner_radius(bevel, radius);
1420 ffi::wlr_scene_bevel_set_thickness(bevel, thickness.max(1.0));
1421 ffi::wlr_scene_bevel_set_light(
1422 bevel,
1423 bevel_lx,
1424 bevel_ly,
1425 layout.bevel_light_intensity,
1426 layout.bevel_shade_intensity,
1427 );
1428 ffi::wlr_scene_bevel_set_shoulder(bevel, layout.bevel_shoulder);
1429 ffi::wlr_scene_bevel_set_color(bevel, layout.bevel_color.as_ptr());
1430 }
1431 bevel_idx += 1;
1432 };
1433
1434 // Helper closure to manage/reuse the pool of wlr_scene_rect elements.
1435 let mut get_rect = |w: i32, h: i32, color_ptr: *const f32, x: i32, y: i32, corner_r: i32, fade_i: i32| -> *mut ffi::wlr_scene_rect {
1436 let rect = if pool_idx < pool.len() {
1437 let node = pool[pool_idx];
1438 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, true);
1439 ffi::wlr_scene_rect_set_size(node, w, h);
1440 ffi::wlr_scene_rect_set_color(node, color_ptr);
1441 node
1442 } else {
1443 let node = ffi::wlr_scene_rect_create(grid_tree, w, h, color_ptr);
1444 if !node.is_null() {
1445 pool.push(node);
1446 }
1447 node
1448 };
1449
1450 if !rect.is_null() {
1451 ffi::wlr_scene_node_set_position(rect as *mut ffi::wlr_scene_node, x, y);
1452 ffi::river_scene_rect_set_corner_radius(rect, corner_r);
1453 ffi::wlr_scene_rect_set_fade_inset(rect, fade_i);
1454 }
1455 pool_idx += 1;
1456 rect
1457 };
1458
1459 match &spec {
1460 crate::policy::api::BackgroundSpec::Grid(grid) => {
1461 let frame = crate::policy::background::grid_frame(
1462 grid,
1463 wm.layout_camera().0,
1464 viewport_w,
1465 viewport_h,
1466 self.sent.x,
1467 self.sent.y,
1468 );
1469
1470 if let Some((x, y)) = frame.tree_pos {
1471 ffi::river_scene_node_set_position_if_changed(
1472 grid_tree as *mut ffi::wlr_scene_node,
1473 x,
1474 y,
1475 );
1476 ffi::river_scene_node_set_position_if_changed(
1477 backdrop_tree as *mut ffi::wlr_scene_node,
1478 x,
1479 y,
1480 );
1481 }
1482
1483 if force {
1484 // Backdrop in the gap color, then the cell lattice. The
1485 // backdrop always draws — while a grid client is live it
1486 // is the safety net beyond the patch edges during fast
1487 // pans; the CELLS yield to the client's rendering.
1488 set_backdrop(frame.backdrop_w, frame.backdrop_h, grid.gap_color.0.as_ptr());
1489
1490 if let Some(cells) = frame.cells.as_ref().filter(|_| wm.grid_cells_enabled) {
1491 // scenefx fade-inset wire encoding: inset px * 1000
1492 // + fade-mode index; 0 disables the fade.
1493 use crate::policy::api::GridFadeMode;
1494 let fade_mode = match grid.fade_mode {
1495 GridFadeMode::Linear => 0,
1496 GridFadeMode::Smoothstep => 1,
1497 GridFadeMode::Quadratic => 2,
1498 GridFadeMode::Cosine => 3,
1499 GridFadeMode::Gaussian => 4,
1500 };
1501 let inset_scaled = if cells.fade_inset_px > 0 {
1502 cells.fade_inset_px * 1000 + fade_mode
1503 } else {
1504 0
1505 };
1506 // Widened exactly like the windows' corner clip:
1507 // at corner_shape > 2 the superellipse hugs the
1508 // corner, so the raw radius reads nearly square —
1509 // and a tiled window's (widened) arc must land on
1510 // the cell's arc.
1511 let cell_radius = crate::window::widen_corner_radius(
1512 cells.corner_radius_px, cells.cell_w_px, cells.cell_h_px,
1513 );
1514 // The root plate-edge roll (mirroring cce-grid): the
1515 // bevel-width knob clamped to a fraction of the
1516 // rail, pre-scaled by zoom like every cell metric,
1517 // so the rail reads as a flat face with a narrow
1518 // lip at each sunken cell — not a full-ramp grout.
1519 // style.surface.desktop.line_relief overrides the
1520 // width outright (0 = no lip). The ring expands the
1521 // cell box by the roll, inner edge concentric with
1522 // the cell arc.
1523 let gap_px = (frame.period_px_exact_x - cells.cell_w_px as f64)
1524 .min(frame.period_px_exact_y - cells.cell_h_px as f64)
1525 .max(0.0);
1526 let roll = layout
1527 .desktop_line_relief
1528 .map(|v| v * zoom)
1529 .unwrap_or_else(|| (layout.bevel_thickness as f64 * zoom).min(gap_px * 0.25))
1530 .max(0.0);
1531 let hg = roll.round() as i32;
1532 let ring_w_px = cells.cell_w_px + 2 * hg;
1533 let ring_h_px = cells.cell_h_px + 2 * hg;
1534 let ring_radius = cell_radius + hg;
1535 // Cell positions from the EXACT period, rounded per
1536 // cell: a rounded-period spacing drifts from the
1537 // world-anchored windows at fractional zooms (the
1538 // grid visibly slides against window edges when
1539 // panning).
1540 for col in 0..=cells.cols {
1541 let rel_x = (col as f64 * frame.period_px_exact_x).round() as i32;
1542 for row in 0..=cells.rows {
1543 let rel_y = (row as f64 * frame.period_px_exact_y).round() as i32;
1544 get_rect(cells.cell_w_px, cells.cell_h_px, cells.color.0.as_ptr(), rel_x, rel_y, cell_radius, inset_scaled);
1545 if bevel_on && hg > 0 {
1546 get_bevel(ring_w_px, ring_h_px, rel_x - hg, rel_y - hg, ring_radius, roll as f32);
1547 }
1548 }
1549 }
1550 }
1551 }
1552 }
1553 crate::policy::api::BackgroundSpec::Solid(color) => {
1554 ffi::river_scene_node_set_position_if_changed(
1555 grid_tree as *mut ffi::wlr_scene_node,
1556 self.sent.x,
1557 self.sent.y,
1558 );
1559 ffi::river_scene_node_set_position_if_changed(
1560 backdrop_tree as *mut ffi::wlr_scene_node,
1561 self.sent.x,
1562 self.sent.y,
1563 );
1564 if force {
1565 set_backdrop(viewport_w, viewport_h, color.0.as_ptr());
1566 }
1567 }
1568 }
1569
1570 if force {
1571 // Disable unused rects in the pool to release GPU/scene resources.
1572 for i in pool_idx..pool.len() {
1573 ffi::wlr_scene_node_set_enabled(pool[i] as *mut ffi::wlr_scene_node, false);
1574 }
1575 for i in bevel_idx..bevel_pool.len() {
1576 ffi::wlr_scene_node_set_enabled(&mut (*bevel_pool[i]).node as *mut ffi::wlr_scene_node, false);
1577 }
1578 }
1579
1580 self.draw_cell_labels();
1581 }
1582
1583 /// Name every visible desktop square, chess style, while overview is open.
1584 ///
1585 /// The labels live in the grid tree, so they inherit its modulo shift and
1586 /// ride along with a pan for free; only the world index of the first drawn
1587 /// cell (`GridFrame::first_col/row`, computed in policy) is needed to know
1588 /// what to write. Outside overview every node is disabled — this is a
1589 /// navigation aid, not desktop furniture.
1590 unsafe fn draw_cell_labels(&mut self) {
1591 let wm = &(*self.server).wm;
1592 let overview = wm.mode == crate::window_manager::WindowManagerMode::Overview
1593 && wm.layout.desktop_cell_labels;
1594
1595 if !overview {
1596 if !self.cell_label_pool.is_empty() {
1597 for &(node, _) in &self.cell_label_pool {
1598 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, false);
1599 }
1600 }
1601 return;
1602 }
1603 if self.grid_tree.is_null() {
1604 return;
1605 }
1606 if self.cell_label_tree.is_null() {
1607 self.cell_label_tree = ffi::wlr_scene_tree_create(self.grid_tree);
1608 if self.cell_label_tree.is_null() {
1609 return;
1610 }
1611 }
1612 // Above the cell rects and the bevel subtree, which draw_grid raised
1613 // just before this.
1614 ffi::wlr_scene_node_raise_to_top(self.cell_label_tree as *mut ffi::wlr_scene_node);
1615
1616 let (viewport_w, viewport_h) = self.current.dimensions();
1617 let spec = wm.layout.background_spec();
1618 let crate::policy::api::BackgroundSpec::Grid(grid) = &spec else {
1619 self.disable_cell_labels();
1620 return;
1621 };
1622 let frame = crate::policy::background::grid_frame(
1623 grid,
1624 wm.camera(),
1625 viewport_w,
1626 viewport_h,
1627 self.sent.x,
1628 self.sent.y,
1629 );
1630 let Some(cells) = &frame.cells else {
1631 self.disable_cell_labels();
1632 return;
1633 };
1634
1635 // A fixed fraction of the on-screen cell, clamped so labels stay
1636 // readable when zoomed far out and don't swell into billboards when
1637 // near. Below the floor there is no room for glyphs at all.
1638 let min_cell_px = cells.cell_w_px.min(cells.cell_h_px);
1639 let px = ((min_cell_px as f32) * 0.16).clamp(9.0, 40.0);
1640 if px * 3.0 > min_cell_px as f32 {
1641 self.disable_cell_labels();
1642 return;
1643 }
1644 let px_key = px.round() as u32;
1645 if px_key != self.last_label_px || self.cell_labels.len() > 512 {
1646 self.cell_labels.clear();
1647 self.last_label_px = px_key;
1648 // The freed buffers' addresses can be handed straight back to the
1649 // next rasterization, so a stale pointer here would compare equal
1650 // to a different label and skip the update.
1651 for entry in self.cell_label_pool.iter_mut() {
1652 entry.1 = std::ptr::null_mut();
1653 }
1654 }
1655
1656 let inset = (min_cell_px as f64 * 0.06).round() as i32;
1657 let mut idx = 0usize;
1658 for col in 0..=cells.cols {
1659 let rel_x = (col as f64 * frame.period_px_exact_x).round() as i32;
1660 for row in 0..=cells.rows {
1661 let rel_y = (row as f64 * frame.period_px_exact_y).round() as i32;
1662 let Some(label) = self.cell_labels.get_square(frame.first_col + col, frame.first_row + row, px) else {
1663 continue;
1664 };
1665 let (buf, lw, lh) = (label.buffer, label.width, label.height);
1666
1667 let node = if idx < self.cell_label_pool.len() {
1668 let (node, shown) = self.cell_label_pool[idx];
1669 if shown != buf {
1670 ffi::wlr_scene_buffer_set_buffer(node, buf);
1671 self.cell_label_pool[idx].1 = buf;
1672 }
1673 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, true);
1674 node
1675 } else {
1676 let node = ffi::wlr_scene_buffer_create(self.cell_label_tree, buf);
1677 if node.is_null() {
1678 continue;
1679 }
1680 self.cell_label_pool.push((node, buf));
1681 node
1682 };
1683 ffi::wlr_scene_buffer_set_dest_size(node, lw, lh);
1684 // Top-left corner of the cell, inside the fade inset.
1685 ffi::river_scene_node_set_position_if_changed(
1686 node as *mut ffi::wlr_scene_node,
1687 rel_x + inset,
1688 rel_y + inset,
1689 );
1690 let _ = lh;
1691 idx += 1;
1692 }
1693 }
1694
1695 for i in idx..self.cell_label_pool.len() {
1696 ffi::wlr_scene_node_set_enabled(
1697 self.cell_label_pool[i].0 as *mut ffi::wlr_scene_node,
1698 false,
1699 );
1700 }
1701 }
1702
1703 unsafe fn disable_cell_labels(&self) {
1704 for &(node, _) in &self.cell_label_pool {
1705 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, false);
1706 }
1707 }
1708 }
1709
1710 unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1711 let output = crate::container_of!(listener, Output, destroy);
1712
1713 log::debug!("Output destroyed");
1714 crate::xwayland_window::note_output_change();
1715
1716 // Remove listeners
1717 wl_listener_remove(&mut (*output).destroy);
1718 wl_listener_remove(&mut (*output).request_state);
1719 wl_listener_remove(&mut (*output).frame);
1720 wl_listener_remove(&mut (*output).present);
1721
1722 if !(*output).scene_output.is_null() {
1723 ffi::wlr_scene_output_destroy((*output).scene_output);
1724 (*output).scene_output = std::ptr::null_mut();
1725 }
1726
1727 if !(*output).background_rect.is_null() {
1728 ffi::wlr_scene_node_destroy((*output).background_rect as *mut ffi::wlr_scene_node);
1729 (*output).background_rect = std::ptr::null_mut();
1730 }
1731
1732 if !(*output).grid_tree.is_null() {
1733 ffi::wlr_scene_node_destroy((*output).grid_tree as *mut ffi::wlr_scene_node);
1734 (*output).grid_tree = std::ptr::null_mut();
1735 }
1736 if !(*output).grid_backdrop_tree.is_null() {
1737 ffi::wlr_scene_node_destroy((*output).grid_backdrop_tree as *mut ffi::wlr_scene_node);
1738 (*output).grid_backdrop_tree = std::ptr::null_mut();
1739 (*output).grid_backdrop_rect = std::ptr::null_mut();
1740 }
1741
1742 if !(*output).adjust_tree.is_null() {
1743 ffi::wlr_scene_node_destroy((*output).adjust_tree as *mut ffi::wlr_scene_node);
1744 (*output).adjust_tree = std::ptr::null_mut();
1745 }
1746 (*output).adjust_rects.clear();
1747
1748 if !(*output).wlr_output.is_null() {
1749 ffi::river_wlr_output_set_data((*output).wlr_output, std::ptr::null_mut());
1750 }
1751
1752 (*output).wlr_output = std::ptr::null_mut();
1753 (*output).scheduled.mode = OutputMode::None;
1754 (*output).sent.mode = OutputMode::None;
1755 (*output).current.mode = OutputMode::None;
1756 (*output).scheduled.state = OutputStateValue::Destroying;
1757
1758 (*(*output).server).wm.dirty_windowing();
1759 }
1760
1761 unsafe extern "C" fn handle_request_state(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
1762 let output = &mut *crate::container_of!(listener, Output, request_state);
1763 let event = data as *mut ffi::wlr_output_event_request_state;
1764
1765 let committed: u32 = std::mem::transmute((*(*event).state).committed);
1766 // mode field is bit 1 (mode)
1767 if committed & 1 != 0 {
1768 if !(*(*event).state).mode.is_null() {
1769 output.scheduled.mode = OutputMode::Standard((*(*event).state).mode);
1770 } else {
1771 output.scheduled.mode = OutputMode::Custom {
1772 width: (*(*event).state).custom_mode.width,
1773 height: (*(*event).state).custom_mode.height,
1774 refresh: (*(*event).state).custom_mode.refresh,
1775 };
1776 }
1777 }
1778
1779 (*output.server).wm.dirty_windowing();
1780 }
1781
1782 /// `CCE_FRAME_DEBUG` (any value) also ticks every output frame, so a client's
1783 /// frame-callback interval can be compared against the rate the output is
1784 /// actually rendering at — the two diverging is the signature of a surface
1785 /// being skipped by the scene's visible gate in `wlr_scene_buffer_send_frame_done`.
1786 pub(crate) fn frame_debug() -> bool {
1787 static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1788 *FLAG.get_or_init(|| std::env::var_os("CCE_FRAME_DEBUG").is_some())
1789 }
1790
1791 /// `CCE_OVDBG=1` arms the `/tmp/cce-ovdbg` per-frame scene dump (see
1792 /// `render_and_commit`); without it the file is never even looked for.
1793 fn ovdbg_enabled() -> bool {
1794 static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1795 *FLAG.get_or_init(|| std::env::var_os("CCE_OVDBG").is_some())
1796 }
1797
1798 /// Ceiling on how long a status segment's backdrop reading may go unmeasured
1799 /// while frames are being rendered; matches the readback throttle in
1800 /// `window_backdrop_sample`. A still desktop renders no frames and measures
1801 /// nothing at all.
1802 const BACKDROP_REFRESH: std::time::Duration = std::time::Duration::from_millis(250);
1803
1804 impl Output {
1805 /// The refresh period to plan frames by: what the last `present`
1806 /// reported, else the current mode's rate, else 60 Hz.
1807 unsafe fn refresh_period_ns(&self) -> u64 {
1808 if self.present_refresh_ns > 0 {
1809 return self.present_refresh_ns;
1810 }
1811 let mhz = if self.wlr_output.is_null() { 0 } else { ffi::river_wlr_output_get_refresh(self.wlr_output) };
1812 if mhz > 0 {
1813 1_000_000_000_000 / mhz as u64
1814 } else {
1815 16_666_667
1816 }
1817 }
1818
1819 /// When the frame rendered now is expected to reach the screen: the
1820 /// first point of a vblank grid after now (plus a small render lead).
1821 /// The camera animates to this instant, so its step is an exact whole
1822 /// number of refresh periods whether the frame callback ran early or
1823 /// late, a missed vblank is a double step rather than a stumble, and a
1824 /// second frame inside one period gets the same target (a zero step).
1825 ///
1826 /// The grid's phase locks to the hardware presentation timestamps and
1827 /// re-anchors only when they drift by more than a quarter period, so
1828 /// the per-present jitter of the timestamps themselves (and the
1829 /// headless backend's commit-time stamps) never reaches the camera.
1830 pub unsafe fn predicted_present_ns(&mut self) -> u64 {
1831 let now = util::timestamp_ns();
1832 let period = self.refresh_period_ns().max(1);
1833 if self.present_when_ns != 0 {
1834 let phase = self.present_when_ns % period;
1835 let drift = if self.frame_phase_ns == u64::MAX {
1836 u64::MAX
1837 } else {
1838 let d = phase.abs_diff(self.frame_phase_ns);
1839 d.min(period - d)
1840 };
1841 if drift > period / 4 {
1842 self.frame_phase_ns = phase;
1843 }
1844 } else if self.frame_phase_ns == u64::MAX {
1845 self.frame_phase_ns = now % period;
1846 }
1847 let lead = period / 8;
1848 let base = (now + lead).saturating_sub(self.frame_phase_ns);
1849 self.frame_phase_ns + (base / period + 1) * period
1850 }
1851 }
1852
1853 unsafe extern "C" fn handle_frame(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1854 let output = &mut *crate::container_of!(listener, Output, frame);
1855 // The camera steps here, on the vblank, to where it should be at the
1856 // instant THIS frame is presented (see WindowManager::step_camera_frame).
1857 let frame_target_ns = output.predicted_present_ns();
1858 (*output.server).wm.step_camera_frame(frame_target_ns);
1859 // Likewise the interactive move/resize: one configure + relayout per
1860 // vblank, for the pointer's latest position.
1861 (*output.server).wm.step_op_frame();
1862 let render_start = if frame_debug() {
1863 Some(std::time::Instant::now())
1864 } else {
1865 None
1866 };
1867 if let Err(e) = output.render_and_commit() {
1868 log::error!("{}", e);
1869 }
1870 if let Some(start) = render_start {
1871 // Epoch ms mod 100000 — the shared tracer time base (see cce-ui's
1872 // CCE_PRESENT_DEBUG), so compositor and client logs interleave.
1873 let t = std::time::SystemTime::now()
1874 .duration_since(std::time::UNIX_EPOCH)
1875 .unwrap()
1876 .as_millis()
1877 % 100000;
1878 log::info!(
1879 "[cce-frame] t={} output frame (render_and_commit {}us)",
1880 t,
1881 start.elapsed().as_micros()
1882 );
1883 }
1884 let now = util::timestamp();
1885 let mut ffi_now = ffi::timespec {
1886 tv_sec: now.tv_sec,
1887 tv_nsec: now.tv_nsec,
1888 };
1889 ffi::wlr_scene_output_send_frame_done(output.scene_output, &mut ffi_now);
1890 // The scene's frame-done pass is gated on a node being visible, and the
1891 // desktop grid spends most of its life behind opaque windows — so the
1892 // one client that MUST repaint on demand is the one whose callbacks dry
1893 // up. cce-ui's runner waits on a frame callback before it renders, and
1894 // only a 250ms starvation fallback unblocks it: every patch took a
1895 // quarter second to come back, which is longer than the overview ramp
1896 // and is why an exit's replacement patch used to land after the
1897 // animation. While a patch is in the air, drive the client directly.
1898 (*output.server).wm.send_frame_done_to_grid_clients_awaiting_patch();
1899 }
1900
1901 unsafe extern "C" fn handle_present(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
1902 let output = &mut *crate::container_of!(listener, Output, present);
1903 let event = data as *mut ffi::wlr_output_event_present;
1904 if !(*event).presented {
1905 return;
1906 }
1907 // Presentation clock bookkeeping, and the dropped-frame count: a vblank
1908 // sequence that advanced by more than one since the last present means
1909 // frames were skipped. Backends without a counter (headless) report
1910 // seq 0; there the gap is inferred from time, but only while the camera
1911 // is animating — a still desktop legitimately presents nothing for ages.
1912 {
1913 let when_ns = (*event).when.tv_sec as u64 * 1_000_000_000 + (*event).when.tv_nsec as u64;
1914 if (*event).refresh > 0 {
1915 output.present_refresh_ns = (*event).refresh as u64;
1916 }
1917 let period = output.refresh_period_ns();
1918 let seq = (*event).seq as u32;
1919 let mut dropped = 0u64;
1920 if seq != 0 && output.present_seq != 0 && seq > output.present_seq + 1 {
1921 dropped = (seq - output.present_seq - 1) as u64;
1922 } else if seq == 0 && output.present_when_ns != 0 && (*output.server).wm.camera_anim_active {
1923 let gap = when_ns.saturating_sub(output.present_when_ns);
1924 let periods = (gap + period / 2) / period;
1925 dropped = periods.saturating_sub(1);
1926 }
1927 if dropped > 0 {
1928 output.present_dropped += dropped;
1929 if frame_debug() {
1930 log::info!(
1931 "[cce-frame] present seq={} dropped={} (total {}) refresh={}us",
1932 seq,
1933 dropped,
1934 output.present_dropped,
1935 period / 1000
1936 );
1937 }
1938 }
1939 output.present_when_ns = when_ns;
1940 output.present_seq = seq;
1941 }
1942 match output.lock_render_state {
1943 LockRenderState::PendingUnlock => {
1944 output.lock_render_state = LockRenderState::Unlocked;
1945 }
1946 LockRenderState::PendingBlank => {
1947 output.lock_render_state = LockRenderState::Blanked;
1948 (*output.server).lock_manager.maybe_lock();
1949 }
1950 LockRenderState::PendingLockSurface => {
1951 output.lock_render_state = LockRenderState::LockSurface;
1952 (*output.server).lock_manager.maybe_lock();
1953 }
1954 _ => {}
1955 }
1956 }
1957
1958 // Helpers for raw Wayland FFI protocol events
1959 pub unsafe fn zcce_output_send_removed(resource: *mut ffi::wl_resource) {
1960 ffi::wl_resource_post_event(resource, 0);
1961 }
1962
1963 pub unsafe fn zcce_output_send_wl_output(resource: *mut ffi::wl_resource, name: u32) {
1964 ffi::wl_resource_post_event(resource, 1, name);
1965 }
1966
1967 pub unsafe fn zcce_output_send_position(resource: *mut ffi::wl_resource, x: i32, y: i32) {
1968 ffi::wl_resource_post_event(resource, 2, x, y);
1969 }
1970
1971 pub unsafe fn zcce_output_send_dimensions(resource: *mut ffi::wl_resource, width: i32, height: i32) {
1972 ffi::wl_resource_post_event(resource, 3, width, height);
1973 }