diff options
| author | srdusr <[email protected]> | 2026-05-04 21:37:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-05-04 21:37:00 +0200 |
| commit | 790ee906957b8b855dfc23cf1b206628585fd1ec (patch) | |
| tree | 2351094dede798ed8b3fa32fd8fa41641752c64b /crates/wayland/src/winit | |
| parent | 3ea84a31ee671b88d8c343b3007de0e975eff31c (diff) | |
| download | srdwm-790ee906957b8b855dfc23cf1b206628585fd1ec.tar.gz srdwm-790ee906957b8b855dfc23cf1b206628585fd1ec.zip | |
Support monitor split in the nested backend, correcting a wrong "blocked"
Earlier today I wrote that a nested compositor cannot produce a second
monitor because split and fake monitors "need real head machinery that only
the DRM backend has". That was inferred from both commands returning ok and
changing nothing, not read from the code, and the first half is false.
udev/platform.rs was simply the only backend draining those request queues.
The winit poll never took them off, so the request sat there forever and the
dispatch looked like it had worked. Nothing about split is DRM-bound:
MonitorSplit is bookkeeping in WindowManager and split_rect is pure geometry
in core. The winit poll now drains split requests the same way, and its
monitors() expands a split into one Monitor per part with its own
full_geometry and maximize_geometry, matching the udev expansion. Splitting
the nested output into two 640x800 monitors with a seam now works, which is
what a multi-monitor repro needs. Fake monitors stay udev-only; that half was
not re-checked and is not claimed either way.
Also corrected: the capture pass measured the shadow rect from w.geometry
while both on-screen loops measure it from effective_frame, the client's real
committed size. src indexes into a buffer rasterised at the frame's size, so
the two disagreeing reads the wrong region whenever a client settles on a
different size than it was asked for.
The seam check this was meant to unblock is still not done. With the split
working, the negative control failed: a floating window off the seam had no
shadow either. Running both clients in one instance showed Alacritty renders
a shadow in a capture and Nemo renders none, same settings, both floating,
either focus. Nemo is server-side decorated and Alacritty is not, which is a
lead and not a conclusion. Recorded in docs/TODO.md as open rather than
guessed at.
515 tests pass, clippy clean.
Diffstat (limited to 'crates/wayland/src/winit')
| -rw-r--r-- | crates/wayland/src/winit/capture.rs | 16 | ||||
| -rw-r--r-- | crates/wayland/src/winit/nested_platform.rs | 48 |
2 files changed, 61 insertions, 3 deletions
diff --git a/crates/wayland/src/winit/capture.rs b/crates/wayland/src/winit/capture.rs index c382190..095b56b 100644 --- a/crates/wayland/src/winit/capture.rs +++ b/crates/wayland/src/winit/capture.rs @@ -133,8 +133,20 @@ impl WaylandPlatform { // and re-checked by. Border strips are still absent - a real // remaining gap, called out here rather than left silent. if let Some(shadow) = self.state.shadow_buffers.get(&id) { - let full = crate::decoration::shadow_rect(w.geometry); - let rect = crate::decoration::shadow_rect_clipped(w.geometry, &monitor_bounds); + // `effective_frame`, not `w.geometry` - the same correction + // both on-screen render loops apply before sizing any + // decoration bitmap (see its own doc comment). The shadow + // buffer was rasterised at the *frame's* size, and `src` + // below indexes into that buffer, so measuring from + // `w.geometry` instead reads the wrong region whenever the + // two differ. They differ for exactly the windows that + // matter here: a CSD client whose committed surface is not + // the size this compositor asked for. Found by sampling + // pixels - Alacritty's shadow appeared in a capture and + // Nemo's did not, and the only difference was that gap. + let frame = self.state.effective_frame(id, w.geometry); + let full = crate::decoration::shadow_rect(frame); + let rect = crate::decoration::shadow_rect_clipped(frame, &monitor_bounds); for fragment in crate::elements::visible_border_fragments(rect, &occluders) { let src = Rectangle::new( Point::from(((fragment.x - full.x) as f64, (fragment.y - full.y) as f64)), diff --git a/crates/wayland/src/winit/nested_platform.rs b/crates/wayland/src/winit/nested_platform.rs index 40d03ff..3b762ab 100644 --- a/crates/wayland/src/winit/nested_platform.rs +++ b/crates/wayland/src/winit/nested_platform.rs @@ -79,6 +79,28 @@ impl Platform for WaylandPlatform { for (pid, window) in self.wm.borrow_mut().drain_pin_input_requests() { self.state.set_virtual_pointer_pin(pid, window); } + // Monitor split, drained the same way `udev/platform.rs` drains it. + // + // This backend used to ignore the request entirely: the dispatch + // returned `{"ok":true}`, the request queued, and nothing ever + // took it off the queue, so `srd dispatch set output split` looked + // like it had worked and changed nothing. That silence cost real + // time - a two-monitor rendering bug could not be reproduced in a + // nested instance, and the conclusion drawn was "split needs DRM + // head machinery", which is not true of any part of it: `Monitor + // Split` is bookkeeping in `WindowManager` and `split_rect` is + // pure geometry in `core`. Splitting the one nested output into + // several logical monitors is exactly what a multi-monitor repro + // needs, and it works here for the same reason it works there. + let split_requests = self.wm.borrow_mut().drain_monitor_split_requests(); + if !split_requests.is_empty() { + for (name, parts, rows) in split_requests { + self.wm.borrow_mut().set_monitor_split(name, parts, rows); + } + // Same "just go recompute the monitor list" event the udev + // drain pushes - the payload is ignored by the handler. + self.pending.borrow_mut().push(CoreEvent::MonitorAdded(srdwm_core::Monitor::new(0, "", srdwm_core::Rect::new(0, 0, 0, 0)))); + } let wait = TARGET_FRAME_TIME.saturating_sub(self.last_frame.elapsed()); let _ = self.idle_event_loop.dispatch(Some(wait), &mut self.state); self.last_frame = Instant::now(); @@ -92,8 +114,32 @@ impl Platform for WaylandPlatform { // single output at the global origin, so the output-local zone // rectangle already is the usable global-space rect. let zone = layer_map_for_output(&self.output).non_exclusive_zone(); + let usable = srdwm_core::Rect::new(zone.loc.x, zone.loc.y, zone.size.w as u32, zone.size.h as u32); + let full_size = self.backend.window_size(); + let full = srdwm_core::Rect::new(0, 0, full_size.w as u32, full_size.h as u32); + let maximize = crate::input::maximize_geometry_for(&self.output, full); + // Expanded into one `Monitor` per split part, exactly as + // `udev/platform.rs`'s own `monitors()` does - see the split drain + // in `poll` above for why this backend supports it at all. + let split = self.wm.borrow().monitor_split("winit"); + let parts = split.map(|s| s.parts).unwrap_or(1).max(1); + let rows = split.map(|s| s.rows).unwrap_or(false); + if parts > 1 { + return Ok((0..parts) + .map(|part| { + let mut m = srdwm_core::Monitor::new(part, format!("winit-{}", part + 1), srdwm_core::monitor::split_rect(usable, part, parts, rows)); + m.full_geometry = srdwm_core::monitor::split_rect(full, part, parts, rows); + m.maximize_geometry = srdwm_core::monitor::split_rect(maximize, part, parts, rows); + // Exactly one primary, same rule as the udev backend: + // the split parts share one underlying output. + m.primary = part == 0; + m.split = true; + m + }) + .collect()); + } Ok(vec![{ - let rect = srdwm_core::Rect::new(zone.loc.x, zone.loc.y, zone.size.w as u32, zone.size.h as u32); + let rect = usable; let mut m = srdwm_core::Monitor::new(0, "winit", rect); // Same fix as `udev/platform.rs`'s matching function: `Monitor::new` // defaults `full_geometry` to `geometry`, which is already |