1
0
Fork 0
forked from wry/wry

virtual-output: add support for virtual outputs

This commit is contained in:
Julian Orth 2026-03-17 18:42:49 +01:00
parent c25d17514d
commit 530e66ef78
27 changed files with 1480 additions and 9 deletions

View file

@ -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 {

View file

@ -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<MetalError>),
AllocateScanoutBuffers(ConnectorKernelId, #[source] Box<dyn Error>),
#[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 {

View file

@ -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<PossibleValue> {
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<Self>, 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<Self>, randr: JayRandrId, args: CardArgs) {
let tc = &self.tc;
match args.command {

View file

@ -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);

View file

@ -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(())
}

View file

@ -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,
}
}
}

View file

@ -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();

View file

@ -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<State>,
new: String,
}
impl ControlCenterInner {
pub fn create_virtual_outputs_pane(self: &Rc<Self>) -> 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();
}
});
}
}

View file

@ -78,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError);
impl Global for JayCompositorGlobal {
fn version(&self) -> u32 {
29
30
}
fn required_caps(&self) -> ClientCaps {

View file

@ -270,6 +270,11 @@ impl JayRandr {
}
fn get_connector(&self, name: &str) -> Option<Rc<ConnectorData>> {
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<Rc<OutputData>> {
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<Self>,
) -> Result<(), Self::Error> {
self.state
.virtual_outputs
.get_or_create(&self.state, req.name);
Ok(())
}
fn remove_virtual_output(
&self,
req: RemoveVirtualOutput<'_>,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
self.state
.virtual_outputs
.remove_output(&self.state, req.name);
Ok(())
}
}
object_base! {

View file

@ -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;

View file

@ -114,6 +114,7 @@ mod user_session;
mod utils;
mod version;
mod video;
mod virtual_output;
mod vulkan_core;
mod wheel;
mod wire;

View file

@ -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<ObjectDropQueue<Rc<dyn BufferObject>>>,
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) {

View file

@ -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<State>) {
let mut beh = BackendEventHandler { state };

View file

@ -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));

View file

@ -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)
}

1105
src/virtual_output.rs Normal file

File diff suppressed because it is too large Load diff