diff --git a/src/backend.rs b/src/backend.rs index c69c5bd1..9b05dc4f 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -79,7 +79,12 @@ pub trait Connector { fn on_change(&self, cb: Rc); fn damage(&self); fn drm_dev(&self) -> Option; - fn set_enabled(&self, enabled: bool); + fn enabled(&self) -> bool { + true + } + fn set_enabled(&self, enabled: bool) { + let _ = enabled; + } fn drm_feedback(&self) -> Option> { None } @@ -232,4 +237,5 @@ pub trait BackendDrmDevice { fn gtx_api(&self) -> GfxApi; fn version(&self) -> Result; fn set_direct_scanout_enabled(&self, enabled: bool); + fn is_render_device(&self) -> bool; } diff --git a/src/backends/dummy.rs b/src/backends/dummy.rs index ed6f8cfb..2a035ffe 100644 --- a/src/backends/dummy.rs +++ b/src/backends/dummy.rs @@ -53,10 +53,6 @@ impl Connector for DummyOutput { None } - fn set_enabled(&self, _enabled: bool) { - // nothing - } - fn set_mode(&self, _mode: Mode) { // nothing } diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index f0710b95..470f87ca 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -130,6 +130,10 @@ impl BackendDrmDevice for MetalDrmDevice { fn set_direct_scanout_enabled(&self, enabled: bool) { self.direct_scanout_enabled.set(Some(enabled)); } + + fn is_render_device(&self) -> bool { + Some(self.id) == self.backend.ctx.get().map(|c| c.dev_id) + } } pub struct HandleEvents { @@ -846,6 +850,10 @@ impl Connector for MetalConnector { Some(self.dev.id) } + fn enabled(&self) -> bool { + self.enabled.get() + } + fn set_enabled(&self, enabled: bool) { if self.enabled.replace(enabled) != enabled { if self.display.borrow_mut().connection == ConnectorStatus::Connected { diff --git a/src/backends/x.rs b/src/backends/x.rs index c195ca14..6aaeb5ae 100644 --- a/src/backends/x.rs +++ b/src/backends/x.rs @@ -994,6 +994,10 @@ impl BackendDrmDevice for XDrmDevice { fn set_direct_scanout_enabled(&self, enabled: bool) { let _ = enabled; } + + fn is_render_device(&self) -> bool { + true + } } struct XOutput { @@ -1055,10 +1059,6 @@ impl Connector for XOutput { Some(self.backend.drm_device_id) } - fn set_enabled(&self, _enabled: bool) { - // nothing - } - fn set_mode(&self, _mode: Mode) { log::warn!("X backend doesn't support changing the connector mode"); } diff --git a/src/cli.rs b/src/cli.rs index e6321cfa..1191a2b7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,6 +2,7 @@ mod generate; mod idle; mod log; mod quit; +mod randr; mod run_privileged; pub mod screenshot; mod seat_test; @@ -9,7 +10,7 @@ mod set_log_level; mod unlock; use { - crate::{compositor::start_compositor, portal}, + crate::{cli::randr::RandrArgs, compositor::start_compositor, portal}, ::log::Level, clap::{Args, Parser, Subcommand, ValueEnum}, clap_complete::Shell, @@ -55,6 +56,8 @@ pub enum Cmd { SeatTest(SeatTestArgs), /// Run the desktop portal. Portal, + /// Inspect/modify graphics card and connector settings. + Randr(RandrArgs), #[cfg(feature = "it")] RunTests, } @@ -217,6 +220,7 @@ pub fn main() { Cmd::RunPrivileged(a) => run_privileged::main(cli.global, a), Cmd::SeatTest(a) => seat_test::main(cli.global, a), Cmd::Portal => portal::run(cli.global), + Cmd::Randr(a) => randr::main(cli.global, a), #[cfg(feature = "it")] Cmd::RunTests => crate::it::run_tests(), } diff --git a/src/cli/randr.rs b/src/cli/randr.rs new file mode 100644 index 00000000..01509ecf --- /dev/null +++ b/src/cli/randr.rs @@ -0,0 +1,594 @@ +use { + crate::{ + cli::GlobalArgs, + scale::Scale, + tools::tool_client::{with_tool_client, Handle, ToolClient}, + utils::transform_ext::TransformExt, + wire::{jay_compositor, jay_randr, JayRandrId}, + }, + clap::{Args, Subcommand}, + isnt::std_1::vec::IsntVecExt, + jay_config::video::Transform, + std::{ + cell::RefCell, + fmt::{Display, Formatter}, + rc::Rc, + }, +}; + +#[derive(Args, Debug)] +pub struct RandrArgs { + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug)] +pub enum RandrCmd { + /// Show the current settings. + Show(ShowArgs), + /// Modify the settings of a graphics card. + Card(CardArgs), + /// Modify the settings of an output. + Output(OutputArgs), +} + +impl Default for RandrCmd { + fn default() -> Self { + Self::Show(Default::default()) + } +} + +#[derive(Args, Debug, Default)] +pub struct ShowArgs { + /// Show all available modes. + #[arg(long)] + pub modes: bool, +} + +#[derive(Args, Debug)] +pub struct CardArgs { + /// The card to modify, e.g. card0. + pub card: String, + #[clap(subcommand)] + pub command: CardCommand, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum CardCommand { + /// Make this device the primary device. + Primary, + /// Modify the graphics API used by the card. + Api(ApiArgs), + /// Modify the direct scanout setting of the card. + DirectScanout(DirectScanoutArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct ApiArgs { + #[clap(subcommand)] + pub cmd: ApiCmd, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum ApiCmd { + /// Use OpenGL for rendering in this card. + #[clap(name = "opengl")] + OpenGl, + /// Use Vulkan for rendering in this card. + #[clap(name = "vulkan")] + Vulkan, +} + +#[derive(Args, Debug, Clone)] +pub struct DirectScanoutArgs { + #[clap(subcommand)] + pub cmd: DirectScanoutCmd, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum DirectScanoutCmd { + /// Enable direct scanout. + Enable, + /// Disable direct scanout. + Disable, +} + +#[derive(Args, Debug)] +pub struct OutputArgs { + /// The output to modify, e.g. DP-1. + pub output: String, + #[clap(subcommand)] + pub command: OutputCommand, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum OutputCommand { + /// Modify the transform of the output. + Transform(TransformArgs), + /// Modify the scale of the output. + Scale(ScaleArgs), + /// Modify the mode of the output. + Mode(ModeArgs), + /// Modify the position of the output. + Position(PositionArgs), + /// Enable the output. + Enable, + /// Disable the output. + Disable, +} + +#[derive(Args, Debug, Clone)] +pub struct PositionArgs { + /// The top-left x coordinate. + pub x: i32, + /// The top-left y coordinate. + pub y: i32, +} + +#[derive(Args, Debug, Clone)] +pub struct ModeArgs { + /// The width. + pub width: i32, + /// The height. + pub height: i32, + /// The refresh rate. + pub refresh_rate: f64, +} + +#[derive(Args, Debug, Clone)] +pub struct ScaleArgs { + /// The new scale. + pub scale: f64, +} + +#[derive(Args, Debug, Clone)] +pub struct TransformArgs { + #[clap(subcommand)] + pub command: TransformCmd, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum TransformCmd { + /// Apply no transformation. + None, + /// Rotate the content 90 degrees counter-clockwise. + #[clap(name = "rotate-90")] + Rotate90, + /// Rotate the content 180 degrees counter-clockwise. + #[clap(name = "rotate-180")] + Rotate180, + /// Rotate the content 270 degrees counter-clockwise. + #[clap(name = "rotate-270")] + Rotate270, + /// Flip the content around the vertical axis. + Flip, + /// Flip the content around the vertical axis, then rotate 90 degrees counter-clockwise. + #[clap(name = "flip-rotate-90")] + FlipRotate90, + /// Flip the content around the vertical axis, then rotate 180 degrees counter-clockwise. + #[clap(name = "flip-rotate-180")] + FlipRotate180, + /// Flip the content around the vertical axis, then rotate 270 degrees counter-clockwise. + #[clap(name = "flip-rotate-270")] + FlipRotate270, +} + +pub fn main(global: GlobalArgs, args: RandrArgs) { + with_tool_client(global.log_level.into(), |tc| async move { + let idle = Rc::new(Randr { tc: tc.clone() }); + idle.run(args).await; + }); +} + +#[derive(Clone, Debug)] +struct Device { + pub id: u64, + pub syspath: String, + pub devnode: String, + pub vendor: u32, + pub vendor_name: String, + pub model: u32, + pub model_name: String, + pub gfx_api: String, + pub render_device: bool, +} + +#[derive(Clone, Debug)] +struct Connector { + pub _id: u64, + pub drm_device: Option, + pub name: String, + pub enabled: bool, + pub output: Option, +} + +#[derive(Clone, Debug)] +struct Output { + pub scale: f64, + pub width: i32, + pub height: i32, + pub x: i32, + pub y: i32, + pub transform: Transform, + pub manufacturer: String, + pub product: String, + pub serial_number: String, + pub width_mm: i32, + pub height_mm: i32, + pub current_mode: Option, + pub modes: Vec, +} + +#[derive(Copy, Clone, Debug)] +struct Mode { + pub width: i32, + pub height: i32, + pub refresh_rate_millihz: u32, + pub current: bool, +} + +impl Mode { + fn refresh_rate(&self) -> f64 { + (self.refresh_rate_millihz as f64) / 1000.0 + } +} + +impl Display for Mode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} x {} @ {}", + self.width, + self.height, + self.refresh_rate(), + ) + } +} + +#[derive(Clone, Debug, Default)] +struct Data { + default_api: String, + drm_devices: Vec, + connectors: Vec, +} + +struct Randr { + tc: Rc, +} + +impl Randr { + async fn run(self: &Rc, args: RandrArgs) { + let tc = &self.tc; + let comp = tc.jay_compositor().await; + let randr = tc.id(); + tc.send(jay_compositor::GetRandr { + self_id: comp, + id: randr, + }); + match args.command.unwrap_or_default() { + RandrCmd::Show(args) => self.show(randr, args).await, + RandrCmd::Card(args) => self.card(randr, args).await, + RandrCmd::Output(args) => self.output(randr, args).await, + } + } + + fn handle_error(&self, randr: JayRandrId, f: F) { + jay_randr::Error::handle(&self.tc, randr, (), move |_, msg| { + f(msg.msg); + std::process::exit(1); + }); + } + + async fn output(self: &Rc, randr: JayRandrId, args: OutputArgs) { + let tc = &self.tc; + match args.command { + OutputCommand::Transform(t) => { + self.handle_error(randr, |msg| { + eprintln!("Could not modify the transform: {}", msg); + }); + let transform = match t.command { + TransformCmd::None => Transform::None, + TransformCmd::Rotate90 => Transform::Rotate90, + TransformCmd::Rotate180 => Transform::Rotate180, + TransformCmd::Rotate270 => Transform::Rotate270, + TransformCmd::Flip => Transform::Flip, + TransformCmd::FlipRotate90 => Transform::FlipRotate90, + TransformCmd::FlipRotate180 => Transform::FlipRotate180, + TransformCmd::FlipRotate270 => Transform::FlipRotate270, + }; + tc.send(jay_randr::SetTransform { + self_id: randr, + output: &args.output, + transform: transform.to_wl(), + }); + } + OutputCommand::Scale(t) => { + self.handle_error(randr, |msg| { + eprintln!("Could not modify the scale: {}", msg); + }); + let scale = Scale::from_f64(t.scale); + tc.send(jay_randr::SetScale { + self_id: randr, + output: &args.output, + scale: scale.0, + }); + } + OutputCommand::Mode(t) => { + let name = args.output.to_ascii_lowercase(); + let data = self.get(randr).await; + let Some(connector) = data + .connectors + .iter() + .find(|c| c.name.to_ascii_lowercase() == name) + else { + log::error!("Connector with name `{}` does not exist", args.output); + return; + }; + let Some(output) = &connector.output else { + log::error!("Connector {} is not connected", connector.name); + return; + }; + let Some(mode) = output.modes.iter().find(|m| { + m.width == t.width && m.height == t.height && m.refresh_rate() == t.refresh_rate + }) else { + log::error!( + "Output {} does not support this refresh rate", + connector.name + ); + return; + }; + self.handle_error(randr, |msg| { + eprintln!("Could not modify the mode: {}", msg); + }); + tc.send(jay_randr::SetMode { + self_id: randr, + output: &args.output, + width: mode.width, + height: mode.height, + refresh_rate_millihz: mode.refresh_rate_millihz, + }); + } + OutputCommand::Position(t) => { + self.handle_error(randr, |msg| { + eprintln!("Could not modify the position: {}", msg); + }); + tc.send(jay_randr::SetPosition { + self_id: randr, + output: &args.output, + x: t.x, + y: t.y, + }); + } + OutputCommand::Enable | OutputCommand::Disable => { + let (enable, name) = match args.command { + OutputCommand::Enable => (true, "enable"), + _ => (false, "disable"), + }; + self.handle_error(randr, move |msg| { + eprintln!("Could not {} the output: {}", name, msg); + }); + tc.send(jay_randr::SetEnabled { + self_id: randr, + output: &args.output, + enabled: enable as _, + }); + } + } + tc.round_trip().await; + } + + async fn card(self: &Rc, randr: JayRandrId, args: CardArgs) { + let tc = &self.tc; + match args.command { + CardCommand::Primary => { + self.handle_error(randr, |msg| { + eprintln!("Could not set the primary device: {}", msg); + }); + tc.send(jay_randr::MakeRenderDevice { + self_id: randr, + dev: &args.card, + }); + } + CardCommand::Api(api) => { + self.handle_error(randr, |msg| { + eprintln!("Could not set the API: {}", msg); + }); + let api = match &api.cmd { + ApiCmd::OpenGl => "opengl", + ApiCmd::Vulkan => "vulkan", + }; + tc.send(jay_randr::SetApi { + self_id: randr, + dev: &args.card, + api, + }); + } + CardCommand::DirectScanout(ds) => { + self.handle_error(randr, |msg| { + eprintln!("Could not modify direct-scanout behavior: {}", msg); + }); + tc.send(jay_randr::SetDirectScanout { + self_id: randr, + dev: &args.card, + enabled: match ds.cmd { + DirectScanoutCmd::Enable => 1, + DirectScanoutCmd::Disable => 0, + }, + }); + } + } + tc.round_trip().await; + } + + async fn show(self: &Rc, randr: JayRandrId, args: ShowArgs) { + let mut data = self.get(randr).await; + data.drm_devices.sort_by(|l, r| l.devnode.cmp(&r.devnode)); + if data.drm_devices.is_not_empty() { + println!("drm devices:"); + } + for dev in &data.drm_devices { + self.print_drm_device(dev); + println!(" connectors:"); + let mut connectors: Vec<_> = data + .connectors + .iter() + .filter(|c| c.drm_device == Some(dev.id)) + .collect(); + connectors.sort_by_key(|c| &c.name); + for c in connectors { + self.print_connector(c, args.modes); + } + } + { + let mut connectors: Vec<_> = data + .connectors + .iter() + .filter(|c| c.drm_device.is_none()) + .collect(); + if connectors.is_not_empty() { + connectors.sort_by_key(|c| &c.name); + println!("unbound connectors:"); + for c in connectors { + self.print_connector(c, args.modes); + } + } + } + } + + fn print_drm_device(&self, dev: &Device) { + println!(" {}:", dev.devnode); + println!(" model: {} {}", dev.vendor_name, dev.model_name); + println!(" pci-id: {:x}:{:x}", dev.vendor, dev.model); + println!(" syspath: {}", dev.syspath); + println!(" api: {}", dev.gfx_api); + if dev.render_device { + println!(" primary device"); + } + } + + fn print_connector(&self, connector: &Connector, modes: bool) { + println!(" {}:", connector.name); + let Some(o) = &connector.output else { + if !connector.enabled { + println!(" disabled"); + } else { + println!(" disconnected"); + } + return; + }; + println!(" product: {}", o.product); + println!(" manufacturer: {}", o.manufacturer); + println!(" serial number: {}", o.serial_number); + println!(" position: {} x {}", o.x, o.y); + println!(" logical size: {} x {}", o.width, o.height); + println!( + " physical size: {}mm x {}mm", + o.width_mm, o.height_mm + ); + if let Some(mode) = &o.current_mode { + print!(" mode: "); + self.print_mode(mode, false); + } + if o.scale != 1.0 { + println!(" scale: {}", o.scale); + } + if o.transform != Transform::None { + let name = match o.transform { + Transform::None => "none", + Transform::Rotate90 => "rotate-90", + Transform::Rotate180 => "rotate-180", + Transform::Rotate270 => "rotate-270", + Transform::Flip => "flip", + Transform::FlipRotate90 => "flip-rotate-90", + Transform::FlipRotate180 => "flip-rotate-180", + Transform::FlipRotate270 => "flip-rotate-270", + }; + println!(" transform: {}", name); + } + if o.modes.is_not_empty() && modes { + println!(" modes:"); + for mode in &o.modes { + print!(" "); + self.print_mode(mode, true); + } + } + } + + fn print_mode(&self, m: &Mode, print_current: bool) { + print!("{}", m); + if print_current && m.current { + print!(" (current)"); + } + println!(); + } + + async fn get(self: &Rc, randr: JayRandrId) -> Data { + let tc = &self.tc; + tc.send(jay_randr::Get { self_id: randr }); + let data = Rc::new(RefCell::new(Data::default())); + jay_randr::Global::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + data.default_api = msg.default_gfx_api.to_string(); + }); + jay_randr::DrmDevice::handle(tc, randr, data.clone(), |data, msg| { + data.borrow_mut().drm_devices.push(Device { + id: msg.id, + syspath: msg.syspath.to_string(), + devnode: msg.devnode.to_string(), + vendor: msg.vendor, + vendor_name: msg.vendor_name.to_string(), + model: msg.model, + model_name: msg.model_name.to_string(), + gfx_api: msg.gfx_api.to_string(), + render_device: msg.render_device != 0, + }); + }); + jay_randr::Connector::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + data.connectors.push(Connector { + _id: msg.id, + drm_device: (msg.drm_device != 0).then_some(msg.drm_device), + name: msg.name.to_string(), + enabled: msg.enabled != 0, + output: None, + }); + }); + jay_randr::Output::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + c.output = Some(Output { + scale: Scale(msg.scale).to_f64(), + width: msg.width, + height: msg.height, + x: msg.x, + y: msg.y, + transform: Transform::from_wl(msg.transform).unwrap(), + manufacturer: msg.manufacturer.to_string(), + product: msg.product.to_string(), + serial_number: msg.serial_number.to_string(), + width_mm: msg.width_mm, + height_mm: msg.height_mm, + modes: Default::default(), + current_mode: None, + }); + }); + jay_randr::Mode::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let o = c.output.as_mut().unwrap(); + let mode = Mode { + width: msg.width, + height: msg.height, + refresh_rate_millihz: msg.refresh_rate_millihz, + current: msg.current != 0, + }; + if mode.current { + o.current_mode = Some(mode); + } + o.modes.push(mode); + }); + tc.round_trip().await; + let x = data.borrow_mut().clone(); + x + } +} diff --git a/src/ifs.rs b/src/ifs.rs index 6039d639..e77d55c8 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -10,6 +10,7 @@ pub mod jay_idle; pub mod jay_log_file; pub mod jay_output; pub mod jay_pointer; +pub mod jay_randr; pub mod jay_render_ctx; pub mod jay_screencast; pub mod jay_screenshot; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 321bbecf..cdad0134 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -5,9 +5,9 @@ use { globals::{Global, GlobalName}, ifs::{ jay_idle::JayIdle, jay_log_file::JayLogFile, jay_output::JayOutput, - jay_pointer::JayPointer, jay_render_ctx::JayRenderCtx, jay_screencast::JayScreencast, - jay_screenshot::JayScreenshot, jay_seat_events::JaySeatEvents, - jay_workspace_watcher::JayWorkspaceWatcher, + jay_pointer::JayPointer, jay_randr::JayRandr, jay_render_ctx::JayRenderCtx, + jay_screencast::JayScreencast, jay_screenshot::JayScreenshot, + jay_seat_events::JaySeatEvents, jay_workspace_watcher::JayWorkspaceWatcher, }, leaks::Tracker, object::Object, @@ -308,6 +308,14 @@ impl JayCompositor { self.client.add_client_obj(&sc)?; Ok(()) } + + fn get_randr(&self, parser: MsgParser<'_, '_>) -> Result<(), JayCompositorError> { + let req: GetRandr = self.client.parse(self, parser)?; + let sc = Rc::new(JayRandr::new(req.id, &self.client)); + track!(self.client, sc); + self.client.add_client_obj(&sc)?; + Ok(()) + } } object_base! { @@ -329,6 +337,7 @@ object_base! { GET_RENDER_CTX => get_render_ctx, WATCH_WORKSPACES => watch_workspaces, CREATE_SCREENCAST => create_screencast, + GET_RANDR => get_randr, } impl Object for JayCompositor {} diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs new file mode 100644 index 00000000..24cb21a5 --- /dev/null +++ b/src/ifs/jay_randr.rs @@ -0,0 +1,291 @@ +use { + crate::{ + backend, + client::{Client, ClientError}, + compositor::MAX_EXTENTS, + leaks::Tracker, + object::Object, + scale::Scale, + state::{ConnectorData, DrmDevData, OutputData}, + utils::{ + buffd::{MsgParser, MsgParserError}, + gfx_api_ext::GfxApiExt, + transform_ext::TransformExt, + }, + wire::{jay_randr::*, JayRandrId}, + }, + jay_config::video::{GfxApi, Transform}, + std::rc::Rc, + thiserror::Error, +}; + +pub struct JayRandr { + pub id: JayRandrId, + pub client: Rc, + pub tracker: Tracker, +} + +impl JayRandr { + pub fn new(id: JayRandrId, client: &Rc) -> Self { + Self { + id, + client: client.clone(), + tracker: Default::default(), + } + } + + fn destroy(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let _req: Destroy = self.client.parse(self, parser)?; + self.client.remove_obj(self)?; + Ok(()) + } + + fn get(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let _req: Get = self.client.parse(self, parser)?; + let state = &self.client.state; + self.send_global(); + for dev in state.drm_devs.lock().values() { + self.send_drm_device(dev); + } + for connector in state.connectors.lock().values() { + self.send_connector(connector); + } + Ok(()) + } + + fn send_global(&self) { + self.client.event(Global { + self_id: self.id, + default_gfx_api: self.client.state.default_gfx_api.get().to_str(), + }) + } + + fn send_drm_device(&self, data: &DrmDevData) { + self.client.event(DrmDevice { + self_id: self.id, + id: data.dev.id().raw() as _, + syspath: data.syspath.as_deref().unwrap_or_default(), + vendor: data.pci_id.map(|p| p.vendor).unwrap_or_default(), + vendor_name: data.vendor.as_deref().unwrap_or_default(), + model: data.pci_id.map(|p| p.model).unwrap_or_default(), + model_name: data.model.as_deref().unwrap_or_default(), + devnode: data.devnode.as_deref().unwrap_or_default(), + gfx_api: data.dev.gtx_api().to_str(), + render_device: data.dev.is_render_device() as _, + }); + } + + fn send_connector(&self, data: &ConnectorData) { + self.client.event(Connector { + self_id: self.id, + id: data.connector.id().raw() as _, + drm_device: data + .drm_dev + .as_ref() + .map(|d| d.dev.id().raw() as _) + .unwrap_or_default(), + enabled: data.connector.enabled() as _, + name: &data.name, + }); + if let Some(output) = self.client.state.outputs.get(&data.connector.id()) { + let global = &output.node.global; + let pos = global.pos.get(); + self.client.event(Output { + self_id: self.id, + scale: global.preferred_scale.get().0, + width: pos.width(), + height: pos.height(), + x: pos.x1(), + y: pos.y1(), + transform: global.transform.get().to_wl(), + manufacturer: &output.monitor_info.manufacturer, + product: &output.monitor_info.product, + serial_number: &output.monitor_info.serial_number, + width_mm: global.width_mm, + height_mm: global.height_mm, + }); + let current_mode = global.mode.get(); + for mode in &global.modes { + self.client.event(Mode { + self_id: self.id, + width: mode.width, + height: mode.height, + refresh_rate_millihz: mode.refresh_rate_millihz, + current: (mode == ¤t_mode) as _, + }); + } + } + } + + fn send_error(&self, msg: &str) { + self.client.event(Error { + self_id: self.id, + msg, + }); + } + + fn get_device(&self, name: &str) -> Option> { + let mut candidates = vec![]; + for dev in self.client.state.drm_devs.lock().values() { + if let Some(node) = &dev.devnode { + if node.ends_with(name) { + candidates.push(dev.clone()); + } + } + } + if candidates.len() == 1 { + return Some(candidates[0].clone()); + } + if candidates.len() == 0 { + self.send_error(&format!("Found no device matching `{}`", name)); + } else { + self.send_error(&format!("The device suffix `{}` is ambiguous", name)); + } + None + } + + fn get_connector(&self, name: &str) -> Option> { + let namelc = name.to_ascii_lowercase(); + for c in self.client.state.connectors.lock().values() { + if c.name.to_ascii_lowercase() == namelc { + return Some(c.clone()); + } + } + self.send_error(&format!("Found no connector matching `{}`", name)); + None + } + + fn get_output(&self, name: &str) -> Option> { + let namelc = name.to_ascii_lowercase(); + for c in self.client.state.outputs.lock().values() { + if c.connector.name.to_ascii_lowercase() == namelc { + return Some(c.clone()); + } + } + if let Some(c) = self.get_connector(name) { + self.send_error(&format!("Connector {} is not connected", c.name)); + } + None + } + + fn set_api(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let req: SetApi = self.client.parse(self, parser)?; + let Some(dev) = self.get_device(req.dev) else { + return Ok(()); + }; + let Some(api) = GfxApi::from_str_lossy(req.api) else { + self.send_error(&format!("Unknown API `{}`", req.api)); + return Ok(()); + }; + dev.dev.set_gfx_api(api); + Ok(()) + } + + fn make_render_device(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let req: MakeRenderDevice = self.client.parse(self, parser)?; + let Some(dev) = self.get_device(req.dev) else { + return Ok(()); + }; + dev.make_render_device(); + Ok(()) + } + + fn set_direct_scanout(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let req: SetDirectScanout = self.client.parse(self, parser)?; + let Some(dev) = self.get_device(req.dev) else { + return Ok(()); + }; + dev.dev.set_direct_scanout_enabled(req.enabled != 0); + Ok(()) + } + + fn set_transform(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let req: SetTransform = self.client.parse(self, parser)?; + let Some(c) = self.get_output(req.output) else { + return Ok(()); + }; + let Some(transform) = Transform::from_wl(req.transform) else { + self.send_error(&format!("Unknown transform {}", req.transform)); + return Ok(()); + }; + c.node.update_transform(transform); + Ok(()) + } + + fn set_scale(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let req: SetScale = self.client.parse(self, parser)?; + let Some(c) = self.get_output(req.output) else { + return Ok(()); + }; + c.node.set_preferred_scale(Scale(req.scale)); + Ok(()) + } + + fn set_mode(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let req: SetMode = self.client.parse(self, parser)?; + let Some(c) = self.get_output(req.output) else { + return Ok(()); + }; + c.connector.connector.set_mode(backend::Mode { + width: req.width, + height: req.height, + refresh_rate_millihz: req.refresh_rate_millihz, + }); + Ok(()) + } + + fn set_position(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let req: SetPosition = self.client.parse(self, parser)?; + let Some(c) = self.get_output(req.output) else { + return Ok(()); + }; + if req.x < 0 || req.y < 0 { + self.send_error("x and y cannot be less than 0"); + return Ok(()); + } + if req.x > MAX_EXTENTS || req.y > MAX_EXTENTS { + self.send_error(&format!("x and y cannot be greater than {MAX_EXTENTS}")); + return Ok(()); + } + c.node.set_position(req.x, req.y); + Ok(()) + } + + fn set_enabled(&self, parser: MsgParser<'_, '_>) -> Result<(), JayRandrError> { + let req: SetEnabled = self.client.parse(self, parser)?; + let Some(c) = self.get_connector(req.output) else { + return Ok(()); + }; + c.connector.set_enabled(req.enabled != 0); + Ok(()) + } +} + +object_base! { + self = JayRandr; + + DESTROY => destroy, + GET => get, + SET_API => set_api, + MAKE_RENDER_DEVICE => make_render_device, + SET_DIRECT_SCANOUT => set_direct_scanout, + SET_TRANSFORM => set_transform, + SET_SCALE => set_scale, + SET_MODE => set_mode, + SET_POSITION => set_position, + SET_ENABLED => set_enabled, +} + +impl Object for JayRandr {} + +simple_add_obj!(JayRandr); + +#[derive(Debug, Error)] +pub enum JayRandrError { + #[error("Parsing failed")] + MsgParserError(Box), + #[error(transparent)] + ClientError(Box), +} +efrom!(JayRandrError, MsgParserError); +efrom!(JayRandrError, ClientError); diff --git a/src/it/test_backend.rs b/src/it/test_backend.rs index 2de308a3..b60d380c 100644 --- a/src/it/test_backend.rs +++ b/src/it/test_backend.rs @@ -243,10 +243,6 @@ impl Connector for TestConnector { None } - fn set_enabled(&self, _enabled: bool) { - // todo - } - fn set_mode(&self, _mode: Mode) { // todo } diff --git a/src/utils.rs b/src/utils.rs index 68f72d1d..b6ac70e6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,6 +13,7 @@ pub mod debug_fn; pub mod double_click_state; pub mod errorfmt; pub mod fdcloser; +pub mod gfx_api_ext; pub mod hex; pub mod linkedlist; pub mod log_on_drop; diff --git a/src/utils/gfx_api_ext.rs b/src/utils/gfx_api_ext.rs new file mode 100644 index 00000000..4dd7fc0d --- /dev/null +++ b/src/utils/gfx_api_ext.rs @@ -0,0 +1,25 @@ +use jay_config::video::GfxApi; + +pub trait GfxApiExt: Sized { + fn to_str(&self) -> &'static str; + + fn from_str_lossy(s: &str) -> Option; +} + +impl GfxApiExt for GfxApi { + fn to_str(&self) -> &'static str { + match self { + GfxApi::OpenGl => "OpenGl", + GfxApi::Vulkan => "Vulkan", + _ => "unknown", + } + } + + fn from_str_lossy(s: &str) -> Option { + match &*s.to_ascii_lowercase() { + "opengl" => Some(Self::OpenGl), + "vulkan" => Some(Self::Vulkan), + _ => None, + } + } +} diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index f51335be..ac69231f 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -65,6 +65,10 @@ msg create_screencast = 15 { id: id(jay_screencast), } +msg get_randr = 16 { + id: id(jay_randr), +} + # events msg client_id = 0 { diff --git a/wire/jay_randr.txt b/wire/jay_randr.txt new file mode 100644 index 00000000..e51295b6 --- /dev/null +++ b/wire/jay_randr.txt @@ -0,0 +1,101 @@ +# requests + +msg destroy = 0 { + +} + +msg get = 1 { + +} + +msg set_api = 2 { + dev: str, + api: str, +} + +msg make_render_device = 3 { + dev: str, +} + +msg set_direct_scanout = 4 { + dev: str, + enabled: u32, +} + +msg set_transform = 5 { + output: str, + transform: i32, +} + +msg set_scale = 6 { + output: str, + scale: u32, +} + +msg set_mode = 7 { + output: str, + width: i32, + height: i32, + refresh_rate_millihz: u32, +} + +msg set_position = 8 { + output: str, + x: i32, + y: i32, +} + +msg set_enabled = 9 { + output: str, + enabled: u32, +} + +# events + +msg global = 0 { + default_gfx_api: str, +} + +msg drm_device = 1 { + id: pod(u64), + syspath: str, + vendor: u32, + vendor_name: str, + model: u32, + model_name: str, + devnode: str, + gfx_api: str, + render_device: u32, +} + +msg connector = 2 { + id: pod(u64), + drm_device: pod(u64), + name: str, + enabled: u32, +} + +msg output = 3 { + scale: u32, + width: i32, + height: i32, + x: i32, + y: i32, + transform: i32, + manufacturer: str, + product: str, + serial_number: str, + width_mm: i32, + height_mm: i32, +} + +msg mode = 4 { + width: i32, + height: i32, + refresh_rate_millihz: u32, + current: u32, +} + +msg error = 5 { + msg: str, +}