Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
src/server/output.rs (68.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 pub struct Output {
140 pub server: *mut Server,
141 pub wlr_output: *mut ffi::wlr_output,
142 pub scene_output: *mut ffi::wlr_scene_output,
143 pub background_rect: *mut ffi::wlr_scene_rect,
144 pub grid_tree: *mut ffi::wlr_scene_tree,
145 /// The grid's backdrop (gap colour, or the Solid spec's colour), kept in
146 /// its own tree under `scene.layers.background_clients` so a client
147 /// background surface paints over it while the cells in `grid_tree` stay
148 /// on top. Positioned in lockstep with `grid_tree`.
149 pub grid_backdrop_tree: *mut ffi::wlr_scene_tree,
150 pub grid_backdrop_rect: *mut ffi::wlr_scene_rect,
151 pub adjust_tree: *mut ffi::wlr_scene_tree,
152 pub adjust_rects: Vec<*mut ffi::wlr_scene_rect>,
153 pub last_adjust_mode: bool,
154 pub object: *mut ffi::wl_resource, // zcce_output_v1 resource
155 pub layer_shell: LayerShellOutput,
156 pub lock_render_state: LockRenderState,
157 pub link: ffi::wl_list,
158 pub link_sent: ffi::wl_list,
159 pub scheduled: OutputState,
160 pub sent: OutputState,
161 pub current: OutputState,
162 pub sent_wl_output: bool,
163 pub rendering_requested: RenderingState,
164 pub rendering_current: RenderingState,
165
166 // Cached grid parameters to avoid redrawing when unchanged
167 /// Camera state at this output's last rendered frame; any difference
168 /// forces a full-output repaint (see the frame chokepoint).
169 pub last_rendered_pan_x: f64,
170 pub last_rendered_pan_y: f64,
171 pub last_rendered_zoom: f64,
172 /// Presentation clock, from the `present` event: when the last frame
173 /// turned into light (CLOCK_MONOTONIC ns, 0 = never), its vblank
174 /// sequence number (0 = the backend has none), and the refresh period.
175 /// `predicted_present_ns` derives the camera's frame clock from these.
176 pub present_when_ns: u64,
177 pub present_seq: u32,
178 pub present_refresh_ns: u64,
179 /// Running count of vblanks skipped between consecutive presents —
180 /// the dropped-frame counter `CCE_FRAME_DEBUG` reports.
181 pub present_dropped: u64,
182 /// Phase of the frame clock's vblank grid (ns within a refresh period),
183 /// locked to the presentation times; `u64::MAX` until the first frame.
184 pub frame_phase_ns: u64,
185 /// A tearing page-flip test failed for the current tearing episode; do
186 /// not repeat the atomic TEST_ONLY commit every frame. Cleared when the
187 /// fullscreen client's tearing request goes away.
188 pub tearing_test_failed: bool,
189 /// Darkened by the idle timeout (`IdleManager::set_displays`), so the
190 /// next activity wakes this one and leaves a client-darkened output alone.
191 pub idle_off: bool,
192 pub last_grid_viewport_w: i32,
193 pub last_grid_viewport_h: i32,
194 pub last_grid_zoom: f64,
195 /// The spec the pool was last drawn from; a change (or viewport/zoom
196 /// change) forces a redraw. Pan alone never redraws — it only moves the
197 /// grid tree.
198 pub last_grid_spec: Option<crate::policy::api::BackgroundSpec>,
199 pub grid_rect_pool: Vec<*mut ffi::wlr_scene_rect>,
200 /// Lit-chamfer rims over the grid cells — the same scenefx bevel node
201 /// the windows use, so the grid lines read as raised rails descending
202 /// into each cell through a shaded fillet that wraps the corner arcs.
203 /// Pooled like `grid_rect_pool`, but in their own subtree kept above
204 /// the rects: pool reuse must never stack a rim beneath a
205 /// later-created cell rect.
206 pub grid_bevel_pool: Vec<*mut ffi::wlr_scene_bevel>,
207 pub grid_bevel_tree: *mut ffi::wlr_scene_tree,
208 /// The cell labels' own subtree inside `grid_tree`, raised above the
209 /// cell rects and bevels on every draw: label nodes used to be direct
210 /// children of the grid tree, so any cell rect the pool created after
211 /// them stacked on top and hid them.
212 pub cell_label_tree: *mut ffi::wlr_scene_tree,
213 /// Bevel params the rims were last drawn with (enabled, thickness,
214 /// light x/y/intensity, shade, shoulder as bits) — the spec alone does
215 /// not cover them, and a live config reload must redraw the rims too.
216 pub last_grid_bevel: Option<[u32; 7]>,
217 pub grid_force_redraw_frames: u8,
218 /// Scene nodes for the per-square chess-style labels (overview only), and
219 /// the rasterized glyph buffers behind them. Pooled exactly like
220 /// `grid_rect_pool`: reused across frames, disabled past the live count.
221 /// Each entry remembers the buffer it currently shows, because
222 /// `wlr_scene_buffer_set_buffer` damages the node unconditionally — even
223 /// when handed the buffer already on it — and these are re-walked every
224 /// frame while the overview camera moves.
225 pub cell_label_pool: Vec<(*mut ffi::wlr_scene_buffer, *mut ffi::wlr_buffer)>,
226 pub cell_labels: crate::text::LabelCache,
227 /// Label point size actually in use, so a zoom change can re-rasterize.
228 pub last_label_px: u32,
229
230 pub destroy: ffi::wl_listener,
231 pub request_state: ffi::wl_listener,
232 pub frame: ffi::wl_listener,
233 pub present: ffi::wl_listener,
234 }
235
236 unsafe extern "C" fn handle_destroy_resource(resource: *mut ffi::wl_resource) {
237 let output = ffi::wl_resource_get_user_data(resource) as *mut Output;
238 if !output.is_null() {
239 if (*output).object != resource {
240 return;
241 }
242 (*output).object = std::ptr::null_mut();
243 (*output).sent_wl_output = false;
244 }
245 }
246
247 unsafe extern "C" fn output_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
248 ffi::wl_resource_destroy(resource);
249 }
250
251 unsafe extern "C" fn output_set_presentation_mode(
252 _client: *mut ffi::wl_client,
253 resource: *mut ffi::wl_resource,
254 mode: u32,
255 ) {
256 let output = ffi::wl_resource_get_user_data(resource) as *mut Output;
257 if output.is_null() {
258 return;
259 }
260 if !(*(*output).server).wm.ensure_rendering() {
261 return;
262 }
263 match mode {
264 ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_VSYNC => {
265 (*output).rendering_requested.tearing = false;
266 }
267 ffi::zcce_output_v1_presentation_mode_ZCCE_OUTPUT_V1_PRESENTATION_MODE_ASYNC => {
268 (*output).rendering_requested.tearing = true;
269 }
270 _ => {
271 ffi::wl_resource_post_error(
272 resource,
273 ffi::zcce_output_v1_error_ZCCE_OUTPUT_V1_ERROR_INVALID_PRESENTATION_MODE,
274 b"invalid presentation mode enum value\0".as_ptr() as *const _,
275 );
276 }
277 }
278 }
279
280 static OUTPUT_INTERFACE: ffi::zcce_output_v1_interface = ffi::zcce_output_v1_interface {
281 destroy: Some(output_destroy),
282 set_presentation_mode: Some(output_set_presentation_mode),
283 };
284
285 static INERT_OUTPUT_INTERFACE: ffi::zcce_output_v1_interface = ffi::zcce_output_v1_interface {
286 destroy: Some(output_destroy),
287 set_presentation_mode: None,
288 };
289
290 impl Output {
291 pub unsafe fn make_inert(&mut self) {
292 if !self.object.is_null() {
293 ffi::wl_resource_post_event(self.object, 0); // zcce_output.removed
294 ffi::wl_resource_set_implementation(
295 self.object,
296 &INERT_OUTPUT_INTERFACE as *const _ as *const _,
297 std::ptr::null_mut(),
298 None,
299 );
300 self.layer_shell.make_inert();
301 self.object = std::ptr::null_mut();
302 self.sent_wl_output = false;
303 self.grid_rect_pool.clear();
304 self.grid_bevel_pool.clear();
305 self.grid_bevel_tree = std::ptr::null_mut();
306 self.cell_label_pool.clear();
307 self.cell_label_tree = std::ptr::null_mut();
308 }
309 }
310
311 pub unsafe fn manage_start(&mut self) {
312 match self.scheduled.state {
313 OutputStateValue::Enabled | OutputStateValue::DisabledSoft => {
314 assert!(!self.scheduled.mode_none());
315 let wlr_output = self.wlr_output;
316
317 let self_ptr = self as *mut Output;
318 let layer_shell_ptr = &mut self.layer_shell as *mut LayerShellOutput;
319 (*layer_shell_ptr).manage_start(self_ptr);
320
321 let wm_v1 = (*self.server).wm.object;
322 if !wm_v1.is_null() {
323 let new = self.object.is_null();
324 let output_v1 = if new {
325 let client = ffi::wl_resource_get_client(wm_v1);
326 let res = ffi::wl_resource_create(
327 client,
328 &ffi::zcce_output_v1_interface,
329 ffi::wl_resource_get_version(wm_v1),
330 0,
331 );
332 if res.is_null() {
333 log::error!("out of memory");
334 return;
335 }
336 self.object = res;
337 ffi::wl_resource_set_implementation(
338 res,
339 &OUTPUT_INTERFACE as *const _ as *const _,
340 self as *mut Output as *mut _,
341 Some(handle_destroy_resource),
342 );
343 ffi::wl_resource_post_event(wm_v1, ffi::ZCCE_WINDOW_MANAGER_V1_OUTPUT, res); // zcce_window_manager_v1.output
344 res
345 } else {
346 self.object
347 };
348
349 if !self.sent_wl_output {
350 let global = ffi::river_wlr_output_get_global(wlr_output);
351 if !global.is_null() {
352 let client = ffi::wl_resource_get_client(output_v1);
353 let wl_output_name = ffi::wl_global_get_name(global, client);
354 zcce_output_send_wl_output(output_v1, wl_output_name);
355 self.sent_wl_output = true;
356 }
357 }
358
359 let (scheduled_width, scheduled_height) = self.scheduled.dimensions();
360 let (sent_width, sent_height) = self.sent.dimensions();
361
362 if new || scheduled_width != sent_width || scheduled_height != sent_height {
363 zcce_output_send_dimensions(output_v1, scheduled_width, scheduled_height);
364 }
365 if new || self.scheduled.x != self.sent.x || self.scheduled.y != self.sent.y {
366 zcce_output_send_position(output_v1, self.scheduled.x, self.scheduled.y);
367 }
368 }
369
370 self.sent = self.scheduled;
371
372 wl_list_remove(&mut self.link_sent as *mut ffi::wl_list as *mut WlList);
373 let sent_outputs = &mut (*self.server).wm.sent.outputs as *mut ffi::wl_list as *mut WlList;
374 wl_list_insert((*sent_outputs).prev, &mut self.link_sent as *mut ffi::wl_list as *mut WlList);
375 }
376 OutputStateValue::DisabledHard | OutputStateValue::Destroying => {
377 self.make_inert();
378
379 self.sent = self.scheduled;
380
381 if self.scheduled.state == OutputStateValue::Destroying {
382 assert!(self.wlr_output.is_null());
383
384 if !self.background_rect.is_null() {
385 ffi::wlr_scene_node_destroy(self.background_rect as *mut ffi::wlr_scene_node);
386 self.background_rect = std::ptr::null_mut();
387 }
388
389 if !self.grid_tree.is_null() {
390 ffi::wlr_scene_node_destroy(self.grid_tree as *mut ffi::wlr_scene_node);
391 self.grid_tree = std::ptr::null_mut();
392 }
393 if !self.grid_backdrop_tree.is_null() {
394 ffi::wlr_scene_node_destroy(self.grid_backdrop_tree as *mut ffi::wlr_scene_node);
395 self.grid_backdrop_tree = std::ptr::null_mut();
396 self.grid_backdrop_rect = std::ptr::null_mut();
397 }
398 self.grid_rect_pool.clear();
399 // The bevel and label subtrees died with grid_tree above.
400 self.grid_bevel_pool.clear();
401 self.grid_bevel_tree = std::ptr::null_mut();
402 self.cell_label_pool.clear();
403 self.cell_label_tree = std::ptr::null_mut();
404
405 if !self.adjust_tree.is_null() {
406 ffi::wlr_scene_node_destroy(self.adjust_tree as *mut ffi::wlr_scene_node);
407 self.adjust_tree = std::ptr::null_mut();
408 }
409 self.adjust_rects.clear();
410
411 // remove output from windows fullscreen hint
412 for &window in (*self.server).wm.windows.iter() {
413 if let crate::window::FullscreenRequest::Fullscreen(out) = (*window).wm_scheduled.fullscreen_requested {
414 if out == self as *mut Output {
415 (*window).wm_scheduled.fullscreen_requested = crate::window::FullscreenRequest::Fullscreen(std::ptr::null_mut());
416 }
417 }
418 if (*window).wm_requested.fullscreen == self as *mut Output {
419 (*window).wm_requested.fullscreen = std::ptr::null_mut();
420 }
421 }
422
423 wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut WlList);
424 wl_list_remove(&mut self.link_sent as *mut ffi::wl_list as *mut WlList);
425
426 let _ = Box::from_raw(self as *mut Output);
427 }
428 }
429 }
430 }
431
432 pub unsafe fn create(server: *mut Server, wlr_output: *mut ffi::wlr_output) -> Result<(), &'static str> {
433 let title = format!("river - {}\0", std::ffi::CStr::from_ptr(ffi::river_wlr_output_get_name(wlr_output)).to_string_lossy());
434
435 // Check if output is Wayland/X11 and set application title/app_id
436 if ffi::wlr_output_is_wl(wlr_output) {
437 ffi::wlr_wl_output_set_app_id(wlr_output, "river\0".as_ptr() as *const _);
438 ffi::wlr_wl_output_set_title(wlr_output, title.as_ptr() as *const _);
439 } else if ffi::wlr_output_is_x11(wlr_output) {
440 ffi::wlr_x11_output_set_title(wlr_output, title.as_ptr() as *const _);
441 }
442
443 if !ffi::wlr_output_init_render(wlr_output, (*server).allocator, (*server).renderer) {
444 return Err("Failed to initialize renderer for output");
445 }
446
447 let scene_output = ffi::wlr_scene_output_create((*server).scene.wlr_scene, wlr_output);
448 if scene_output.is_null() {
449 return Err("Failed to create wlr_scene_output");
450 }
451
452 let name_raw = ffi::river_wlr_output_get_name(wlr_output);
453 let name = std::ffi::CStr::from_ptr(name_raw).to_string_lossy();
454 let scale_key = format!("scale_{}", name);
455 let output_scale = (*server).wm.display.get(&scale_key)
456 .map(|&s| s as f32)
457 .unwrap_or((*server).wm.output_scale);
458
459 // The physical size every client's wl_output geometry will carry,
460 // which cce-ui's `units::Metric` measures logical px per mm against.
461 // A configured `size_mm` replaces the EDID figure before the global
462 // exists (wlr_output_layout_add creates it), so no client ever sees
463 // the lie. Logged either way: an output with no size at all leaves
464 // clients on the assumed 96 ppi, and that is worth knowing.
465 let (mut edid_w, mut edid_h) = (0i32, 0i32);
466 ffi::river_wlr_output_get_phys_size(wlr_output, &mut edid_w, &mut edid_h);
467 let configured = (*server).wm.display.get(&format!("mm_w_{}", name))
468 .zip((*server).wm.display.get(&format!("mm_h_{}", name)))
469 .map(|(&w, &h)| (w.round() as i32, h.round() as i32));
470 match configured {
471 Some((w, h)) => {
472 ffi::river_wlr_output_set_phys_size(wlr_output, w, h);
473 log::info!("output {}: physical size {}x{} mm (configured size_mm; EDID said {}x{})", name, w, h, edid_w, edid_h);
474 }
475 None if edid_w > 0 && edid_h > 0 => {
476 log::info!("output {}: physical size {}x{} mm (EDID)", name, edid_w, edid_h);
477 }
478 None => {
479 log::info!("output {}: no physical size — clients assume 96 ppi; set `output {{ {} size_mm=\"WxH\" }}` to measure", name, name);
480 }
481 }
482
483 let initial = OutputState {
484 state: OutputStateValue::DisabledHard,
485 x: 0,
486 y: 0,
487 mode: OutputMode::None,
488 scale: output_scale,
489 transform: ffi::wl_output_transform_WL_OUTPUT_TRANSFORM_NORMAL,
490 adaptive_sync: ffi::river_wlr_output_get_adaptive_sync_status(wlr_output) == ffi::wlr_output_adaptive_sync_status_WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED,
491 auto_layout: true,
492 };
493
494 let output = Box::new(Output {
495 server,
496 wlr_output,
497 scene_output,
498 background_rect: std::ptr::null_mut(),
499 grid_tree: std::ptr::null_mut(),
500 grid_backdrop_tree: std::ptr::null_mut(),
501 grid_backdrop_rect: std::ptr::null_mut(),
502 adjust_tree: std::ptr::null_mut(),
503 adjust_rects: Vec::new(),
504 last_adjust_mode: false,
505 object: std::ptr::null_mut(),
506 layer_shell: LayerShellOutput::default(),
507 lock_render_state: LockRenderState::Blanked,
508 link: std::mem::zeroed(),
509 link_sent: std::mem::zeroed(),
510 scheduled: initial,
511 sent: initial,
512 current: initial,
513 sent_wl_output: false,
514 rendering_requested: RenderingState { tearing: false },
515 rendering_current: RenderingState { tearing: false },
516 last_rendered_pan_x: f64::NAN,
517 last_rendered_pan_y: f64::NAN,
518 last_rendered_zoom: f64::NAN,
519 present_when_ns: 0,
520 present_seq: 0,
521 present_refresh_ns: 0,
522 present_dropped: 0,
523 frame_phase_ns: u64::MAX,
524 tearing_test_failed: false,
525 idle_off: false,
526 last_grid_viewport_w: 0,
527 last_grid_viewport_h: 0,
528 last_grid_zoom: 0.0,
529 last_grid_spec: None,
530 grid_rect_pool: Vec::new(),
531 grid_bevel_pool: Vec::new(),
532 grid_bevel_tree: std::ptr::null_mut(),
533 cell_label_tree: std::ptr::null_mut(),
534 last_grid_bevel: None,
535 grid_force_redraw_frames: 0,
536 cell_label_pool: Vec::new(),
537 cell_labels: Default::default(),
538 last_label_px: 0,
539 destroy: std::mem::zeroed(),
540 request_state: std::mem::zeroed(),
541 frame: std::mem::zeroed(),
542 present: std::mem::zeroed(),
543 });
544
545 let raw = Box::into_raw(output);
546 ffi::river_wlr_output_set_data(wlr_output, raw as *mut std::ffi::c_void);
547
548 let list_head = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
549 let link_custom = &mut (*raw).link as *mut ffi::wl_list as *mut WlList;
550 wl_list_insert((*list_head).prev, link_custom);
551
552 ffi::wl_list_init(&mut (*raw).link_sent);
553
554 // Add event listeners
555 let d_listener = &mut (*raw).destroy as *mut ffi::wl_listener as *mut WlListener;
556 (*d_listener).notify = Some(handle_destroy);
557 wl_signal_add(ffi::river_wlr_output_get_destroy_signal(wlr_output), &mut (*raw).destroy);
558
559 let req_listener = &mut (*raw).request_state as *mut ffi::wl_listener as *mut WlListener;
560 (*req_listener).notify = Some(handle_request_state);
561 wl_signal_add(ffi::river_wlr_output_get_request_state_signal(wlr_output), &mut (*raw).request_state);
562
563 let frame_listener = &mut (*raw).frame as *mut ffi::wl_listener as *mut WlListener;
564 (*frame_listener).notify = Some(handle_frame);
565 wl_signal_add(ffi::river_wlr_output_get_frame_signal(wlr_output), &mut (*raw).frame);
566
567 let pres_listener = &mut (*raw).present as *mut ffi::wl_listener as *mut WlListener;
568 (*pres_listener).notify = Some(handle_present);
569 wl_signal_add(ffi::river_wlr_output_get_present_signal(wlr_output), &mut (*raw).present);
570
571 (*raw).scheduled.state = OutputStateValue::Enabled;
572 let preferred = ffi::wlr_output_preferred_mode(wlr_output);
573 if !preferred.is_null() {
574 (*raw).scheduled.mode = OutputMode::Standard(preferred);
575 } else {
576 (*raw).scheduled.mode = OutputMode::Custom { width: 1280, height: 720, refresh: 0 };
577 }
578
579 (*server).wm.dirty_windowing();
580 Ok(())
581 }
582
583 pub unsafe fn render_and_commit(&mut self) -> Result<(), &'static str> {
584 // Update grid node positions and parameters first, which marks the scene output as damaged if changed
585 self.draw_grid();
586 self.draw_adjust_overlay();
587 (*self.server).wm.draw_selection();
588 // A parked `ccectl screenshot` targeting this output forces a render
589 // even without damage so there is a fresh buffer to read back.
590 let pending_shot = {
591 let wm = &mut (*self.server).wm;
592 if wm
593 .pending_screenshot
594 .as_ref()
595 .map(|s| s.output == self as *mut Output)
596 .unwrap_or(false)
597 {
598 wm.pending_screenshot.take()
599 } else {
600 None
601 }
602 };
603
604 // One chokepoint for every camera-mutation path (wheel zoom, IPC,
605 // keyed actions, edge-pan, the pan animation — whichever of
606 // update_viewport_local or the manage transaction carried it): if
607 // the camera changed since this output last rendered, per-node
608 // damage under-reports the whole-screen relayout (stale slivers of
609 // the previous zoom survive wherever idle content used to be), so
610 // force a full repaint. Must run BEFORE the needs-frame early-out —
611 // a camera change with no other pending damage would otherwise skip
612 // the frame entirely.
613 {
614 // Quantized to screen pixels: a sub-pixel pan moves no node
615 // (see `update_viewport_local`), so it is not a reason to paint.
616 let wm = &(*self.server).wm;
617 let q = wm.desk_zoom * self.current.scale as f64;
618 let cam = ((wm.desk_pan_x * q).round(), (wm.desk_pan_y * q).round(), wm.desk_zoom);
619 if cam != (self.last_rendered_pan_x, self.last_rendered_pan_y, self.last_rendered_zoom) {
620 self.last_rendered_pan_x = cam.0;
621 self.last_rendered_pan_y = cam.1;
622 self.last_rendered_zoom = cam.2;
623 if !self.scene_output.is_null() {
624 ffi::river_scene_output_damage_whole(self.scene_output);
625 }
626 }
627 }
628
629 if pending_shot.is_none() && !ffi::wlr_scene_output_needs_frame(self.scene_output) {
630 return Ok(());
631 }
632
633 // Re-apply scale to all windows whose scale is not 1.0 right before
634 // rendering — and to any whose buffers are still shrunk from a
635 // scale that has just returned to 1.0, which this pass resets.
636 let wm = &(*self.server).wm;
637 for &window in wm.windows.iter() {
638 if !window.is_null() && ((*window).scale != 1.0 || (*window).x11_buffer_scale() != 1.0 || (*window).buffers_scaled) {
639 (*window).scale_only_render_finish();
640 }
641 }
642 for &or in wm.override_redirects.iter() {
643 if !or.is_null() {
644 (*or).apply_x11_scale();
645 }
646 }
647
648 // Overview-delay debugging: with `CCE_OVDBG=1` in the compositor's
649 // environment, while /tmp/cce-ovdbg exists (contents = comma-separated
650 // app_id substrings), dump the scene-side truth for matching windows
651 // every rendered frame. Toggle live with
652 // `echo firefox,cce-calendar > /tmp/cce-ovdbg`; `rm` to stop. The env
653 // gate is what keeps a release build from doing an open()+read() of
654 // that path on every frame it ever renders.
655 if ovdbg_enabled() {
656 if let Ok(filter) = std::fs::read_to_string("/tmp/cce-ovdbg") {
657 let pats: Vec<&str> = filter.trim().split(',').filter(|p| !p.is_empty()).collect();
658 for &window in wm.windows.iter() {
659 if window.is_null() || (*window).closed {
660 continue;
661 }
662 let app = (*window).get_app_id_string().unwrap_or_default();
663 if !pats.iter().any(|p| app.contains(p)) {
664 continue;
665 }
666 let now = std::time::SystemTime::now()
667 .duration_since(std::time::UNIX_EPOCH)
668 .unwrap_or_default();
669 eprintln!(
670 "[ovdbg] t={}.{:03} win {} scale={} req=({},{}) box=({},{}) state={:?} hidden={} saved={} tree_en={}",
671 now.as_secs(),
672 now.subsec_millis(),
673 app,
674 (*window).scale,
675 (*window).rendering_requested.x,
676 (*window).rendering_requested.y,
677 (*window).box_geom.x,
678 (*window).box_geom.y,
679 (*window).state,
680 (*window).rendering_requested.hidden,
681 (*window).surfaces.saved,
682 ffi::river_scene_node_get_enabled((*window).tree as *mut ffi::wlr_scene_node),
683 );
684 if let Ok(tag) = std::ffi::CString::new(app) {
685 ffi::river_scene_shadow_dbg((*window).shadow, tag.as_ptr());
686 ffi::river_scene_ovdbg_dump(
687 (*window).tree as *mut ffi::wlr_scene_node,
688 tag.as_ptr(),
689 );
690 }
691 }
692 }
693 }
694
695 let mut state = std::mem::zeroed();
696 ffi::wlr_output_state_init(&mut state);
697
698 self.current.apply_no_modeset(&mut state);
699
700 if !ffi::wlr_scene_output_build_state(self.scene_output, &mut state, std::ptr::null()) {
701 ffi::wlr_output_state_finish(&mut state);
702 return Err("Failed to build scene state");
703 }
704
705 if self.rendering_current.tearing {
706 // The test is an atomic TEST_ONLY commit; once it has said no for
707 // this episode, asking again every frame just taxes the game.
708 if !self.tearing_test_failed {
709 state.tearing_page_flip = true;
710 if !ffi::wlr_output_test_state(self.wlr_output, &state) {
711 state.tearing_page_flip = false;
712 self.tearing_test_failed = true;
713 }
714 }
715 } else {
716 self.tearing_test_failed = false;
717 }
718
719 if !ffi::wlr_output_commit_state(self.wlr_output, &state) {
720 ffi::wlr_output_state_finish(&mut state);
721 // The damage this frame carried is still pending (only a
722 // successful commit clears it), but nothing else asks for
723 // another frame: a refused commit (the panel's EBUSY bursts)
724 // left the screen stale until something unrelated moved. Retry
725 // at the next vblank.
726 ffi::wlr_output_schedule_frame(self.wlr_output);
727 return Err("Failed to commit state");
728 }
729
730 // Read the just-committed frame back while the state's buffer is
731 // still alive; encode/notify happen on a worker thread.
732 if let Some(mut shot) = pending_shot {
733 if state.buffer.is_null() {
734 log::warn!("screenshot: output state has no buffer");
735 shot.reply_err("screenshot: output state has no buffer");
736 } else {
737 crate::screenshot::capture_state_buffer(
738 (*self.server).renderer,
739 state.buffer,
740 ffi::river_wlr_output_get_width(self.wlr_output),
741 ffi::river_wlr_output_get_height(self.wlr_output),
742 shot,
743 );
744 }
745 }
746
747 ffi::wlr_output_state_finish(&mut state);
748
749 match (*self.server).lock_manager.state {
750 LockState::Unlocked => {
751 if self.lock_render_state != LockRenderState::Unlocked {
752 self.lock_render_state = LockRenderState::PendingUnlock;
753 }
754 }
755 LockState::Locked => {
756 // Assert normal tree disabled, lock surface rendered
757 }
758 LockState::WaitingForBlank => {
759 if self.lock_render_state != LockRenderState::Blanked {
760 self.lock_render_state = LockRenderState::PendingBlank;
761 }
762 }
763 LockState::WaitingForLockSurfaces => {
764 if let Some(_lock_surf) = (*self.server).lock_manager.lock_surface_from_output(self) {
765 if self.lock_render_state != LockRenderState::LockSurface {
766 self.lock_render_state = LockRenderState::PendingLockSurface;
767 }
768 } else {
769 if self.lock_render_state != LockRenderState::Unlocked {
770 self.lock_render_state = LockRenderState::PendingUnlock;
771 }
772 }
773 }
774 }
775
776 Ok(())
777 }
778
779 pub unsafe fn update_background_color(&mut self) {
780 if !self.background_rect.is_null() {
781 let wm = &(*self.server).wm;
782 let color: [f32; 4] = [
783 (wm.layout.background_r as f64 / u32::MAX as f64) as f32,
784 (wm.layout.background_g as f64 / u32::MAX as f64) as f32,
785 (wm.layout.background_b as f64 / u32::MAX as f64) as f32,
786 (wm.layout.background_a as f64 / u32::MAX as f64) as f32,
787 ];
788 ffi::wlr_scene_rect_set_color(self.background_rect, color.as_ptr());
789 }
790 }
791
792 pub unsafe fn draw_adjust_overlay(&mut self) {
793 if self.adjust_tree.is_null() {
794 return;
795 }
796
797 let wm = &(*self.server).wm;
798 if wm.adjust_position_mode != self.last_adjust_mode {
799 self.last_adjust_mode = wm.adjust_position_mode;
800 ffi::wlr_output_schedule_frame(self.wlr_output);
801 }
802
803 if !wm.adjust_position_mode {
804 ffi::wlr_scene_node_set_enabled(self.adjust_tree as *mut ffi::wlr_scene_node, false);
805 return;
806 }
807
808 // Enable the overlay tree.
809 ffi::wlr_scene_node_set_enabled(self.adjust_tree as *mut ffi::wlr_scene_node, true);
810 ffi::wlr_scene_node_lower_to_bottom(self.adjust_tree as *mut ffi::wlr_scene_node);
811
812 let (viewport_w, viewport_h) = self.current.dimensions();
813 let w = viewport_w;
814 let h = viewport_h;
815
816 // Semicircles size: 120 x 120 (so radius = 60).
817 let targets = [
818 // TopLeft (nw): x = 0, y = -60
819 (0, -60),
820 // TopCenter (n): x = w/2 - 60, y = -60
821 (w / 2 - 60, -60),
822 // TopRight (ne): x = w - 120, y = -60
823 (w - 120, -60),
824 // BottomLeft (sw): x = 0, y = h - 60
825 (0, h - 60),
826 // BottomCenter (s): x = w/2 - 60, y = h - 60
827 (w / 2 - 60, h - 60),
828 // BottomRight (se): x = w - 120, y = h - 60
829 (w - 120, h - 60),
830 // Left (w): x = -60, y = h/2 - 60
831 (-60, h / 2 - 60),
832 // Right (e): x = w - 60, y = h/2 - 60
833 (w - 60, h / 2 - 60),
834 ];
835
836 let color: [f32; 4] = [0.4, 0.6, 0.9, 0.5];
837 let color_ptr = color.as_ptr();
838
839 for (idx, &(tx, ty)) in targets.iter().enumerate() {
840 let rect = if idx < self.adjust_rects.len() {
841 let node = self.adjust_rects[idx];
842 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, true);
843 ffi::wlr_scene_rect_set_size(node, 120, 120);
844 ffi::wlr_scene_rect_set_color(node, color_ptr);
845 node
846 } else {
847 let node = ffi::wlr_scene_rect_create(self.adjust_tree, 120, 120, color_ptr);
848 if !node.is_null() {
849 self.adjust_rects.push(node);
850 // Make it circular!
851 ffi::river_scene_rect_set_corner_radius(node, 60);
852 }
853 node
854 };
855
856 if !rect.is_null() {
857 ffi::wlr_scene_node_set_position(rect as *mut ffi::wlr_scene_node, tx, ty);
858 }
859 }
860
861 // Disable any extra rects in the pool if we somehow have more
862 for idx in targets.len()..self.adjust_rects.len() {
863 let node = self.adjust_rects[idx];
864 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, false);
865 }
866 }
867
868 /// Draw the desktop background from the policy crate's declarative
869 /// spec: `Layout::background_spec()` says WHAT to show, and (for the
870 /// grid) `policy::background::grid_frame` derives this frame's geometry
871 /// — tree shift, backdrop extent, cell lattice, density fade. This side
872 /// keeps the scene nodes, the rect reuse pool, and scenefx's fade-inset
873 /// wire encoding.
874 pub unsafe fn draw_grid(&mut self) {
875 if self.grid_tree.is_null() {
876 return;
877 }
878
879 let wm = &(*self.server).wm;
880
881 // Enable the grid tree.
882 ffi::wlr_scene_node_set_enabled(self.grid_tree as *mut ffi::wlr_scene_node, true);
883
884 // Keep the grid tree (cells + rims) at the top of the background layer,
885 // above the client backgrounds in layers.background_clients.
886 ffi::wlr_scene_node_raise_to_top(self.grid_tree as *mut ffi::wlr_scene_node);
887
888 // The backdrop goes BELOW the client backgrounds: its own tree, placed
889 // just above this output's base rect (or at the very bottom), so a
890 // layer-shell Background surface or a wallpaper window replaces the flat
891 // colour and keeps the cell lattice.
892 if self.grid_backdrop_tree.is_null() {
893 self.grid_backdrop_tree = ffi::wlr_scene_tree_create((*self.server).scene.layers.background);
894 ffi::river_scene_tree_set_desk_offset(self.grid_backdrop_tree, true);
895 if self.grid_backdrop_tree.is_null() {
896 return;
897 }
898 if !self.background_rect.is_null() {
899 ffi::wlr_scene_node_lower_to_bottom(self.background_rect as *mut ffi::wlr_scene_node);
900 ffi::wlr_scene_node_place_above(
901 self.grid_backdrop_tree as *mut ffi::wlr_scene_node,
902 self.background_rect as *mut ffi::wlr_scene_node,
903 );
904 } else {
905 ffi::wlr_scene_node_lower_to_bottom(self.grid_backdrop_tree as *mut ffi::wlr_scene_node);
906 }
907 }
908 ffi::wlr_scene_node_set_enabled(self.grid_backdrop_tree as *mut ffi::wlr_scene_node, true);
909 let backdrop_tree = self.grid_backdrop_tree;
910 let backdrop_rect = &mut self.grid_backdrop_rect;
911 let mut set_backdrop = |w: i32, h: i32, color_ptr: *const f32| {
912 if backdrop_rect.is_null() {
913 *backdrop_rect = ffi::wlr_scene_rect_create(backdrop_tree, w, h, color_ptr);
914 } else {
915 ffi::wlr_scene_rect_set_size(*backdrop_rect, w, h);
916 ffi::wlr_scene_rect_set_color(*backdrop_rect, color_ptr);
917 }
918 };
919
920 let (viewport_w, viewport_h) = self.current.dimensions();
921 let spec = wm.layout.background_spec();
922 let zoom = crate::policy::background::sanitized_zoom(wm.desk_zoom);
923
924 // Spec/viewport/zoom/bevel changes force a redraw of the pools;
925 // pan alone only moves the grid tree.
926 let bevel_key = [
927 wm.layout.bevel_enabled as u32,
928 wm.layout.bevel_thickness.to_bits(),
929 wm.layout.bevel_light_x.to_bits(),
930 wm.layout.bevel_light_y.to_bits(),
931 wm.layout.bevel_light_intensity.to_bits(),
932 wm.layout.bevel_shade_intensity.to_bits(),
933 wm.layout.bevel_shoulder.to_bits(),
934 ];
935 let structure_changed = self.last_grid_viewport_w != viewport_w
936 || self.last_grid_viewport_h != viewport_h
937 || self.last_grid_zoom != zoom
938 || self.last_grid_spec.as_ref() != Some(&spec)
939 || self.last_grid_bevel != Some(bevel_key);
940 if structure_changed {
941 self.grid_force_redraw_frames = 3;
942 // Fallback-grid structure (spec/zoom/viewport) is backdrop
943 // content in the optimized-blur capture set — same staleness
944 // rule as the client-grid latch. Not while a camera gesture
945 // holds the bakes frozen: the zoom restructures the grid every
946 // frame, and the settle re-bakes once at the end.
947 if !wm.viewport_is_active {
948 ffi::river_scene_mark_optimized_blur_dirty((*self.server).scene.wlr_scene);
949 }
950 }
951 let force = self.grid_force_redraw_frames > 0;
952 if force {
953 self.grid_force_redraw_frames -= 1;
954 self.last_grid_viewport_w = viewport_w;
955 self.last_grid_viewport_h = viewport_h;
956 self.last_grid_zoom = zoom;
957 self.last_grid_spec = Some(spec.clone());
958 self.last_grid_bevel = Some(bevel_key);
959 }
960
961 // The cell rims live in their own subtree kept above every pooled
962 // rect (incl. the backdrop), so reuse order can never bury one.
963 if self.grid_bevel_tree.is_null() {
964 self.grid_bevel_tree = ffi::wlr_scene_tree_create(self.grid_tree);
965 }
966 ffi::wlr_scene_node_raise_to_top(self.grid_bevel_tree as *mut ffi::wlr_scene_node);
967
968 let grid_tree = self.grid_tree;
969 if !grid_tree.is_null() {
970 // Desk content: rendered with the camera's sub-pixel offset.
971 ffi::river_scene_tree_set_desk_offset(grid_tree, true);
972 }
973 let pool = &mut self.grid_rect_pool;
974 let mut pool_idx = 0;
975
976 let layout = &wm.layout;
977 // The relief lives on the LINES, never the cells (mirroring the
978 // cce-grid client, which must be able to latch without swapping the
979 // grid's material): each cell's chamfer box is expanded by the
980 // half-gap, so the lit wall occupies exactly the half-rail around
981 // the cell — a crest at the rail centerline descending to the cell
982 // edge — and neighboring rings abut without overlap. Cell floors
983 // stay flat.
984 let bevel_on = layout.bevel_enabled;
985 // Light normalized exactly like the window bevels — the grid is lit
986 // by the same lamp.
987 let (bevel_lx, bevel_ly) = {
988 let (lx, ly) = (layout.bevel_light_x, layout.bevel_light_y);
989 let len = (lx * lx + ly * ly).sqrt();
990 if len > 1e-6 { (lx / len, ly / len) } else { (-0.7071, -0.7071) }
991 };
992 let bevel_tree = self.grid_bevel_tree;
993 let bevel_pool = &mut self.grid_bevel_pool;
994 let mut bevel_idx = 0;
995
996 let mut get_bevel = |w: i32, h: i32, x: i32, y: i32, radius: i32, thickness: f32| {
997 let bevel = if bevel_idx < bevel_pool.len() {
998 let node = bevel_pool[bevel_idx];
999 ffi::wlr_scene_node_set_enabled(&mut (*node).node as *mut ffi::wlr_scene_node, true);
1000 ffi::wlr_scene_bevel_set_size(node, w, h);
1001 node
1002 } else {
1003 let node = ffi::wlr_scene_bevel_create(bevel_tree, w, h, 0, 0.0, layout.bevel_color.as_ptr());
1004 if !node.is_null() {
1005 bevel_pool.push(node);
1006 }
1007 node
1008 };
1009 if !bevel.is_null() {
1010 ffi::wlr_scene_node_set_position(&mut (*bevel).node as *mut ffi::wlr_scene_node, x, y);
1011 ffi::wlr_scene_bevel_set_corner_radius(bevel, radius);
1012 ffi::wlr_scene_bevel_set_thickness(bevel, thickness.max(1.0));
1013 ffi::wlr_scene_bevel_set_light(
1014 bevel,
1015 bevel_lx,
1016 bevel_ly,
1017 layout.bevel_light_intensity,
1018 layout.bevel_shade_intensity,
1019 );
1020 ffi::wlr_scene_bevel_set_shoulder(bevel, layout.bevel_shoulder);
1021 ffi::wlr_scene_bevel_set_color(bevel, layout.bevel_color.as_ptr());
1022 }
1023 bevel_idx += 1;
1024 };
1025
1026 // Helper closure to manage/reuse the pool of wlr_scene_rect elements.
1027 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 {
1028 let rect = if pool_idx < pool.len() {
1029 let node = pool[pool_idx];
1030 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, true);
1031 ffi::wlr_scene_rect_set_size(node, w, h);
1032 ffi::wlr_scene_rect_set_color(node, color_ptr);
1033 node
1034 } else {
1035 let node = ffi::wlr_scene_rect_create(grid_tree, w, h, color_ptr);
1036 if !node.is_null() {
1037 pool.push(node);
1038 }
1039 node
1040 };
1041
1042 if !rect.is_null() {
1043 ffi::wlr_scene_node_set_position(rect as *mut ffi::wlr_scene_node, x, y);
1044 ffi::river_scene_rect_set_corner_radius(rect, corner_r);
1045 ffi::wlr_scene_rect_set_fade_inset(rect, fade_i);
1046 }
1047 pool_idx += 1;
1048 rect
1049 };
1050
1051 // A forced rebuild resizes, recolours and moves every pooled cell
1052 // rect and rim — hundreds at overview zoom, every frame of a zoom
1053 // flight. Each setter on a live node re-walks the scene for the
1054 // region it touched; under a disabled ancestor it returns at once
1055 // (scene_node_update). So the tree is off while the pools are
1056 // redrawn and on again after: two walks for the whole rebuild.
1057 let suspended = force && !grid_tree.is_null();
1058 if suspended {
1059 ffi::wlr_scene_node_set_enabled(grid_tree as *mut ffi::wlr_scene_node, false);
1060 }
1061
1062 match &spec {
1063 crate::policy::api::BackgroundSpec::Grid(grid) => {
1064 let frame = crate::policy::background::grid_frame(
1065 grid,
1066 wm.layout_camera().0,
1067 viewport_w,
1068 viewport_h,
1069 self.sent.x,
1070 self.sent.y,
1071 );
1072
1073 if let Some((x, y)) = frame.tree_pos {
1074 ffi::river_scene_node_set_position_if_changed(
1075 grid_tree as *mut ffi::wlr_scene_node,
1076 x,
1077 y,
1078 );
1079 ffi::river_scene_node_set_position_if_changed(
1080 backdrop_tree as *mut ffi::wlr_scene_node,
1081 x,
1082 y,
1083 );
1084 }
1085
1086 if force {
1087 // Backdrop in the gap color, then the cell lattice. The
1088 // backdrop always draws — while a grid client is live it
1089 // is the safety net beyond the patch edges during fast
1090 // pans; the CELLS yield to the client's rendering.
1091 set_backdrop(frame.backdrop_w, frame.backdrop_h, grid.gap_color.0.as_ptr());
1092
1093 if let Some(cells) = frame.cells.as_ref().filter(|_| wm.grid_cells_enabled) {
1094 // scenefx fade-inset wire encoding: inset px * 1000
1095 // + fade-mode index; 0 disables the fade.
1096 use crate::policy::api::GridFadeMode;
1097 let fade_mode = match grid.fade_mode {
1098 GridFadeMode::Linear => 0,
1099 GridFadeMode::Smoothstep => 1,
1100 GridFadeMode::Quadratic => 2,
1101 GridFadeMode::Cosine => 3,
1102 GridFadeMode::Gaussian => 4,
1103 };
1104 let inset_scaled = if cells.fade_inset_px > 0 {
1105 cells.fade_inset_px * 1000 + fade_mode
1106 } else {
1107 0
1108 };
1109 // Widened exactly like the windows' corner clip:
1110 // at corner_shape > 2 the superellipse hugs the
1111 // corner, so the raw radius reads nearly square —
1112 // and a tiled window's (widened) arc must land on
1113 // the cell's arc.
1114 let cell_radius = crate::window::widen_corner_radius(
1115 cells.corner_radius_px, cells.cell_w_px, cells.cell_h_px,
1116 );
1117 // The root plate-edge roll (mirroring cce-grid): the
1118 // bevel-width knob clamped to a fraction of the
1119 // rail, pre-scaled by zoom like every cell metric,
1120 // so the rail reads as a flat face with a narrow
1121 // lip at each sunken cell — not a full-ramp grout.
1122 // style.surface.desktop.line_relief overrides the
1123 // width outright (0 = no lip). The ring expands the
1124 // cell box by the roll, inner edge concentric with
1125 // the cell arc.
1126 let gap_px = (frame.period_px_exact_x - cells.cell_w_px as f64)
1127 .min(frame.period_px_exact_y - cells.cell_h_px as f64)
1128 .max(0.0);
1129 let roll = layout
1130 .desktop_line_relief
1131 .map(|v| v * zoom)
1132 .unwrap_or_else(|| (layout.bevel_thickness as f64 * zoom).min(gap_px * 0.25))
1133 .max(0.0);
1134 let hg = roll.round() as i32;
1135 let ring_w_px = cells.cell_w_px + 2 * hg;
1136 let ring_h_px = cells.cell_h_px + 2 * hg;
1137 let ring_radius = cell_radius + hg;
1138 // Cell positions from the EXACT period, rounded per
1139 // cell: a rounded-period spacing drifts from the
1140 // world-anchored windows at fractional zooms (the
1141 // grid visibly slides against window edges when
1142 // panning).
1143 for col in 0..=cells.cols {
1144 let rel_x = (col as f64 * frame.period_px_exact_x).round() as i32;
1145 for row in 0..=cells.rows {
1146 let rel_y = (row as f64 * frame.period_px_exact_y).round() as i32;
1147 get_rect(cells.cell_w_px, cells.cell_h_px, cells.color.0.as_ptr(), rel_x, rel_y, cell_radius, inset_scaled);
1148 if bevel_on && hg > 0 {
1149 get_bevel(ring_w_px, ring_h_px, rel_x - hg, rel_y - hg, ring_radius, roll as f32);
1150 }
1151 }
1152 }
1153 }
1154 }
1155 }
1156 crate::policy::api::BackgroundSpec::Solid(color) => {
1157 ffi::river_scene_node_set_position_if_changed(
1158 grid_tree as *mut ffi::wlr_scene_node,
1159 self.sent.x,
1160 self.sent.y,
1161 );
1162 ffi::river_scene_node_set_position_if_changed(
1163 backdrop_tree as *mut ffi::wlr_scene_node,
1164 self.sent.x,
1165 self.sent.y,
1166 );
1167 if force {
1168 set_backdrop(viewport_w, viewport_h, color.0.as_ptr());
1169 }
1170 }
1171 }
1172
1173 if force {
1174 // Disable unused rects in the pool to release GPU/scene resources.
1175 for i in pool_idx..pool.len() {
1176 ffi::wlr_scene_node_set_enabled(pool[i] as *mut ffi::wlr_scene_node, false);
1177 }
1178 for i in bevel_idx..bevel_pool.len() {
1179 ffi::wlr_scene_node_set_enabled(&mut (*bevel_pool[i]).node as *mut ffi::wlr_scene_node, false);
1180 }
1181 }
1182
1183 self.draw_cell_labels();
1184 if suspended {
1185 ffi::wlr_scene_node_set_enabled(grid_tree as *mut ffi::wlr_scene_node, true);
1186 }
1187 }
1188
1189 /// Name every visible desktop square, chess style, while overview is open.
1190 ///
1191 /// The labels live in the grid tree, so they inherit its modulo shift and
1192 /// ride along with a pan for free; only the world index of the first drawn
1193 /// cell (`GridFrame::first_col/row`, computed in policy) is needed to know
1194 /// what to write. Outside overview every node is disabled — this is a
1195 /// navigation aid, not desktop furniture.
1196 unsafe fn draw_cell_labels(&mut self) {
1197 let wm = &(*self.server).wm;
1198 let overview = wm.mode == crate::window_manager::WindowManagerMode::Overview
1199 && wm.layout.desktop_cell_labels;
1200
1201 if !overview {
1202 if !self.cell_label_pool.is_empty() {
1203 for &(node, _) in &self.cell_label_pool {
1204 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, false);
1205 }
1206 }
1207 return;
1208 }
1209 if self.grid_tree.is_null() {
1210 return;
1211 }
1212 if self.cell_label_tree.is_null() {
1213 self.cell_label_tree = ffi::wlr_scene_tree_create(self.grid_tree);
1214 if self.cell_label_tree.is_null() {
1215 return;
1216 }
1217 }
1218 // Above the cell rects and the bevel subtree, which draw_grid raised
1219 // just before this.
1220 ffi::wlr_scene_node_raise_to_top(self.cell_label_tree as *mut ffi::wlr_scene_node);
1221
1222 let (viewport_w, viewport_h) = self.current.dimensions();
1223 let spec = wm.layout.background_spec();
1224 let crate::policy::api::BackgroundSpec::Grid(grid) = &spec else {
1225 self.disable_cell_labels();
1226 return;
1227 };
1228 let frame = crate::policy::background::grid_frame(
1229 grid,
1230 wm.camera(),
1231 viewport_w,
1232 viewport_h,
1233 self.sent.x,
1234 self.sent.y,
1235 );
1236 let Some(cells) = &frame.cells else {
1237 self.disable_cell_labels();
1238 return;
1239 };
1240
1241 // A fixed fraction of the on-screen cell, clamped so labels stay
1242 // readable when zoomed far out and don't swell into billboards when
1243 // near. Below the floor there is no room for glyphs at all.
1244 let min_cell_px = cells.cell_w_px.min(cells.cell_h_px);
1245 let px = ((min_cell_px as f32) * 0.16).clamp(9.0, 40.0);
1246 if px * 3.0 > min_cell_px as f32 {
1247 self.disable_cell_labels();
1248 return;
1249 }
1250 let px_key = px.round() as u32;
1251 if px_key != self.last_label_px || self.cell_labels.len() > 512 {
1252 self.cell_labels.clear();
1253 self.last_label_px = px_key;
1254 // The freed buffers' addresses can be handed straight back to the
1255 // next rasterization, so a stale pointer here would compare equal
1256 // to a different label and skip the update.
1257 for entry in self.cell_label_pool.iter_mut() {
1258 entry.1 = std::ptr::null_mut();
1259 }
1260 }
1261
1262 let inset = (min_cell_px as f64 * 0.06).round() as i32;
1263 let mut idx = 0usize;
1264 for col in 0..=cells.cols {
1265 let rel_x = (col as f64 * frame.period_px_exact_x).round() as i32;
1266 for row in 0..=cells.rows {
1267 let rel_y = (row as f64 * frame.period_px_exact_y).round() as i32;
1268 let Some(label) = self.cell_labels.get_square(frame.first_col + col, frame.first_row + row, px) else {
1269 continue;
1270 };
1271 let (buf, lw, lh) = (label.buffer, label.width, label.height);
1272
1273 let node = if idx < self.cell_label_pool.len() {
1274 let (node, shown) = self.cell_label_pool[idx];
1275 if shown != buf {
1276 ffi::wlr_scene_buffer_set_buffer(node, buf);
1277 self.cell_label_pool[idx].1 = buf;
1278 }
1279 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, true);
1280 node
1281 } else {
1282 let node = ffi::wlr_scene_buffer_create(self.cell_label_tree, buf);
1283 if node.is_null() {
1284 continue;
1285 }
1286 self.cell_label_pool.push((node, buf));
1287 node
1288 };
1289 ffi::wlr_scene_buffer_set_dest_size(node, lw, lh);
1290 // Top-left corner of the cell, inside the fade inset.
1291 ffi::river_scene_node_set_position_if_changed(
1292 node as *mut ffi::wlr_scene_node,
1293 rel_x + inset,
1294 rel_y + inset,
1295 );
1296 let _ = lh;
1297 idx += 1;
1298 }
1299 }
1300
1301 for i in idx..self.cell_label_pool.len() {
1302 ffi::wlr_scene_node_set_enabled(
1303 self.cell_label_pool[i].0 as *mut ffi::wlr_scene_node,
1304 false,
1305 );
1306 }
1307 }
1308
1309 unsafe fn disable_cell_labels(&self) {
1310 for &(node, _) in &self.cell_label_pool {
1311 ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, false);
1312 }
1313 }
1314 }
1315
1316 unsafe extern "C" fn handle_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1317 let output = crate::container_of!(listener, Output, destroy);
1318
1319 log::debug!("Output destroyed");
1320 crate::xwayland_window::note_output_change();
1321
1322 // Remove listeners
1323 wl_listener_remove(&mut (*output).destroy);
1324 wl_listener_remove(&mut (*output).request_state);
1325 wl_listener_remove(&mut (*output).frame);
1326 wl_listener_remove(&mut (*output).present);
1327
1328 if !(*output).scene_output.is_null() {
1329 ffi::wlr_scene_output_destroy((*output).scene_output);
1330 (*output).scene_output = std::ptr::null_mut();
1331 }
1332
1333 if !(*output).background_rect.is_null() {
1334 ffi::wlr_scene_node_destroy((*output).background_rect as *mut ffi::wlr_scene_node);
1335 (*output).background_rect = std::ptr::null_mut();
1336 }
1337
1338 if !(*output).grid_tree.is_null() {
1339 ffi::wlr_scene_node_destroy((*output).grid_tree as *mut ffi::wlr_scene_node);
1340 (*output).grid_tree = std::ptr::null_mut();
1341 }
1342 if !(*output).grid_backdrop_tree.is_null() {
1343 ffi::wlr_scene_node_destroy((*output).grid_backdrop_tree as *mut ffi::wlr_scene_node);
1344 (*output).grid_backdrop_tree = std::ptr::null_mut();
1345 (*output).grid_backdrop_rect = std::ptr::null_mut();
1346 }
1347
1348 if !(*output).adjust_tree.is_null() {
1349 ffi::wlr_scene_node_destroy((*output).adjust_tree as *mut ffi::wlr_scene_node);
1350 (*output).adjust_tree = std::ptr::null_mut();
1351 }
1352 (*output).adjust_rects.clear();
1353
1354 if !(*output).wlr_output.is_null() {
1355 ffi::river_wlr_output_set_data((*output).wlr_output, std::ptr::null_mut());
1356 }
1357
1358 (*output).wlr_output = std::ptr::null_mut();
1359 (*output).scheduled.mode = OutputMode::None;
1360 (*output).sent.mode = OutputMode::None;
1361 (*output).current.mode = OutputMode::None;
1362 (*output).scheduled.state = OutputStateValue::Destroying;
1363
1364 (*(*output).server).wm.dirty_windowing();
1365 }
1366
1367 unsafe extern "C" fn handle_request_state(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
1368 let output = &mut *crate::container_of!(listener, Output, request_state);
1369 let event = data as *mut ffi::wlr_output_event_request_state;
1370
1371 let committed: u32 = std::mem::transmute((*(*event).state).committed);
1372 // mode field is bit 1 (mode)
1373 if committed & 1 != 0 {
1374 if !(*(*event).state).mode.is_null() {
1375 output.scheduled.mode = OutputMode::Standard((*(*event).state).mode);
1376 } else {
1377 output.scheduled.mode = OutputMode::Custom {
1378 width: (*(*event).state).custom_mode.width,
1379 height: (*(*event).state).custom_mode.height,
1380 refresh: (*(*event).state).custom_mode.refresh,
1381 };
1382 }
1383 }
1384
1385 (*output.server).wm.dirty_windowing();
1386 }
1387
1388 /// `CCE_FRAME_DEBUG` (any value) also ticks every output frame, so a client's
1389 /// frame-callback interval can be compared against the rate the output is
1390 /// actually rendering at — the two diverging is the signature of a surface
1391 /// being skipped by the scene's visible gate in `wlr_scene_buffer_send_frame_done`.
1392 pub(crate) fn frame_debug() -> bool {
1393 static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1394 *FLAG.get_or_init(|| std::env::var_os("CCE_FRAME_DEBUG").is_some())
1395 }
1396
1397 /// `CCE_OVDBG=1` arms the `/tmp/cce-ovdbg` per-frame scene dump (see
1398 /// `render_and_commit`); without it the file is never even looked for.
1399 fn ovdbg_enabled() -> bool {
1400 static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1401 *FLAG.get_or_init(|| std::env::var_os("CCE_OVDBG").is_some())
1402 }
1403
1404 impl Output {
1405 /// The refresh period to plan frames by: what the last `present`
1406 /// reported, else the current mode's rate, else 60 Hz.
1407 unsafe fn refresh_period_ns(&self) -> u64 {
1408 if self.present_refresh_ns > 0 {
1409 return self.present_refresh_ns;
1410 }
1411 let mhz = if self.wlr_output.is_null() { 0 } else { ffi::river_wlr_output_get_refresh(self.wlr_output) };
1412 if mhz > 0 {
1413 1_000_000_000_000 / mhz as u64
1414 } else {
1415 16_666_667
1416 }
1417 }
1418
1419 /// When the frame rendered now is expected to reach the screen: the
1420 /// first point of a vblank grid after now (plus a small render lead).
1421 /// The camera animates to this instant, so its step is an exact whole
1422 /// number of refresh periods whether the frame callback ran early or
1423 /// late, a missed vblank is a double step rather than a stumble, and a
1424 /// second frame inside one period gets the same target (a zero step).
1425 ///
1426 /// The grid's phase locks to the hardware presentation timestamps and
1427 /// re-anchors only when they drift by more than a quarter period, so
1428 /// the per-present jitter of the timestamps themselves (and the
1429 /// headless backend's commit-time stamps) never reaches the camera.
1430 pub unsafe fn predicted_present_ns(&mut self) -> u64 {
1431 let now = util::timestamp_ns();
1432 let period = self.refresh_period_ns().max(1);
1433 if self.present_when_ns != 0 {
1434 let phase = self.present_when_ns % period;
1435 let drift = if self.frame_phase_ns == u64::MAX {
1436 u64::MAX
1437 } else {
1438 let d = phase.abs_diff(self.frame_phase_ns);
1439 d.min(period - d)
1440 };
1441 if drift > period / 4 {
1442 self.frame_phase_ns = phase;
1443 }
1444 } else if self.frame_phase_ns == u64::MAX {
1445 self.frame_phase_ns = now % period;
1446 }
1447 let lead = period / 8;
1448 let base = (now + lead).saturating_sub(self.frame_phase_ns);
1449 self.frame_phase_ns + (base / period + 1) * period
1450 }
1451 }
1452
1453 /// Log a failed frame, at most once per 10 s with a count: a refused commit
1454 /// can repeat at frame rate for minutes (38,784 lines in one session), and
1455 /// wlroots logs each one itself as well.
1456 fn log_render_error(e: &str) {
1457 use std::sync::atomic::{AtomicU64, Ordering};
1458 use std::sync::Mutex;
1459 static SUPPRESSED: AtomicU64 = AtomicU64::new(0);
1460 static LAST: Mutex<Option<std::time::Instant>> = Mutex::new(None);
1461 let mut last = LAST.lock().unwrap();
1462 if last.is_some_and(|t| t.elapsed().as_secs() < 10) {
1463 SUPPRESSED.fetch_add(1, Ordering::Relaxed);
1464 return;
1465 }
1466 *last = Some(std::time::Instant::now());
1467 match SUPPRESSED.swap(0, Ordering::Relaxed) {
1468 0 => log::error!("{}", e),
1469 n => log::error!("{} (and {} more in the last 10 s)", e, n),
1470 }
1471 }
1472
1473 unsafe extern "C" fn handle_frame(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
1474 let output = &mut *crate::container_of!(listener, Output, frame);
1475 // The camera steps here, on the vblank, to where it should be at the
1476 // instant THIS frame is presented (see WindowManager::step_camera_frame).
1477 let frame_target_ns = output.predicted_present_ns();
1478 (*output.server).wm.step_camera_frame(frame_target_ns);
1479 // Likewise the interactive move/resize: one configure + relayout per
1480 // vblank, for the pointer's latest position.
1481 (*output.server).wm.step_op_frame();
1482 let render_start = if frame_debug() {
1483 Some(std::time::Instant::now())
1484 } else {
1485 None
1486 };
1487 if let Err(e) = output.render_and_commit() {
1488 log_render_error(e);
1489 }
1490 if let Some(start) = render_start {
1491 // Epoch ms mod 100000 — the shared tracer time base (see cce-ui's
1492 // CCE_PRESENT_DEBUG), so compositor and client logs interleave.
1493 let t = std::time::SystemTime::now()
1494 .duration_since(std::time::UNIX_EPOCH)
1495 .unwrap()
1496 .as_millis()
1497 % 100000;
1498 log::info!(
1499 "[cce-frame] t={} output frame (render_and_commit {}us)",
1500 t,
1501 start.elapsed().as_micros()
1502 );
1503 }
1504 let now = util::timestamp();
1505 let mut ffi_now = ffi::timespec {
1506 tv_sec: now.tv_sec,
1507 tv_nsec: now.tv_nsec,
1508 };
1509 ffi::wlr_scene_output_send_frame_done(output.scene_output, &mut ffi_now);
1510 // The scene's frame-done pass is gated on a node being visible, and the
1511 // desktop grid spends most of its life behind opaque windows — so the
1512 // one client that MUST repaint on demand is the one whose callbacks dry
1513 // up. cce-ui's runner waits on a frame callback before it renders, and
1514 // only a 250ms starvation fallback unblocks it: every patch took a
1515 // quarter second to come back, which is longer than the overview ramp
1516 // and is why an exit's replacement patch used to land after the
1517 // animation. While a patch is in the air, drive the client directly.
1518 (*output.server).wm.send_frame_done_to_grid_clients_awaiting_patch();
1519 }
1520
1521 unsafe extern "C" fn handle_present(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
1522 let output = &mut *crate::container_of!(listener, Output, present);
1523 let event = data as *mut ffi::wlr_output_event_present;
1524 if !(*event).presented {
1525 return;
1526 }
1527 // Presentation clock bookkeeping, and the dropped-frame count: a vblank
1528 // sequence that advanced by more than one since the last present means
1529 // frames were skipped. Backends without a counter (headless) report
1530 // seq 0; there the gap is inferred from time, but only while the camera
1531 // is animating — a still desktop legitimately presents nothing for ages.
1532 {
1533 let when_ns = (*event).when.tv_sec as u64 * 1_000_000_000 + (*event).when.tv_nsec as u64;
1534 if (*event).refresh > 0 {
1535 output.present_refresh_ns = (*event).refresh as u64;
1536 }
1537 let period = output.refresh_period_ns();
1538 let seq = (*event).seq as u32;
1539 let mut dropped = 0u64;
1540 if seq != 0 && output.present_seq != 0 && seq > output.present_seq + 1 {
1541 dropped = (seq - output.present_seq - 1) as u64;
1542 } else if seq == 0 && output.present_when_ns != 0 && (*output.server).wm.camera_anim_active {
1543 let gap = when_ns.saturating_sub(output.present_when_ns);
1544 let periods = (gap + period / 2) / period;
1545 dropped = periods.saturating_sub(1);
1546 }
1547 if dropped > 0 {
1548 output.present_dropped += dropped;
1549 if frame_debug() {
1550 log::info!(
1551 "[cce-frame] present seq={} dropped={} (total {}) refresh={}us",
1552 seq,
1553 dropped,
1554 output.present_dropped,
1555 period / 1000
1556 );
1557 }
1558 }
1559 output.present_when_ns = when_ns;
1560 output.present_seq = seq;
1561 }
1562 match output.lock_render_state {
1563 LockRenderState::PendingUnlock => {
1564 output.lock_render_state = LockRenderState::Unlocked;
1565 }
1566 LockRenderState::PendingBlank => {
1567 output.lock_render_state = LockRenderState::Blanked;
1568 (*output.server).lock_manager.maybe_lock();
1569 }
1570 LockRenderState::PendingLockSurface => {
1571 output.lock_render_state = LockRenderState::LockSurface;
1572 (*output.server).lock_manager.maybe_lock();
1573 }
1574 _ => {}
1575 }
1576 }
1577
1578 // Helpers for raw Wayland FFI protocol events
1579 pub unsafe fn zcce_output_send_removed(resource: *mut ffi::wl_resource) {
1580 ffi::wl_resource_post_event(resource, 0);
1581 }
1582
1583 pub unsafe fn zcce_output_send_wl_output(resource: *mut ffi::wl_resource, name: u32) {
1584 ffi::wl_resource_post_event(resource, 1, name);
1585 }
1586
1587 pub unsafe fn zcce_output_send_position(resource: *mut ffi::wl_resource, x: i32, y: i32) {
1588 ffi::wl_resource_post_event(resource, 2, x, y);
1589 }
1590
1591 pub unsafe fn zcce_output_send_dimensions(resource: *mut ffi::wl_resource, width: i32, height: i32) {
1592 ffi::wl_resource_post_event(resource, 3, width, height);
1593 }