diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 7a46f9d1..bd4989d2 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1236,6 +1236,14 @@ impl ConfigClient { self.send(&ClientMessage::SetTearingMode { connector, mode }) } + pub fn create_virtual_output(&self, name: &str) { + self.send(&ClientMessage::CreateVirtualOutput { name }) + } + + pub fn remove_virtual_output(&self, name: &str) { + self.send(&ClientMessage::RemoveVirtualOutput { name }) + } + pub fn drm_devices(&self) -> Vec { let res = self.send_with_response(&ClientMessage::GetDrmDevices); get_response!(res, vec![], GetDrmDevices { devices }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index d002f7b7..29c90cf2 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -852,6 +852,12 @@ pub enum ClientMessage<'a> { GetConnectorByName { name: &'a str, }, + CreateVirtualOutput { + name: &'a str, + }, + RemoveVirtualOutput { + name: &'a str, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/video.rs b/jay-config/src/video.rs index 9dcbea0e..ff0680eb 100644 --- a/jay-config/src/video.rs +++ b/jay-config/src/video.rs @@ -539,6 +539,7 @@ pub mod connector_type { pub const CON_SPI: ConnectorType = ConnectorType(19); pub const CON_USB: ConnectorType = ConnectorType(20); pub const CON_EMBEDDED_WINDOW: ConnectorType = ConnectorType(u32::MAX); + pub const CON_VIRTUAL_OUTPUT: ConnectorType = ConnectorType(u32::MAX - 1); } /// A *Direct Rendering Manager* (DRM) device. @@ -730,6 +731,25 @@ pub fn set_tearing_mode(mode: TearingMode) { get!().set_tearing_mode(None, mode) } +/// Creates a virtual output with the given name. +/// +/// This is a no-op if a virtual output with that name already exists. +/// +/// The created connector can be accessed with [`get_connector_by_name("VO-{name}")`]. +/// +/// A newly created connector is initially disabled. When a connector is destroyed and +/// later recreated, its previous state is restored. +pub fn create_virtual_output(name: &str) { + get!().create_virtual_output(name); +} + +/// Removes the virtual output with the given name. +/// +/// This is a no-op if a virtual output with that name does not exist. +pub fn remove_virtual_output(name: &str) { + get!().remove_virtual_output(name); +} + /// A graphics format. #[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct Format(pub u32); diff --git a/src/backend.rs b/src/backend.rs index 44cf6fe6..5a2296be 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -84,6 +84,10 @@ impl Mode { n => 1_000_000_000_000 / (n as u64), } } + + pub fn size(&self) -> (i32, i32) { + (self.width, self.height) + } } impl Display for Mode { diff --git a/src/backend/transaction.rs b/src/backend/transaction.rs index 790f6f7b..ebbafd26 100644 --- a/src/backend/transaction.rs +++ b/src/backend/transaction.rs @@ -4,7 +4,6 @@ use { BackendColorSpace, BackendConnectorState, BackendEotfs, Connector, ConnectorId, ConnectorKernelId, Mode, }, - backends::metal::MetalError, state::State, utils::{errorfmt::ErrorFmt, hash_map_ext::HashMapExt}, video::drm::DrmError, @@ -14,6 +13,7 @@ use { any::{Any, TypeId}, cell::{Cell, RefCell}, collections::hash_map::Entry, + error::Error, hash::{Hash, Hasher}, rc::Rc, }, @@ -119,13 +119,17 @@ pub enum BackendConnectorTransactionError { #[error("Could not create a mode blob")] CreateModeBlob(#[source] DrmError), #[error("Could not allocate buffers for connector {}", .0)] - AllocateScanoutBuffers(ConnectorKernelId, #[source] Box), + AllocateScanoutBuffers(ConnectorKernelId, #[source] Box), #[error("Test commit failed")] AtomicTestFailed(#[source] DrmError), #[error("Commit failed")] AtomicCommitFailed(#[source] DrmError), #[error("Could not create a gamma lut blob")] CreateGammaLutBlob(#[source] DrmError), + #[error("Connector {} does not support gamma lut", .0)] + GammaLutNotSupported(ConnectorKernelId), + #[error("There is no render context")] + NoRenderContext, } pub trait BackendConnectorTransaction { diff --git a/src/cli/randr.rs b/src/cli/randr.rs index d9b14112..ca076a94 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -42,6 +42,8 @@ pub enum RandrCmd { Card(CardArgs), /// Modify the settings of an output. Output(OutputArgs), + /// Modify virtual outputs. + VirtualOutput(VirtualOutputArgs), } impl Default for RandrCmd { @@ -465,6 +467,32 @@ fn blend_space_possible_values() -> Vec { res } +#[derive(Args, Debug)] +pub struct VirtualOutputArgs { + #[clap(subcommand)] + pub command: VirtualOutputCommand, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum VirtualOutputCommand { + /// Create a virtual output. + Create(CreateVirtualOutputArgs), + /// Remove a virtual output. + Remove(RemoveVirtualOutputArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct CreateVirtualOutputArgs { + /// The name of the virtual output. + pub name: String, +} + +#[derive(Args, Debug, Clone)] +pub struct RemoveVirtualOutputArgs { + /// The name of the virtual output. + pub name: String, +} + pub fn main(global: GlobalArgs, args: RandrArgs) { with_tool_client(global.log_level, |tc| async move { let idle = Rc::new(Randr { tc: tc.clone() }); @@ -580,6 +608,7 @@ impl Randr { RandrCmd::Show(args) => self.show(randr, args).await, RandrCmd::Card(args) => self.card(randr, args).await, RandrCmd::Output(args) => self.output(randr, args).await, + RandrCmd::VirtualOutput(args) => self.virtual_output(randr, args).await, } } @@ -848,6 +877,31 @@ impl Randr { tc.round_trip().await; } + async fn virtual_output(self: &Rc, randr: JayRandrId, args: VirtualOutputArgs) { + let tc = &self.tc; + match args.command { + VirtualOutputCommand::Create(t) => { + self.handle_error(randr, |msg| { + eprintln!("Could not create a virtual output: {}", msg); + }); + tc.send(jay_randr::CreateVirtualOutput { + self_id: randr, + name: &t.name, + }); + } + VirtualOutputCommand::Remove(t) => { + self.handle_error(randr, |msg| { + eprintln!("Could not remove a virtual output: {}", msg); + }); + tc.send(jay_randr::RemoveVirtualOutput { + self_id: randr, + name: &t.name, + }); + } + } + tc.round_trip().await; + } + async fn card(self: &Rc, randr: JayRandrId, args: CardArgs) { let tc = &self.tc; match args.command { diff --git a/src/compositor.rs b/src/compositor.rs index c48af940..01940220 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -395,6 +395,7 @@ fn start_compositor2( bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), egg_state: Default::default(), control_centers: Default::default(), + virtual_outputs: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); diff --git a/src/config/handler.rs b/src/config/handler.rs index 73cf2be9..2dd975da 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1605,6 +1605,14 @@ impl ConfigProxyHandler { self.respond(Response::GetConnector { connector }); } + fn handle_create_virtual_output(&self, name: &str) { + self.state.virtual_outputs.get_or_create(&self.state, name); + } + + fn handle_remove_virtual_output(&self, name: &str) { + self.state.virtual_outputs.remove_output(&self.state, name); + } + fn handle_get_connector_active_workspace(&self, connector: Connector) -> Result<(), CphError> { let output = self.get_output_node(connector)?; let workspace = output @@ -3357,6 +3365,8 @@ impl ConfigProxyHandler { .handle_connector_supports_arbitrary_modes(connector) .wrn("connector_supports_arbitrary_modes")?, ClientMessage::GetConnectorByName { name } => self.handle_get_connector_by_name(name), + ClientMessage::CreateVirtualOutput { name } => self.handle_create_virtual_output(name), + ClientMessage::RemoveVirtualOutput { name } => self.handle_remove_virtual_output(name), } Ok(()) } diff --git a/src/control_center.rs b/src/control_center.rs index 203146b6..30c7c7d2 100644 --- a/src/control_center.rs +++ b/src/control_center.rs @@ -9,6 +9,7 @@ use { cc_input::InputPane, cc_look_and_feel::LookAndFeelPane, cc_outputs::OutputsPane, + cc_virtual_outputs::VirtualOutputsPane, cc_window::{WindowPane, WindowSearchPane}, cc_xwayland::XwaylandPane, }, @@ -51,6 +52,7 @@ mod cc_input; mod cc_look_and_feel; mod cc_outputs; mod cc_sidebar; +mod cc_virtual_outputs; mod cc_window; mod cc_xwayland; @@ -93,6 +95,7 @@ bitflags! { CCI_GPUS, CCI_INPUT, CCI_LOOK_AND_FEEL, + CCI_VIRTUAL_OUTPUTS, } pub struct ControlCenter { @@ -145,6 +148,7 @@ enum PaneType { Client(ClientPane), WindowSearch(WindowSearchPane), Window(WindowPane), + VirtualOutputs(VirtualOutputsPane), } struct CcBehavior<'a> { @@ -174,6 +178,7 @@ impl Pane { PaneType::Client(v) => v.title(res), PaneType::WindowSearch(v) => v.title(res), PaneType::Window(v) => v.title(res), + PaneType::VirtualOutputs(v) => v.title(res), } } @@ -191,6 +196,7 @@ impl Pane { PaneType::Client(p) => p.show(behavior, ui), PaneType::WindowSearch(p) => p.show(behavior, ui), PaneType::Window(p) => p.show(behavior, ui), + PaneType::VirtualOutputs(p) => p.show(ui), } } } @@ -210,6 +216,7 @@ impl PaneType { PaneType::Client(_) => ControlCenterInterest::none(), PaneType::WindowSearch(_) => ControlCenterInterest::none(), PaneType::Window(_) => ControlCenterInterest::none(), + PaneType::VirtualOutputs(_) => CCI_VIRTUAL_OUTPUTS, } } } diff --git a/src/control_center/cc_sidebar.rs b/src/control_center/cc_sidebar.rs index 23f2116b..e7dc18e9 100644 --- a/src/control_center/cc_sidebar.rs +++ b/src/control_center/cc_sidebar.rs @@ -18,6 +18,7 @@ enum PaneName { LookAndFeel, Clients, WindowSearch, + VirtualOutputs, } impl PaneName { @@ -33,6 +34,7 @@ impl PaneName { PaneName::LookAndFeel => "Look and Feel", PaneName::Clients => "Clients", PaneName::WindowSearch => "Window Search", + PaneName::VirtualOutputs => "Virtual Outputs", } } } @@ -79,6 +81,9 @@ impl ControlCenterInner { PaneName::WindowSearch => { PaneType::WindowSearch(self.create_window_search_pane()) } + PaneName::VirtualOutputs => { + PaneType::VirtualOutputs(self.create_virtual_outputs_pane()) + } }; self.open(tree, ty); ui.ctx().request_repaint(); diff --git a/src/control_center/cc_virtual_outputs.rs b/src/control_center/cc_virtual_outputs.rs new file mode 100644 index 00000000..c45e8ae4 --- /dev/null +++ b/src/control_center/cc_virtual_outputs.rs @@ -0,0 +1,49 @@ +use { + crate::{ + control_center::ControlCenterInner, egui_adapter::egui_platform::icons::ICON_CLOSE, + state::State, + }, + egui::Ui, + std::rc::Rc, +}; + +pub struct VirtualOutputsPane { + state: Rc, + new: String, +} + +impl ControlCenterInner { + pub fn create_virtual_outputs_pane(self: &Rc) -> VirtualOutputsPane { + VirtualOutputsPane { + state: self.state.clone(), + new: Default::default(), + } + } +} + +impl VirtualOutputsPane { + pub fn title(&self, res: &mut String) { + res.push_str("Virtual Outputs"); + } + + pub fn show(&mut self, ui: &mut Ui) { + let s = &self.state; + let mut outputs: Vec<_> = s.virtual_outputs.outputs.lock().keys().cloned().collect(); + outputs.sort(); + for o in &outputs { + ui.horizontal(|ui| { + if ui.button(ICON_CLOSE).clicked() { + s.virtual_outputs.remove_output(s, o); + } + ui.label(o); + }); + } + ui.horizontal(|ui| { + ui.text_edit_singleline(&mut self.new); + if ui.button("Add").clicked() { + s.virtual_outputs.get_or_create(s, &self.new); + ui.ctx().request_repaint(); + } + }); + } +} diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 3e4b9ca3..c406b87c 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -78,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError); impl Global for JayCompositorGlobal { fn version(&self) -> u32 { - 29 + 30 } fn required_caps(&self) -> ClientCaps { diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs index c04bee1a..6b79475d 100644 --- a/src/ifs/jay_randr.rs +++ b/src/ifs/jay_randr.rs @@ -270,6 +270,11 @@ impl JayRandr { } fn get_connector(&self, name: &str) -> Option> { + for c in self.client.state.connectors.lock().values() { + if *c.name == name { + return Some(c.clone()); + } + } let namelc = name.to_ascii_lowercase(); for c in self.client.state.connectors.lock().values() { if c.name.to_ascii_lowercase() == namelc { @@ -281,6 +286,11 @@ impl JayRandr { } fn get_output(&self, name: &str) -> Option> { + for c in self.client.state.outputs.lock().values() { + if *c.connector.name == name { + return Some(c.clone()); + } + } let namelc = name.to_ascii_lowercase(); for c in self.client.state.outputs.lock().values() { if c.connector.name.to_ascii_lowercase() == namelc { @@ -588,6 +598,28 @@ impl JayRandrRequestHandler for JayRandr { c.set_use_native_gamut(req.use_native_gamut != 0); Ok(()) } + + fn create_virtual_output( + &self, + req: CreateVirtualOutput<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.state + .virtual_outputs + .get_or_create(&self.state, req.name); + Ok(()) + } + + fn remove_virtual_output( + &self, + req: RemoveVirtualOutput<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.state + .virtual_outputs + .remove_output(&self.state, req.name); + Ok(()) + } } object_base! { diff --git a/src/ifs/wp_presentation_feedback.rs b/src/ifs/wp_presentation_feedback.rs index 5a481120..ca468891 100644 --- a/src/ifs/wp_presentation_feedback.rs +++ b/src/ifs/wp_presentation_feedback.rs @@ -62,7 +62,6 @@ pub struct WpPresentationFeedback { } pub const KIND_VSYNC: u32 = 0x1; -#[expect(dead_code)] pub const KIND_HW_CLOCK: u32 = 0x2; pub const KIND_HW_COMPLETION: u32 = 0x4; pub const KIND_ZERO_COPY: u32 = 0x8; diff --git a/src/main.rs b/src/main.rs index 17b1662b..ed4c92ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,6 +114,7 @@ mod user_session; mod utils; mod version; mod video; +mod virtual_output; mod vulkan_core; mod wheel; mod wire; diff --git a/src/state.rs b/src/state.rs index 04fb9cbd..d80129a6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -131,6 +131,7 @@ use { dmabuf::DmaBufIds, drm::{Drm, wait_for_syncobj::WaitForSyncobj}, }, + virtual_output::VirtualOutputs, wheel::Wheel, wire::{ ExtForeignToplevelListV1Id, ExtIdleNotificationV1Id, JayHeadManagerSessionV1Id, @@ -302,6 +303,7 @@ pub struct State { pub bo_drop_queue: Rc>>, pub egg_state: EggState, pub control_centers: ControlCenters, + pub virtual_outputs: VirtualOutputs, } // impl Drop for State { @@ -674,6 +676,7 @@ impl State { self.icons.clear(); self.wait_for_syncobj .set_ctx(ctx.as_ref().and_then(|c| c.syncobj_ctx().cloned())); + self.virtual_outputs.handle_render_ctx_change(self); 'handle_new_feedback: { if let Some(ctx) = &ctx { @@ -1184,6 +1187,7 @@ impl State { self.bo_drop_queue.kill(); self.egg_state.clear(); self.control_centers.clear(); + self.virtual_outputs.clear(); } pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { diff --git a/src/tasks.rs b/src/tasks.rs index 2f6da5c2..ba9fa14e 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -19,7 +19,9 @@ use { }, std::{rc::Rc, time::Duration}, }; -pub use {hardware_cursor::handle_hardware_cursor_tick, idle::idle}; +pub use { + connector::handle as handle_connector, hardware_cursor::handle_hardware_cursor_tick, idle::idle, +}; pub async fn handle_backend_events(state: Rc) { let mut beh = BackendEventHandler { state }; diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index aba85d42..7d60be36 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -334,7 +334,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(29), + version: s.jay_compositor.1.min(30), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/src/video/drm.rs b/src/video/drm.rs index 0ce27257..301bc24a 100644 --- a/src/video/drm.rs +++ b/src/video/drm.rs @@ -1146,6 +1146,7 @@ pub enum ConnectorType { SPI, USB, EmbeddedWindow, + VirtualOutput, } impl ConnectorType { @@ -1200,6 +1201,7 @@ impl ConnectorType { Self::SPI => sys::DRM_MODE_CONNECTOR_SPI, Self::USB => sys::DRM_MODE_CONNECTOR_USB, Self::EmbeddedWindow => sys::DRM_MODE_CONNECTOR_Unknown, + Self::VirtualOutput => sys::DRM_MODE_CONNECTOR_Unknown, } } @@ -1228,6 +1230,7 @@ impl ConnectorType { Self::SPI => CON_SPI, Self::USB => CON_USB, Self::EmbeddedWindow => CON_EMBEDDED_WINDOW, + Self::VirtualOutput => CON_VIRTUAL_OUTPUT, } } } @@ -1257,6 +1260,7 @@ impl Display for ConnectorType { Self::SPI => "SPI", Self::USB => "USB", Self::EmbeddedWindow => "EmbeddedWindow", + Self::VirtualOutput => "VO", }; f.write_str(s) } diff --git a/src/virtual_output.rs b/src/virtual_output.rs new file mode 100644 index 00000000..5ff0fda2 --- /dev/null +++ b/src/virtual_output.rs @@ -0,0 +1,1105 @@ +use { + crate::{ + allocator::{AllocatorError, BO_USE_RENDERING, BufferObject, BufferUsage}, + async_engine::{Phase, SpawnedFuture}, + backend::{ + BackendConnectorState, Connector, ConnectorEvent, ConnectorId, ConnectorKernelId, + DrmDeviceId, HardwareCursor, HardwareCursorUpdate, Mode, MonitorInfo, + transaction::{ + BackendAppliedConnectorTransaction, BackendConnectorTransaction, + BackendConnectorTransactionError, BackendConnectorTransactionType, + BackendConnectorTransactionTypeDyn, BackendPreparedConnectorTransaction, + }, + }, + cmm::{cmm_description::ColorDescription, cmm_primaries::Primaries}, + control_center::CCI_VIRTUAL_OUTPUTS, + format::{Format, XRGB8888}, + gfx_api::{ + AcquireSync, BufferResv, DirectScanoutPosition, FdSync, GfxBlendBuffer, GfxContext, + GfxError, GfxFramebuffer, GfxRenderPass, GfxTexture, ReleaseSync, create_render_pass, + }, + ifs::{ + wl_output::{BlendSpace, OutputId}, + wp_presentation_feedback::{ + KIND_HW_CLOCK, KIND_HW_COMPLETION, KIND_VSYNC, KIND_ZERO_COPY, + }, + }, + rect::Region, + state::State, + tasks::handle_connector, + tree::OutputNode, + utils::{ + asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, + copyhashmap::CopyHashMap, errorfmt::ErrorFmt, geometric_decay::GeometricDecay, + hash_map_ext::HashMapExt, numcell::NumCell, on_change::OnChange, rc_eq::rc_eq, + timer::TimerFd, + }, + video::drm::ConnectorType, + }, + ahash::AHashMap, + linearize::{Linearize, LinearizeExt, StaticMap, static_map}, + std::{ + any::Any, + cell::{Cell, RefCell}, + fmt::{Debug, Formatter}, + mem, + rc::Rc, + time::Duration, + }, + thiserror::Error, + uapi::c, +}; + +#[derive(Default)] +pub struct VirtualOutputs { + pub outputs: CopyHashMap>, + formats: CloneCell>>, + states: CopyHashMap>, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +enum FrontendState { + #[default] + Disconnected, + Desktop, + NonDesktop, +} + +pub struct VirtualOutput { + state: Rc, + id: ConnectorId, + kernel_id: ConnectorKernelId, + output_id: Rc, + name: String, + frontend_state: Cell, + needs_format_update: Cell, + events: OnChange, + damage: NumCell, + present_trigger: AsyncEvent, + persistent_state: Rc, + vo_state: CloneCell>, + tasks: Cell; 2]>>, + flip_task: Cell>>, + next_vblank_nsec: Cell, + pre_commit_margin: Cell, + pre_commit_margin_decay: GeometricDecay, + need_vblank: AsyncEvent, + seq: NumCell, + pending_flip: Cell>, + trigger_flip: AsyncEvent, + cursor_damage: Cell, + cursor_programming: Cell>, + frame_data: RefCell>, +} + +struct PersistentVirtualOutputState { + backend_state: RefCell, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct CursorProgramming { + x: i32, + y: i32, +} + +struct ScheduledFlip { + on: Rc, + refresh_ns: u64, + vrr: bool, + tearing: bool, + expected_seq: Option, + locked: bool, + frame_data: Option>, +} + +#[derive(Default, Clone)] +struct VoState { + fbs: Option>, + locked: Cell, +} + +#[derive(Copy, Clone, Linearize, Eq, PartialEq)] +enum FbType { + Primary, + Cursor, +} + +struct FbState { + ctx: Rc, + format: &'static Format, + blend_buffer: Option>, + fbs: StaticMap, +} + +struct VoFb { + width: i32, + height: i32, + _bo: Rc, + tex: Rc, + fb: Rc, +} + +struct Transaction { + state: Rc, + changes: AHashMap, +} + +struct TransactionChange { + output: Rc, + new: BackendConnectorState, +} + +struct PreparedTransaction { + state: Rc, + changes: Vec, +} + +struct PreparedTransactionChange { + output: Rc, + old_backend_state: BackendConnectorState, + old_vo_state: Rc, + new_backend_state: BackendConnectorState, + new_vo_state: Rc, +} + +struct Latched { + pass: GfxRenderPass, + damage_count: u64, + damage: Region, + locked: bool, +} + +struct CursorChange<'a> { + swap_buffer: Option>, + enabled: bool, + x: i32, + y: i32, + buffer: &'a VoFb, +} + +struct DirectScanoutData { + buffer_resv: Option>, + tex: Rc, + acquire_sync: AcquireSync, + release_sync: ReleaseSync, + pos: DirectScanoutPosition, +} + +struct FrameData { + dsd: Option, +} + +const CURSOR_SIZE: i32 = 256; + +impl HardwareCursorUpdate for CursorChange<'_> { + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn get_buffer(&self) -> Rc { + self.buffer.fb.clone() + } + + fn set_position(&mut self, x: i32, y: i32) { + self.x = x; + self.y = y; + } + + fn swap_buffer(&mut self, sync: Option) { + self.swap_buffer = Some(sync); + } + + fn size(&self) -> (i32, i32) { + (CURSOR_SIZE, CURSOR_SIZE) + } +} + +fn default_state(state: &State) -> BackendConnectorState { + BackendConnectorState { + serial: state.backend_connector_state_serials.next(), + enabled: false, + active: true, + mode: Mode { + width: 800, + height: 600, + refresh_rate_millihz: 60_000, + }, + non_desktop_override: Default::default(), + vrr: Default::default(), + tearing: Default::default(), + format: XRGB8888, + color_space: Default::default(), + eotf: Default::default(), + gamma_lut: Default::default(), + } +} + +impl VirtualOutputs { + pub fn get_or_create(&self, state: &Rc, name: &str) -> Rc { + if let Some(vo) = self.outputs.get(name) { + return vo; + } + let id = state.connector_ids.next(); + let kernel_id = ConnectorKernelId { + ty: ConnectorType::VirtualOutput, + idx: id.raw(), + }; + let persistent_state = match self.states.get(name) { + Some(s) => s, + _ => { + let state = Rc::new(PersistentVirtualOutputState { + backend_state: RefCell::new(default_state(state)), + }); + self.states.set(name.to_string(), state.clone()); + state + } + }; + let vo = Rc::new(VirtualOutput { + state: state.clone(), + id, + kernel_id, + output_id: Rc::new(OutputId::new( + kernel_id.to_string(), + "Jay".to_string(), + "VirtualOutput".to_string(), + name.to_string(), + )), + name: format!("VO-{}", name), + frontend_state: Default::default(), + needs_format_update: Default::default(), + events: Default::default(), + damage: Default::default(), + present_trigger: Default::default(), + persistent_state, + vo_state: Default::default(), + tasks: Default::default(), + flip_task: Default::default(), + next_vblank_nsec: Default::default(), + pre_commit_margin: Cell::new(PRE_COMMIT_MARGIN), + pre_commit_margin_decay: GeometricDecay::new(0.5, PRE_COMMIT_MARGIN), + need_vblank: Default::default(), + seq: Default::default(), + pending_flip: Default::default(), + trigger_flip: Default::default(), + cursor_damage: Default::default(), + cursor_programming: Default::default(), + frame_data: Default::default(), + }); + vo.handle_render_ctx_change(); + handle_connector(state, &(vo.clone() as Rc)); + self.outputs.set(name.to_string(), vo.clone()); + vo.flip_task + .set(Some(state.eng.spawn("vo-flip", vo.clone().flip_task()))); + state.trigger_cci(CCI_VIRTUAL_OUTPUTS); + vo + } + + pub fn remove_output(&self, state: &Rc, name: &str) { + let Some(o) = self.outputs.remove(name) else { + return; + }; + o.clear(); + o.events.send_event(ConnectorEvent::Disconnected); + o.events.send_event(ConnectorEvent::Removed); + state.trigger_cci(CCI_VIRTUAL_OUTPUTS); + } + + pub fn clear(&self) { + for o in self.outputs.lock().drain_values() { + o.clear(); + o.events.clear(); + } + } + + pub fn handle_render_ctx_change(&self, state: &State) { + let formats = match state.render_ctx.get() { + None => vec![], + Some(c) => c.formats().values().map(|f| f.format).collect(), + }; + self.formats.set(Rc::new(formats)); + for o in self.outputs.lock().values() { + o.handle_render_ctx_change(); + } + } +} + +impl Connector for VirtualOutput { + fn id(&self) -> ConnectorId { + self.id + } + + fn kernel_id(&self) -> ConnectorKernelId { + self.kernel_id + } + + fn event(&self) -> Option { + self.events.events.pop() + } + + fn on_change(&self, cb: Rc) { + self.events.on_change.set(Some(cb)); + } + + fn damage(&self) { + self.damage.fetch_add(1); + self.trigger_present(); + } + + fn drm_dev(&self) -> Option { + None + } + + fn effectively_locked(&self) -> bool { + self.vo_state.get().locked.get() + } + + fn state(&self) -> BackendConnectorState { + self.persistent_state.backend_state.borrow().clone() + } + + fn transaction_type(&self) -> Box { + #[derive(Eq, PartialEq, Hash)] + struct TT; + impl BackendConnectorTransactionType for TT {} + Box::new(TT) + } + + fn create_transaction( + &self, + ) -> Result, BackendConnectorTransactionError> { + Ok(Box::new(self.create_transaction())) + } + + fn name(&self) -> String { + self.name.clone() + } +} + +struct VirtualHc { + o: Rc, +} + +impl Debug for VirtualHc { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VirtualOutput") + .field("id", &self.o.id) + .finish_non_exhaustive() + } +} + +impl HardwareCursor for VirtualHc { + fn damage(&self) { + self.o.cursor_damage.set(true); + self.o.trigger_present(); + } +} + +const NSEC_PER_SEC: u64 = 1_000_000_000; +const PRE_COMMIT_MARGIN: u64 = 500_000; +const PRE_COMMIT_MARGIN_DELTA: u64 = 50_000; +const POST_COMMIT_MARGIN: u64 = 500_000; + +impl VirtualOutput { + fn clear(&self) { + self.flip_task.take(); + self.tasks.take(); + self.pending_flip.take(); + self.frame_data.take(); + } + + fn trigger_present(&self) { + if self.pending_flip.is_none() { + if self.cursor_damage.get() || self.damage.get() > 0 { + self.present_trigger.trigger(); + } + } + } + + async fn present_task(self: Rc) { + let be_state = self.persistent_state.backend_state.borrow().clone(); + let refresh_ns = be_state.mode.refresh_nsec(); + let vo_state = self.vo_state.get(); + let vrr = be_state.vrr; + let tearing = be_state.tearing; + let Some(fbs) = &vo_state.fbs else { + return; + }; + let mut max = 0; + let mut cur_sec = 0; + loop { + self.present_trigger.triggered().await; + if self.pending_flip.is_some() { + continue; + } + let mut start = self.state.now_nsec(); + let mut expected_seq = self.seq.get() + 1; + if !tearing { + let next_present = self + .next_vblank_nsec + .get() + .saturating_sub(self.pre_commit_margin.get()) + .saturating_sub(POST_COMMIT_MARGIN); + if start < next_present { + self.state.ring.timeout(next_present).await.unwrap(); + start = self.state.now_nsec(); + } else if !vrr { + expected_seq += 1; + } + } + let Some(on) = self.state.root.outputs.get(&self.id) else { + continue; + }; + let fb = &fbs.fbs[FbType::Primary]; + let cd = on.global.color_description.get(); + let linear_cd = on.global.linear_color_description.get(); + let blend_cd = match on.global.persistent.blend_space.get() { + BlendSpace::Linear => &linear_cd, + BlendSpace::Srgb => self.state.color_manager.srgb_gamma22(), + }; + let flip = match tearing { + true => start, + false => self.next_vblank_nsec.get().max(start), + }; + on.before_latch(flip).await; + if self.damage.get() > 0 || self.cursor_damage.get() { + on.schedule.commit_cursor(); + } + let cursor_latched = self.latch_cursor(&on, &fbs.fbs[FbType::Cursor]); + let latched = self.latch(&on); + on.latched(tearing); + if latched.is_none() && cursor_latched.is_none() { + continue; + } + let mut frame_data = None; + if let Some(latched) = &latched { + let sync; + if let Some(dsd) = self.prepare_direct_scanout(&be_state, blend_cd, &cd, latched) { + sync = match dsd.acquire_sync.clone() { + AcquireSync::None => None, + AcquireSync::Implicit => None, + AcquireSync::FdSync(sync) => Some(sync), + AcquireSync::Unnecessary => None, + }; + frame_data = Some(FrameData { dsd: Some(dsd) }); + } else { + let res = fb.fb.perform_render_pass( + AcquireSync::Unnecessary, + ReleaseSync::Explicit, + &cd, + &latched.pass, + &latched.damage, + fbs.blend_buffer.as_ref(), + blend_cd, + ); + sync = match res { + Ok(sync) => sync, + Err(e) => { + log::error!("Could not present: {}", ErrorFmt(e)); + return; + } + }; + frame_data = Some(FrameData { dsd: None }); + }; + if let Some(sync) = sync { + sync.signaled(&self.state.ring, "primary").await; + } + } + { + let prev_frame_data = &*self.frame_data.borrow(); + let effective_frame_data = frame_data.as_ref().or(prev_frame_data.as_ref()); + if let Some(fd) = effective_frame_data { + match &fd.dsd { + None => { + on.perform_screencopies( + &fb.tex, + &cd, + None, + &AcquireSync::Unnecessary, + ReleaseSync::None, + true, + 0, + 0, + None, + ); + } + Some(dsd) => { + on.perform_screencopies( + &dsd.tex, + &cd, + dsd.buffer_resv.as_ref(), + &dsd.acquire_sync, + dsd.release_sync, + true, + dsd.pos.crtc_x, + dsd.pos.crtc_y, + Some((dsd.pos.crtc_width, dsd.pos.crtc_height)), + ); + } + } + } + } + if let Some(Some(sync)) = cursor_latched { + sync.signaled(&self.state.ring, "cursor").await; + } + if let Some(latched) = &latched { + vo_state.locked.set(latched.locked); + self.damage.fetch_sub(latched.damage_count); + } + self.pending_flip.set(Some(ScheduledFlip { + on, + refresh_ns, + vrr, + tearing, + expected_seq: (!tearing).then_some(expected_seq), + locked: vo_state.locked.get(), + frame_data: frame_data.map(Some), + })); + if vrr { + self.need_vblank.trigger(); + } + if tearing { + self.trigger_flip.trigger(); + } + let duration = self.state.now_nsec() - start; + max = max.max(duration); + if start / NSEC_PER_SEC != cur_sec { + cur_sec = start / NSEC_PER_SEC; + self.pre_commit_margin_decay.add(max); + self.pre_commit_margin + .set(self.pre_commit_margin_decay.get()); + max = 0; + } + } + } + + fn latch(&self, on: &Rc) -> Option { + let damage_count = self.damage.get(); + if damage_count == 0 { + return None; + } + let damage = { + on.global.connector.damaged.set(false); + on.global.add_visualizer_damage(); + let damage = &mut *on.global.connector.damage.borrow_mut(); + let region = Region::from_rects2(damage); + damage.clear(); + region + }; + let pass = create_render_pass( + on.global.mode.get().size(), + &**on, + &self.state, + Some(on.global.pos.get()), + on.global.persistent.scale.get(), + true, + false, + on.has_fullscreen(), + true, + on.global.persistent.transform.get(), + Some(&self.state.damage_visualizer), + ); + Some(Latched { + pass, + damage_count, + damage, + locked: self.state.lock.locked.get(), + }) + } + + fn prepare_direct_scanout( + &self, + be_state: &BackendConnectorState, + blend_cd: &Rc, + cd: &Rc, + latched: &Latched, + ) -> Option { + let (ct, position) = latched.pass.prepare_direct_scanout( + be_state.mode.width, + be_state.mode.height, + blend_cd, + &cd, + true, + )?; + Some(DirectScanoutData { + buffer_resv: ct.buffer_resv.clone(), + tex: ct.tex.clone(), + acquire_sync: ct.acquire_sync.clone(), + release_sync: ct.release_sync, + pos: position, + }) + } + + fn latch_cursor(&self, on: &Rc, fb: &VoFb) -> Option> { + if !self.cursor_damage.take() { + return None; + } + let mut c = CursorChange { + enabled: false, + swap_buffer: None, + x: 0, + y: 0, + buffer: fb, + }; + if let Some(p) = self.cursor_programming.get() { + c.enabled = true; + c.x = p.x; + c.y = p.y; + } + self.state.present_hardware_cursor(on, &mut c); + let p = c.enabled.then_some(CursorProgramming { x: c.x, y: c.y }); + let mut cursor_changed = false; + cursor_changed |= self.cursor_programming.replace(p) != p; + cursor_changed |= c.swap_buffer.is_some(); + cursor_changed.then_some(c.swap_buffer.take().flatten()) + } + + async fn vblank_task(self: Rc) { + let be_state = self.persistent_state.backend_state.borrow().clone(); + let refresh_nsec = be_state.mode.refresh_nsec(); + let vrr = be_state.vrr; + let handle_vblank = || { + let next_vblank = self.state.now_nsec().saturating_add(refresh_nsec); + self.next_vblank_nsec.set(next_vblank); + self.seq.fetch_add(1); + if self.pending_flip.is_some() { + self.trigger_flip.trigger(); + } + if let Some(on) = self.state.root.outputs.get(&self.id) { + on.vblank(); + } + next_vblank + }; + if vrr { + loop { + let next_vblank = handle_vblank(); + if let Err(e) = self.state.ring.timeout(next_vblank).await { + log::error!("Could not wait for next vblank: {}", e); + return; + } + self.need_vblank.triggered().await; + } + } else { + let tfd = match TimerFd::new(c::CLOCK_MONOTONIC) { + Ok(fd) => fd, + Err(e) => { + log::error!("Could not create a timer fd: {}", ErrorFmt(e)); + return; + } + }; + let duration = Some(Duration::from_nanos(refresh_nsec)); + let res = tfd.program(duration, duration); + if let Err(e) = res { + log::error!("Could not program the timer fd: {}", ErrorFmt(e)); + return; + } + loop { + handle_vblank(); + if let Err(e) = tfd.expired(&self.state.ring).await { + log::error!("Could not wait for timer fd to expire: {}", ErrorFmt(e)); + return; + } + } + } + } + + async fn flip_task(self: Rc) { + let debounce = self.state.ring.debouncer(0); + loop { + self.trigger_flip.triggered().await; + let Some(mut flip) = self.pending_flip.take() else { + continue; + }; + let direct_scanout = { + let fd = &mut *self.frame_data.borrow_mut(); + if let Some(frame) = flip.frame_data.take() { + *fd = frame; + } + matches!(*fd, Some(FrameData { dsd: Some(..), .. })) + }; + let flip_ns = self.state.now_nsec(); + let tv_sec = flip_ns / NSEC_PER_SEC; + let tv_nsec = (flip_ns % NSEC_PER_SEC) as u32; + let mut flags = KIND_HW_COMPLETION | KIND_HW_CLOCK; + if !flip.tearing { + flags |= KIND_VSYNC; + } + if direct_scanout { + flags |= KIND_ZERO_COPY; + } + let seq = self.seq.get(); + flip.on.presented( + tv_sec, + tv_nsec, + flip.refresh_ns.try_into().unwrap_or(0), + seq, + flags, + flip.vrr, + flip.locked, + ); + self.trigger_present(); + if let Some(expected_seq) = flip.expected_seq + && seq > expected_seq + { + let mut margin = self.pre_commit_margin.get(); + if margin < flip.refresh_ns { + margin += PRE_COMMIT_MARGIN_DELTA; + self.pre_commit_margin.set(margin); + self.pre_commit_margin_decay.reset(margin); + } + } + debounce.debounce().await; + } + } + + fn create_transaction(&self) -> Transaction { + Transaction { + state: self.state.clone(), + changes: Default::default(), + } + } + + fn handle_render_ctx_change(self: &Rc) { + self.needs_format_update.set(true); + self.reapply_state(); + self.notify_frontend(); + } + + fn reapply_state(self: &Rc) { + let Err(e) = self.reapply_state_() else { + return; + }; + log::error!("Could not reapply state: {}", ErrorFmt(e)); + let retry = { + let bs = &mut *self.persistent_state.backend_state.borrow_mut(); + mem::replace(&mut bs.format, XRGB8888) != XRGB8888 + }; + if retry { + log::info!("Retrying with format {}", XRGB8888.name); + let Err(e) = self.reapply_state_() else { + return; + }; + log::error!("Could not reapply state: {}", ErrorFmt(e)); + } + let retry = { + let def = default_state(&self.state); + let bs = &mut *self.persistent_state.backend_state.borrow_mut(); + mem::replace(bs, def.clone()) != def + }; + if retry { + log::info!("Retrying with default state"); + let Err(e) = self.reapply_state_() else { + return; + }; + log::error!("Could not reapply state: {}", ErrorFmt(e)); + } + self.tasks.take(); + self.vo_state.take(); + } + + fn reapply_state_(self: &Rc) -> Result<(), BackendConnectorTransactionError> { + let mut transaction = self.create_transaction(); + transaction.add(self, self.persistent_state.backend_state.borrow().clone())?; + transaction.prepare()?.apply(); + Ok(()) + } + + fn notify_frontend(self: &Rc) { + let state = self.persistent_state.backend_state.borrow().clone(); + let desired_state = match state.enabled { + true => FrontendState::Desktop, + false => FrontendState::NonDesktop, + }; + let current_state = self.frontend_state.get(); + if desired_state != current_state { + if current_state != FrontendState::Disconnected { + self.events.send_event(ConnectorEvent::Disconnected); + } + self.events + .send_event(ConnectorEvent::Connected(MonitorInfo { + modes: None, + output_id: self.output_id.clone(), + width_mm: Default::default(), + height_mm: Default::default(), + non_desktop: Default::default(), + non_desktop_effective: !state.enabled, + vrr_capable: true, + eotfs: LinearizeExt::variants() + .filter(|v| *v != Default::default()) + .collect(), + color_spaces: LinearizeExt::variants() + .filter(|v| *v != Default::default()) + .collect(), + primaries: Primaries::SRGB, + luminance: Default::default(), + state: state.clone(), + })); + if state.enabled { + self.needs_format_update.set(true); + let hc = Rc::new(VirtualHc { o: self.clone() }); + self.events + .send_event(ConnectorEvent::HardwareCursor(Some(hc))); + } + } + if state.enabled && self.needs_format_update.take() { + self.events.send_event(ConnectorEvent::FormatsChanged( + self.state.virtual_outputs.formats.get(), + )); + } + self.frontend_state.set(desired_state); + } +} + +impl Transaction { + fn add( + &mut self, + connector: &Rc, + change: BackendConnectorState, + ) -> Result<(), BackendConnectorTransactionError> { + if change.mode.width <= 0 || change.mode.height <= 0 { + return Err(BackendConnectorTransactionError::UnsupportedMode( + connector.kernel_id(), + change.mode, + )); + } + if change.gamma_lut.is_some() { + return Err(BackendConnectorTransactionError::GammaLutNotSupported( + connector.kernel_id(), + )); + } + self.changes.insert( + connector.id, + TransactionChange { + output: connector.clone(), + new: change, + }, + ); + Ok(()) + } + + fn prepare(&mut self) -> Result { + let mut changes = vec![]; + let ctx = self.state.render_ctx.get(); + for change in self.changes.drain_values() { + let old_backend_state = change + .output + .persistent_state + .backend_state + .borrow() + .clone(); + let old_vo_state = change.output.vo_state.get(); + let mut new_vo_state = (*old_vo_state).clone(); + let mode = change.new.mode; + 'discard_fbs: { + if let Some(fbs) = &new_vo_state.fbs { + macro_rules! discard { + () => { + new_vo_state.fbs = None; + break 'discard_fbs; + }; + } + if !change.new.enabled { + discard!(); + } + let Some(ctx) = &ctx else { + discard!(); + }; + if !rc_eq(&fbs.ctx, ctx) { + discard!(); + } + if fbs.format != change.new.format { + discard!(); + } + let fb = &fbs.fbs[FbType::Primary]; + if (fb.width, fb.height) != mode.size() { + discard!(); + } + } + } + if new_vo_state.fbs.is_none() { + new_vo_state.locked.set(true); + } + if change.new.enabled && new_vo_state.fbs.is_none() { + let Some(ctx) = &ctx else { + return Err(BackendConnectorTransactionError::NoRenderContext); + }; + let bb = match ctx.acquire_blend_buffer(mode.width, mode.height) { + Ok(bb) => Some(bb), + Err(e) => { + log::warn!("Could not create a blend buffer: {}", ErrorFmt(e)); + None + } + }; + let sizes = static_map! { + FbType::Cursor => (CURSOR_SIZE, CURSOR_SIZE), + FbType::Primary => mode.size(), + }; + let fbs = allocate_scanout_buffers(&self.state, ctx, change.new.format, sizes) + .map_err(|e| { + BackendConnectorTransactionError::AllocateScanoutBuffers( + change.output.kernel_id(), + Box::new(e), + ) + })?; + new_vo_state.fbs = Some(Rc::new(FbState { + ctx: ctx.clone(), + format: change.new.format, + blend_buffer: bb, + fbs, + })); + } + changes.push(PreparedTransactionChange { + output: change.output, + old_backend_state, + old_vo_state, + new_backend_state: change.new, + new_vo_state: Rc::new(new_vo_state), + }); + } + Ok(PreparedTransaction { + state: self.state.clone(), + changes, + }) + } +} + +impl PreparedTransaction { + fn apply(&mut self) { + let eng = &self.state.eng; + for change in &mut self.changes { + let o = &change.output; + let mut tasks = None; + let ns = &change.new_backend_state; + if ns.enabled && ns.active { + tasks = Some([ + eng.spawn2("vo-present", Phase::Present, o.clone().present_task()), + eng.spawn("vo-vblank", o.clone().vblank_task()), + ]); + o.damage(); + if let Some(on) = self.state.root.outputs.get(&o.id) { + on.global.add_damage_area(&on.global.pos.get()); + on.global.connector.damage(); + } + } else { + if let Some(mut flip) = o.pending_flip.take() { + flip.frame_data = Some(None); + o.pending_flip.set(Some(flip)); + } else { + o.frame_data.take(); + } + } + o.tasks.set(tasks); + o.trigger_flip.trigger(); + *o.persistent_state.backend_state.borrow_mut() = ns.clone(); + o.vo_state.set(change.new_vo_state.clone()); + o.notify_frontend(); + mem::swap(&mut change.new_vo_state, &mut change.old_vo_state); + mem::swap(&mut change.new_backend_state, &mut change.old_backend_state); + } + } +} + +impl BackendConnectorTransaction for Transaction { + fn add( + &mut self, + connector: &Rc, + change: BackendConnectorState, + ) -> Result<(), BackendConnectorTransactionError> { + let Ok(connector) = (connector.clone() as Rc).downcast::() else { + return Err(BackendConnectorTransactionError::UnsupportedConnectorType( + connector.kernel_id(), + )); + }; + self.add(&connector, change) + } + + fn prepare( + mut self: Box, + ) -> Result, BackendConnectorTransactionError> + { + (*self).prepare().map(|t| Box::new(t) as _) + } +} + +impl BackendPreparedConnectorTransaction for PreparedTransaction { + fn apply( + mut self: Box, + ) -> Result, BackendConnectorTransactionError> { + (*self).apply(); + Ok(self) + } +} + +impl BackendAppliedConnectorTransaction for PreparedTransaction { + fn commit(self: Box) { + // nothing + } + + fn rollback(mut self: Box) -> Result<(), BackendConnectorTransactionError> { + (*self).apply(); + Ok(()) + } +} + +#[derive(Debug, Error)] +enum AllocError { + #[error("GfxContext does not support the format")] + GfxFormatNotSupported, + #[error("Could not allocate the BO")] + CreateBo(#[source] AllocatorError), + #[error("Could not import the dmabuf into the GfxContext")] + ImportImage(#[source] GfxError), + #[error("Could not create a texture")] + CreateTexture(#[source] GfxError), + #[error("Could not create a framebuffer")] + CreateFb(#[source] GfxError), +} + +fn allocate_scanout_buffers( + state: &Rc, + ctx: &Rc, + format: &'static Format, + sizes: StaticMap, +) -> Result, AllocError> { + let Some(gfx_format) = ctx.formats().get(&format.drm) else { + return Err(AllocError::GfxFormatNotSupported); + }; + let mut needs_render_usage = false; + let mut modifiers = vec![]; + for modifier in gfx_format.read_modifiers.iter().copied() { + let Some(write_modifier) = gfx_format.write_modifiers.get(&modifier) else { + continue; + }; + needs_render_usage |= write_modifier.needs_render_usage; + modifiers.push(modifier); + } + let mut usage = BufferUsage::none(); + if needs_render_usage { + usage |= BO_USE_RENDERING; + } + let create_fb = |(width, height): (i32, i32)| { + let bo = ctx + .allocator() + .create_bo(&state.dma_buf_ids, width, height, format, &modifiers, usage) + .map_err(AllocError::CreateBo)?; + let img = ctx + .clone() + .dmabuf_img(bo.dmabuf()) + .map_err(AllocError::ImportImage)?; + let tex = img + .clone() + .to_texture() + .map_err(AllocError::CreateTexture)?; + let fb = img.clone().to_framebuffer().map_err(AllocError::CreateFb)?; + Ok(VoFb { + width, + height, + _bo: bo, + tex, + fb, + }) + }; + let fbs = static_map! { + t => create_fb(sizes[t])?, + }; + Ok(fbs) +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 915c686b..99938471 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -183,6 +183,12 @@ pub enum Action { name: String, latch: bool, }, + CreateVirtualOutput { + name: String, + }, + RemoveVirtualOutput { + name: String, + }, } #[derive(Debug, Clone, Default)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 3e0e4702..d7834de8 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -480,6 +480,20 @@ impl ActionParser<'_> { latch: true, }) } + + fn parse_create_virtual_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult { + let (name,) = ext.extract((str("name"),))?; + Ok(Action::CreateVirtualOutput { + name: name.value.to_string(), + }) + } + + fn parse_remove_virtual_output(&mut self, ext: &mut Extractor<'_>) -> ParseResult { + let (name,) = ext.extract((str("name"),))?; + Ok(Action::RemoveVirtualOutput { + name: name.value.to_string(), + }) + } } impl Parser for ActionParser<'_> { @@ -539,6 +553,8 @@ impl Parser for ActionParser<'_> { "copy-mark" => self.parse_copy_mark(&mut ext), "push-mode" => self.parse_push_mode(&mut ext), "latch-mode" => self.parse_latch_mode(&mut ext), + "create-virtual-output" => self.parse_create_virtual_output(&mut ext), + "remove-virtual-output" => self.parse_remove_virtual_output(&mut ext), v => { ext.ignore_unused(); return Err(ActionParserError::UnknownType(v.to_string()).spanned(ty.span)); diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index cbe55830..f3e8e620 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -50,10 +50,10 @@ use { }, toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles, video::{ - ColorSpace, Connector, DrmDevice, Eotf, connectors, drm_devices, + ColorSpace, Connector, DrmDevice, Eotf, connectors, create_virtual_output, drm_devices, on_connector_connected, on_connector_disconnected, on_graphics_initialized, - on_new_connector, on_new_drm_device, set_direct_scanout_enabled, set_gfx_api, - set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode, + on_new_connector, on_new_drm_device, remove_virtual_output, set_direct_scanout_enabled, + set_gfx_api, set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode, }, window::Window, workspace::set_workspace_display_order, @@ -476,6 +476,8 @@ impl Action { state.set_mode(new, latch); }) } + Action::CreateVirtualOutput { name } => b.new(move || create_virtual_output(&name)), + Action::RemoveVirtualOutput { name } => b.new(move || remove_virtual_output(&name)), } } } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 57ba9d01..b88c957e 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -574,6 +574,40 @@ "type", "name" ] + }, + { + "description": "Creates a virtual output.\n\nThis is a no-op if a virtual output with that name already exists.\n\nThe virtual output has the connector name `VO-{name}` and the serial number\n`{name}`.\n\nA newly created connector is initially disabled. When a connector is destroyed\nand later recreated, its previous state is restored.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-x = { type = \"create-virtual-output\", name = \"abcd\" }\n\n [[connectors]]\n match.name = \"VO-abcd\"\n enabled = true\n\n [[outputs]]\n match.connector = \"VO-abcd\"\n mode = { width = 1920, height = 1080, refresh-rate = 120.0 }\n ```\n", + "type": "object", + "properties": { + "type": { + "const": "create-virtual-output" + }, + "name": { + "type": "string", + "description": "The name of the output." + } + }, + "required": [ + "type", + "name" + ] + }, + { + "description": "Removes a virtual output.\n\nThis is a no-op if no virtual output with that name exists.\n", + "type": "object", + "properties": { + "type": { + "const": "remove-virtual-output" + }, + "name": { + "type": "string", + "description": "The name of the output." + } + }, + "required": [ + "type", + "name" + ] } ] } diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 76b0e1f0..82b5bf03 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -840,6 +840,55 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a string. +- `create-virtual-output`: + + Creates a virtual output. + + This is a no-op if a virtual output with that name already exists. + + The virtual output has the connector name `VO-{name}` and the serial number + `{name}`. + + A newly created connector is initially disabled. When a connector is destroyed + and later recreated, its previous state is restored. + + - Example: + + ```toml + [shortcuts] + alt-x = { type = "create-virtual-output", name = "abcd" } + + [[connectors]] + match.name = "VO-abcd" + enabled = true + + [[outputs]] + match.connector = "VO-abcd" + mode = { width = 1920, height = 1080, refresh-rate = 120.0 } + ``` + + The table has the following fields: + + - `name` (required): + + The name of the output. + + The value of this field should be a string. + +- `remove-virtual-output`: + + Removes a virtual output. + + This is a no-op if no virtual output with that name exists. + + The table has the following fields: + + - `name` (required): + + The name of the output. + + The value of this field should be a string. + ### `BarPosition` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index aba09b5e..64eb1fde 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -810,6 +810,47 @@ Action: description: The name of the mode. required: true kind: string + create-virtual-output: + description: | + Creates a virtual output. + + This is a no-op if a virtual output with that name already exists. + + The virtual output has the connector name `VO-{name}` and the serial number + `{name}`. + + A newly created connector is initially disabled. When a connector is destroyed + and later recreated, its previous state is restored. + + - Example: + + ```toml + [shortcuts] + alt-x = { type = "create-virtual-output", name = "abcd" } + + [[connectors]] + match.name = "VO-abcd" + enabled = true + + [[outputs]] + match.connector = "VO-abcd" + mode = { width = 1920, height = 1080, refresh-rate = 120.0 } + ``` + fields: + name: + description: The name of the output. + required: true + kind: string + remove-virtual-output: + description: | + Removes a virtual output. + + This is a no-op if no virtual output with that name exists. + fields: + name: + description: The name of the output. + required: true + kind: string Exec: diff --git a/wire/jay_randr.txt b/wire/jay_randr.txt index 6f4f90b2..0bda895c 100644 --- a/wire/jay_randr.txt +++ b/wire/jay_randr.txt @@ -105,6 +105,14 @@ request set_use_native_gamut (since = 23) { use_native_gamut: u32, } +request create_virtual_output (since = 30) { + name: str, +} + +request remove_virtual_output (since = 30) { + name: str, +} + # events event global {