git.lucas.co / cce-compositor
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 }