add multi-monitor support via RandR
- SHA
b05b49f4563c4a3214579716de60e134c6ffa9c2- Parents
-
da7a230 - Tree
4c7e75e
b05b49f
b05b49f4563c4a3214579716de60e134c6ffa9c2da7a230
4c7e75e| Status | File | + | - |
|---|---|---|---|
| M |
garchomp-ipc/src/lib.rs
|
13 | 0 |
| M |
garchomp/src/compositor/mod.rs
|
13 | 3 |
| M |
garchomp/src/main.rs
|
13 | 0 |
| M |
garchomp/src/x11/connection.rs
|
117 | 0 |
| M |
garchomp/src/x11/mod.rs
|
1 | 1 |
garchomp-ipc/src/lib.rsmodified@@ -55,6 +55,19 @@ pub struct CompositorStatus { | ||
| 55 | 55 | pub current_workspace: usize, |
| 56 | 56 | pub effects_enabled: EffectsStatus, |
| 57 | 57 | pub connected_to_gar: bool, |
| 58 | + #[serde(default)] | |
| 59 | + pub monitors: Vec<MonitorStatus>, | |
| 60 | +} | |
| 61 | + | |
| 62 | +/// Information about a monitor. | |
| 63 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 64 | +pub struct MonitorStatus { | |
| 65 | + pub name: String, | |
| 66 | + pub x: i16, | |
| 67 | + pub y: i16, | |
| 68 | + pub width: u16, | |
| 69 | + pub height: u16, | |
| 70 | + pub primary: bool, | |
| 58 | 71 | } |
| 59 | 72 | |
| 60 | 73 | /// Status of compositor effects. |
garchomp/src/compositor/mod.rsmodified@@ -14,7 +14,7 @@ pub use workspace::{TransitionDirection, WorkspaceState, WorkspaceTransition}; | ||
| 14 | 14 | use crate::config::LuaConfig; |
| 15 | 15 | use crate::ipc::GarConnection; |
| 16 | 16 | use crate::render::{GpuError, HdrConfig, Renderer, WindowRenderInfo}; |
| 17 | -use crate::x11::{CompositeExt, Connection}; | |
| 17 | +use crate::x11::{CompositeExt, Connection, MonitorInfo}; | |
| 18 | 18 | use garchomp_ipc::GarEvent; |
| 19 | 19 | use std::collections::HashMap; |
| 20 | 20 | use thiserror::Error; |
@@ -64,6 +64,8 @@ pub struct Compositor { | ||
| 64 | 64 | lua_config: Option<LuaConfig>, |
| 65 | 65 | /// Root window background pixmap (wallpaper). |
| 66 | 66 | root_pixmap: Option<u32>, |
| 67 | + /// Monitor configuration from RandR. | |
| 68 | + pub monitors: Vec<MonitorInfo>, | |
| 67 | 69 | } |
| 68 | 70 | |
| 69 | 71 | impl Compositor { |
@@ -83,9 +85,9 @@ impl Compositor { | ||
| 83 | 85 | let width = screen.width_in_pixels as u32; |
| 84 | 86 | let height = screen.height_in_pixels as u32; |
| 85 | 87 | |
| 86 | - // Initialize GPU renderer | |
| 88 | + // Initialize GPU renderer (vsync will be configured after loading Lua config) | |
| 87 | 89 | tracing::info!("Initializing GPU renderer for {}x{} surface", width, height); |
| 88 | - let renderer = Renderer::new(overlay, width, height).await?; | |
| 90 | + let renderer = Renderer::new(overlay, width, height, crate::render::VSync::default()).await?; | |
| 89 | 91 | tracing::info!("GPU renderer initialized"); |
| 90 | 92 | |
| 91 | 93 | // Subscribe to events on root window |
@@ -134,6 +136,9 @@ impl Compositor { | ||
| 134 | 136 | tracing::info!("Found root pixmap: {:#x}", pix); |
| 135 | 137 | } |
| 136 | 138 | |
| 139 | + // Query monitor configuration | |
| 140 | + let monitors = conn.get_monitors().unwrap_or_default(); | |
| 141 | + | |
| 137 | 142 | let mut compositor = Self { |
| 138 | 143 | conn, |
| 139 | 144 | overlay, |
@@ -147,11 +152,16 @@ impl Compositor { | ||
| 147 | 152 | gar, |
| 148 | 153 | lua_config, |
| 149 | 154 | root_pixmap, |
| 155 | + monitors, | |
| 150 | 156 | }; |
| 151 | 157 | |
| 152 | 158 | // Get initial active window |
| 153 | 159 | compositor.active_window = compositor.conn.get_active_window(); |
| 154 | 160 | |
| 161 | + // Apply VSync setting from config | |
| 162 | + compositor.renderer.set_vsync(compositor.effects.vsync); | |
| 163 | + tracing::info!("VSync mode: {:?}", compositor.effects.vsync); | |
| 164 | + | |
| 155 | 165 | // Apply HDR config if enabled |
| 156 | 166 | if let Some(ref lua) = compositor.lua_config { |
| 157 | 167 | let hdr_config = lua.get_hdr_config(); |
garchomp/src/main.rsmodified@@ -293,6 +293,18 @@ fn handle_ipc_request(compositor: &mut compositor::Compositor, request: ipc::Cli | ||
| 293 | 293 | } |
| 294 | 294 | } |
| 295 | 295 | Request::Status => { |
| 296 | + let monitors: Vec<garchomp_ipc::MonitorStatus> = compositor.monitors | |
| 297 | + .iter() | |
| 298 | + .map(|m| garchomp_ipc::MonitorStatus { | |
| 299 | + name: m.name.clone(), | |
| 300 | + x: m.x, | |
| 301 | + y: m.y, | |
| 302 | + width: m.width, | |
| 303 | + height: m.height, | |
| 304 | + primary: m.primary, | |
| 305 | + }) | |
| 306 | + .collect(); | |
| 307 | + | |
| 296 | 308 | Response::Status(garchomp_ipc::CompositorStatus { |
| 297 | 309 | version: garchomp_ipc::PROTOCOL_VERSION, |
| 298 | 310 | window_count: compositor.windows.len(), |
@@ -304,6 +316,7 @@ fn handle_ipc_request(compositor: &mut compositor::Compositor, request: ipc::Cli | ||
| 304 | 316 | blur_strength: compositor.effects.blur_strength, |
| 305 | 317 | }, |
| 306 | 318 | connected_to_gar: compositor.is_connected_to_gar(), |
| 319 | + monitors, | |
| 307 | 320 | }) |
| 308 | 321 | } |
| 309 | 322 | }; |
garchomp/src/x11/connection.rsmodified@@ -6,10 +6,28 @@ use thiserror::Error; | ||
| 6 | 6 | use x11rb::connection::{Connection as X11Connection, RequestConnection}; |
| 7 | 7 | use x11rb::protocol::composite::ConnectionExt as CompositeConnectionExt; |
| 8 | 8 | use x11rb::protocol::damage::ConnectionExt as DamageConnectionExt; |
| 9 | +use x11rb::protocol::randr::ConnectionExt as RandrConnectionExt; | |
| 9 | 10 | use x11rb::protocol::xfixes::ConnectionExt as XfixesConnectionExt; |
| 10 | 11 | use x11rb::protocol::xproto::{ConnectionExt as XprotoConnectionExt, Screen, Window}; |
| 11 | 12 | use x11rb::rust_connection::RustConnection; |
| 12 | 13 | |
| 14 | +/// Information about a monitor/output. | |
| 15 | +#[derive(Debug, Clone)] | |
| 16 | +pub struct MonitorInfo { | |
| 17 | + /// Monitor name (e.g., "DP-1", "HDMI-0"). | |
| 18 | + pub name: String, | |
| 19 | + /// X position in pixels. | |
| 20 | + pub x: i16, | |
| 21 | + /// Y position in pixels. | |
| 22 | + pub y: i16, | |
| 23 | + /// Width in pixels. | |
| 24 | + pub width: u16, | |
| 25 | + /// Height in pixels. | |
| 26 | + pub height: u16, | |
| 27 | + /// Whether this is the primary monitor. | |
| 28 | + pub primary: bool, | |
| 29 | +} | |
| 30 | + | |
| 13 | 31 | #[derive(Error, Debug)] |
| 14 | 32 | pub enum ConnectionError { |
| 15 | 33 | #[error("failed to connect to X11 display")] |
@@ -224,4 +242,103 @@ impl Connection { | ||
| 224 | 242 | |
| 225 | 243 | reply.value32().and_then(|mut v| v.next()).filter(|&p| p != 0) |
| 226 | 244 | } |
| 245 | + | |
| 246 | + /// Query all monitors using RandR. | |
| 247 | + /// Returns a list of active monitors with their geometry. | |
| 248 | + pub fn get_monitors(&self) -> Result<Vec<MonitorInfo>> { | |
| 249 | + // Query RandR version first | |
| 250 | + let randr_version = self.conn.randr_query_version(1, 5)?.reply()?; | |
| 251 | + tracing::debug!( | |
| 252 | + "RandR version: {}.{}", | |
| 253 | + randr_version.major_version, | |
| 254 | + randr_version.minor_version | |
| 255 | + ); | |
| 256 | + | |
| 257 | + // Get screen resources | |
| 258 | + let resources = self.conn.randr_get_screen_resources(self.root())?.reply()?; | |
| 259 | + | |
| 260 | + // Get primary output | |
| 261 | + let primary = self.conn.randr_get_output_primary(self.root())?.reply()?; | |
| 262 | + let primary_output = primary.output; | |
| 263 | + | |
| 264 | + let mut monitors = Vec::new(); | |
| 265 | + | |
| 266 | + // Iterate through all CRTCs to find active outputs | |
| 267 | + for crtc in &resources.crtcs { | |
| 268 | + let crtc_info = match self.conn.randr_get_crtc_info(*crtc, 0)?.reply() { | |
| 269 | + Ok(info) => info, | |
| 270 | + Err(_) => continue, | |
| 271 | + }; | |
| 272 | + | |
| 273 | + // Skip disabled CRTCs | |
| 274 | + if crtc_info.width == 0 || crtc_info.height == 0 { | |
| 275 | + continue; | |
| 276 | + } | |
| 277 | + | |
| 278 | + // Get output name from first connected output | |
| 279 | + for output in &crtc_info.outputs { | |
| 280 | + let output_info = match self.conn.randr_get_output_info(*output, 0)?.reply() { | |
| 281 | + Ok(info) => info, | |
| 282 | + Err(_) => continue, | |
| 283 | + }; | |
| 284 | + | |
| 285 | + // Convert name bytes to string | |
| 286 | + let name = String::from_utf8_lossy(&output_info.name).to_string(); | |
| 287 | + | |
| 288 | + monitors.push(MonitorInfo { | |
| 289 | + name, | |
| 290 | + x: crtc_info.x, | |
| 291 | + y: crtc_info.y, | |
| 292 | + width: crtc_info.width, | |
| 293 | + height: crtc_info.height, | |
| 294 | + primary: *output == primary_output, | |
| 295 | + }); | |
| 296 | + | |
| 297 | + // Only take first output per CRTC | |
| 298 | + break; | |
| 299 | + } | |
| 300 | + } | |
| 301 | + | |
| 302 | + // Sort by position (left to right, top to bottom) | |
| 303 | + monitors.sort_by(|a, b| { | |
| 304 | + if a.y != b.y { | |
| 305 | + a.y.cmp(&b.y) | |
| 306 | + } else { | |
| 307 | + a.x.cmp(&b.x) | |
| 308 | + } | |
| 309 | + }); | |
| 310 | + | |
| 311 | + tracing::info!("Found {} monitors", monitors.len()); | |
| 312 | + for (i, m) in monitors.iter().enumerate() { | |
| 313 | + tracing::info!( | |
| 314 | + " Monitor {}: {} {}x{}+{}+{} {}", | |
| 315 | + i, m.name, m.width, m.height, m.x, m.y, | |
| 316 | + if m.primary { "(primary)" } else { "" } | |
| 317 | + ); | |
| 318 | + } | |
| 319 | + | |
| 320 | + Ok(monitors) | |
| 321 | + } | |
| 322 | + | |
| 323 | + /// Find which monitor a point is on. | |
| 324 | + pub fn point_to_monitor(&self, x: i32, y: i32, monitors: &[MonitorInfo]) -> Option<usize> { | |
| 325 | + for (i, m) in monitors.iter().enumerate() { | |
| 326 | + let mx = m.x as i32; | |
| 327 | + let my = m.y as i32; | |
| 328 | + let mw = m.width as i32; | |
| 329 | + let mh = m.height as i32; | |
| 330 | + | |
| 331 | + if x >= mx && x < mx + mw && y >= my && y < my + mh { | |
| 332 | + return Some(i); | |
| 333 | + } | |
| 334 | + } | |
| 335 | + None | |
| 336 | + } | |
| 337 | + | |
| 338 | + /// Find which monitor a window is primarily on (by center point). | |
| 339 | + pub fn window_to_monitor(&self, x: i16, y: i16, width: u16, height: u16, monitors: &[MonitorInfo]) -> Option<usize> { | |
| 340 | + let center_x = x as i32 + (width as i32 / 2); | |
| 341 | + let center_y = y as i32 + (height as i32 / 2); | |
| 342 | + self.point_to_monitor(center_x, center_y, monitors) | |
| 343 | + } | |
| 227 | 344 | } |
garchomp/src/x11/mod.rsmodified@@ -7,5 +7,5 @@ mod visual; | ||
| 7 | 7 | |
| 8 | 8 | pub use atoms::Atoms; |
| 9 | 9 | pub use composite::CompositeExt; |
| 10 | -pub use connection::{Connection, ConnectionError}; | |
| 10 | +pub use connection::{Connection, ConnectionError, MonitorInfo}; | |
| 11 | 11 | pub use visual::{VisualConfig, create_colormap, find_best_visual, get_visual_depth, has_deepcolor_support}; |