diff --git a/build/wire.rs b/build/wire.rs index 98225598..b33f4ad9 100644 --- a/build/wire.rs +++ b/build/wire.rs @@ -274,15 +274,47 @@ fn write_message(f: &mut W, obj: &str, message: &Message) -> Result<() Ok(()) } +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum RequestHandlerDirection { + Request, + Event, +} + fn write_request_handler( f: &mut W, camel_obj_name: &str, messages: &[Lined], + direction: RequestHandlerDirection, ) -> Result<()> { + let snake_direction; + let camel_direction; + let parent; + let parser; + let error; + let param; writeln!(f)?; + match direction { + RequestHandlerDirection::Request => { + snake_direction = "request"; + camel_direction = "Request"; + parent = "crate::object::Object"; + parser = "crate::client::Client"; + error = "crate::client::ClientError"; + param = "req"; + } + RequestHandlerDirection::Event => { + snake_direction = "event"; + camel_direction = "Event"; + parent = "crate::wl_usr::usr_object::UsrObject"; + parser = "crate::wl_usr::UsrCon"; + error = "crate::wl_usr::UsrConError"; + param = "ev"; + writeln!(f, " #[allow(clippy::allow_attributes, dead_code)]")?; + } + } writeln!( f, - " pub trait {camel_obj_name}RequestHandler: crate::object::Object + Sized {{" + " pub trait {camel_obj_name}{camel_direction}Handler: {parent} + Sized {{" )?; writeln!(f, " type Error: std::error::Error;")?; for message in messages { @@ -294,24 +326,24 @@ fn write_request_handler( writeln!(f)?; writeln!( f, - " fn {}(&self, req: {}{lt}, _slf: &Rc) -> Result<(), Self::Error>;", + " fn {}(&self, {param}: {}{lt}, _slf: &Rc) -> Result<(), Self::Error>;", msg.safe_name, msg.camel_name )?; } writeln!(f)?; writeln!(f, " #[inline(always)]")?; - writeln!(f, " fn handle_request_impl(")?; + writeln!(f, " fn handle_{snake_direction}_impl(")?; writeln!(f, " self: Rc,")?; - writeln!(f, " client: &crate::client::Client,")?; + writeln!(f, " client: &{parser},")?; writeln!(f, " req: u32,")?; writeln!( f, " parser: crate::utils::buffd::MsgParser<'_, '_>," )?; - writeln!(f, " ) -> Result<(), crate::client::ClientError> {{")?; + writeln!(f, " ) -> Result<(), {error}> {{")?; if messages.is_empty() { writeln!(f, " #![allow(unused_variables)]")?; - writeln!(f, " Err(crate::client::ClientError::InvalidMethod)")?; + writeln!(f, " Err({error}::InvalidMethod)")?; } else { writeln!(f, " let method;")?; writeln!( @@ -347,10 +379,10 @@ fn write_request_handler( } writeln!( f, - " _ => return Err(crate::client::ClientError::InvalidMethod)," + " _ => return Err({error}::InvalidMethod)," )?; writeln!(f, " }};")?; - writeln!(f, " Err(crate::client::ClientError::MethodError {{")?; + writeln!(f, " Err({error}::MethodError {{")?; writeln!(f, " interface: {camel_obj_name},")?; writeln!(f, " id: self.id(),")?; writeln!(f, " method,")?; @@ -394,6 +426,13 @@ fn write_file( f, &camel_obj_name, &messages.requests, + RequestHandlerDirection::Request, + )?; + write_request_handler( + f, + &camel_obj_name, + &messages.events, + RequestHandlerDirection::Event, )?; writeln!(f, "}}")?; Ok(()) diff --git a/etc/jay-portals.conf b/etc/jay-portals.conf new file mode 100644 index 00000000..607e1691 --- /dev/null +++ b/etc/jay-portals.conf @@ -0,0 +1,6 @@ +[preferred] +default=gtk +org.freedesktop.impl.portal.ScreenCast=jay +org.freedesktop.impl.portal.RemoteDesktop=jay +org.freedesktop.impl.portal.Inhibit=none +org.freedesktop.impl.portal.FileChooser=gtk4 diff --git a/etc/jay.portal b/etc/jay.portal new file mode 100644 index 00000000..c6d67365 --- /dev/null +++ b/etc/jay.portal @@ -0,0 +1,3 @@ +[portal] +DBusName=org.freedesktop.impl.portal.desktop.jay +Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.RemoteDesktop; diff --git a/src/cli.rs b/src/cli.rs index 951fbe85..d5bc6566 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,6 +32,7 @@ use { }, compositor::start_compositor, logger::LogLevel, + portal, pr_caps::drop_all_pr_caps, }, clap::{Args, Parser, Subcommand, ValueEnum, ValueHint}, @@ -87,6 +88,8 @@ pub enum Cmd { Dpms(DpmsArgs), /// Tests the events produced by a seat. SeatTest(SeatTestArgs), + /// Run the desktop portal. + Portal, /// Inspect/modify graphics card and connector settings. Randr(RandrArgs), /// Inspect/modify input settings. @@ -243,6 +246,7 @@ pub fn main() { Cmd::Dpms(a) => dpms::main(cli.global, a), Cmd::Unlock => unlock::main(cli.global), Cmd::SeatTest(a) => seat_test::main(cli.global, a), + Cmd::Portal => portal::run_freestanding(cli.global), Cmd::Randr(a) => randr::main(cli.global, a), Cmd::Input(a) => input::main(cli.global, a), Cmd::DamageTracking(a) => damage_tracking::main(cli.global, a), diff --git a/src/client.rs b/src/client.rs index 058d2373..ce825b0d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -41,9 +41,8 @@ use { }; pub use { error::{ClientError, ParserError}, + objects::MIN_SERVER_ID, }; -#[cfg(feature = "it")] -pub use objects::MIN_SERVER_ID; mod error; mod objects; diff --git a/src/client/objects.rs b/src/client/objects.rs index be1f5465..09550a9e 100644 --- a/src/client/objects.rs +++ b/src/client/objects.rs @@ -6,10 +6,6 @@ use { wp_image_description_reference_v1::WpImageDescriptionReferenceV1, wp_image_description_v1::WpImageDescriptionV1, }, - ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, - ext_image_capture_source_v1::ExtImageCaptureSourceV1, - ext_image_copy::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1, - head_management::jay_head_error_v1::JayHeadErrorV1, data_transfer::{ data_control::{ ext_data_control_source_v1::ExtDataControlSourceV1, @@ -18,7 +14,12 @@ use { wl_data_source::WlDataSource, zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, }, + ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, + ext_image_capture_source_v1::ExtImageCaptureSourceV1, + ext_image_copy::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1, + head_management::jay_head_error_v1::JayHeadErrorV1, jay_output::JayOutput, + jay_screencast::JayScreencast, jay_toplevel::JayToplevel, jay_workspace::JayWorkspace, wl_buffer::WlBuffer, @@ -48,9 +49,9 @@ use { wire::{ ExtDataControlSourceV1Id, ExtForeignToplevelHandleV1Id, ExtImageCaptureSourceV1Id, ExtImageCopyCaptureSessionV1Id, ExtWorkspaceGroupHandleV1Id, JayHeadErrorV1Id, - JayOutputId, JayToplevelId, JayWorkspaceId, WlBufferId, WlDataSourceId, WlOutputId, - WlPointerId, WlRegionId, WlRegistryId, WlSeatId, WlSurfaceId, - WpDrmLeaseConnectorV1Id, WpImageDescriptionReferenceV1Id, + JayOutputId, JayScreencastId, JayToplevelId, JayWorkspaceId, WlBufferId, + WlDataSourceId, WlOutputId, WlPointerId, WlRegionId, WlRegistryId, WlSeatId, + WlSurfaceId, WpDrmLeaseConnectorV1Id, WpImageDescriptionReferenceV1Id, WpImageDescriptionV1Id, WpLinuxDrmSyncobjTimelineV1Id, XdgPopupId, XdgPositionerId, XdgSurfaceId, XdgToplevelId, XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwlrOutputHeadV1Id, ZwlrOutputModeV1Id, ZwpPrimarySelectionSourceV1Id, @@ -79,6 +80,7 @@ pub struct Objects { pub pointers: CopyHashMap>, pub xdg_wm_bases: CopyHashMap>, pub seats: CopyHashMap>, + pub screencasts: CopyHashMap>, pub timelines: CopyHashMap>, pub zwlr_data_sources: CopyHashMap>, pub zwlr_output_heads: CopyHashMap>, @@ -125,6 +127,7 @@ impl Objects { pointers: Default::default(), xdg_wm_bases: Default::default(), seats: Default::default(), + screencasts: Default::default(), timelines: Default::default(), zwlr_data_sources: Default::default(), zwlr_output_heads: Default::default(), @@ -173,6 +176,7 @@ impl Objects { self.xdg_wm_bases.clear(); self.seats.clear(); self.pointers.clear(); + self.screencasts.clear(); self.timelines.clear(); self.zwlr_data_sources.clear(); self.jay_toplevels.clear(); diff --git a/src/compositor.rs b/src/compositor.rs index 6520cadd..c270687b 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -35,6 +35,7 @@ use { head_management::{ HeadManagers, HeadState, jay_head_manager_session_v1::handle_jay_head_manager_done, }, + jay_screencast::{perform_screencast_realloc, perform_toplevel_screencasts}, wl_output::{BlendSpace, OutputId, PersistentOutputState, WlOutputGlobal}, wl_seat::{handle_position_hint_requests, handle_warp_mouse_to_focus}, wl_surface::{ @@ -49,6 +50,7 @@ use { leaks, logger::Logger, output_schedule::create_output_schedule, + portal::{self, PortalStartup}, pr_caps::{PrCapsThread, pr_caps}, scale::Scale, sighand::{self, SighandError}, @@ -115,10 +117,19 @@ pub fn start_compositor(global: GlobalArgs, args: RunArgs) { None }; let forker = create_forker(reaper_pid); + let portal = portal::run_from_compositor(global.log_level); enable_profiler(); let logger = Logger::install_compositor(global.log_level); + let portal = match portal { + Ok(p) => Some(p), + Err(e) => { + log::error!("Could not spawn portal: {}", ErrorFmt(e)); + None + } + }; let res = start_compositor2( Some(forker), + portal, Some(logger.clone()), args, None, @@ -137,7 +148,7 @@ pub fn start_compositor(global: GlobalArgs, args: RunArgs) { #[cfg(feature = "it")] pub fn start_compositor_for_test(future: TestFuture) -> Result<(), CompositorError> { - let res = start_compositor2(None, None, RunArgs::default(), Some(future), None); + let res = start_compositor2(None, None, None, RunArgs::default(), Some(future), None); leaks::log_leaked(); res } @@ -179,6 +190,7 @@ pub type TestFuture = Box) -> Box>>; fn start_compositor2( forker: Option>, + portal: Option, logger: Option>, run_args: RunArgs, test_future: Option, @@ -246,6 +258,8 @@ fn start_compositor2( pending_output_render_data: Default::default(), pending_float_layout: Default::default(), pending_input_popup_positioning: Default::default(), + pending_toplevel_screencasts: Default::default(), + pending_screencast_reallocs_or_reconfigures: Default::default(), pending_placeholder_render_textures: Default::default(), pending_container_tab_render_textures: Default::default(), dbus: Dbus::new(&engine, &ring, &run_toplevel), @@ -401,6 +415,13 @@ fn start_compositor2( forker.setenv(key.as_bytes(), val.as_bytes()); } } + let mut _portal = None; + if let (Some(portal), Some(logger)) = (portal, &logger) { + _portal = Some(engine.spawn( + "portal", + portal.spawn(engine.clone(), ring.clone(), logger.clone()), + )); + } let _compositor = engine.spawn("compositor", start_compositor3(state.clone(), test_future)); ring.run()?; state.clear(); @@ -560,6 +581,16 @@ fn start_global_event_handlers(state: &Rc) -> Vec> { Phase::PostLayout, handle_xdg_surface_configure_events(state.clone()), ), + eng.spawn2( + "toplevel screencast present", + Phase::Present, + perform_toplevel_screencasts(state.clone()), + ), + eng.spawn2( + "screencast realloc", + Phase::PostLayout, + perform_screencast_realloc(state.clone()), + ), eng.spawn( "lazy event sources", handle_lazy_event_sources(state.clone()), @@ -749,6 +780,7 @@ fn create_dummy_output(state: &Rc) { hardware_cursor: Default::default(), update_render_data_scheduled: Cell::new(false), hardware_cursor_needs_render: Cell::new(false), + screencasts: Default::default(), screencopies: Default::default(), title_visible: Cell::new(false), schedule, diff --git a/src/dbus.rs b/src/dbus.rs index b24d8002..5d979b9b 100644 --- a/src/dbus.rs +++ b/src/dbus.rs @@ -16,12 +16,13 @@ use { }, ahash::AHashMap, std::{ - borrow::Cow, + borrow::{Borrow, Cow}, cell::{Cell, RefCell}, fmt::Debug, future::Future, marker::PhantomData, mem, + ops::Deref, pin::Pin, rc::Rc, task::{Context, Poll, Waker}, @@ -174,6 +175,24 @@ pub struct DbusSocket { headers: RefCell)>>, run_toplevel: Rc, signal_handlers: RefCell>, + objects: CopyHashMap, Rc>, +} + +#[derive(Hash, Eq, PartialEq)] +struct MemberHandlerOwnedKey { + key: MemberHandlerKey<'static>, +} + +#[derive(Hash, Eq, PartialEq)] +struct MemberHandlerKey<'a> { + interface: &'a str, + member: &'a str, +} + +impl<'a> Borrow> for MemberHandlerOwnedKey { + fn borrow(&self) -> &MemberHandlerKey<'a> { + &self.key + } } const HDR_PATH: u8 = 1; @@ -197,6 +216,20 @@ const NO_AUTO_START: u8 = 0x2; #[expect(dead_code)] const ALLOW_INTERACTIVE_AUTHORIZATION: u8 = 0x4; +#[expect(dead_code)] +pub const DBUS_NAME_FLAG_ALLOW_REPLACEMENT: u32 = 0x1; +#[expect(dead_code)] +pub const DBUS_NAME_FLAG_REPLACE_EXISTING: u32 = 0x2; +pub const DBUS_NAME_FLAG_DO_NOT_QUEUE: u32 = 0x4; + +pub const DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER: u32 = 1; +#[expect(dead_code)] +pub const DBUS_REQUEST_NAME_REPLY_IN_QUEUE: u32 = 2; +#[expect(dead_code)] +pub const DBUS_REQUEST_NAME_REPLY_EXISTS: u32 = 3; +#[expect(dead_code)] +pub const DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER: u32 = 4; + pub const BUS_DEST: &str = "org.freedesktop.DBus"; pub const BUS_PATH: &str = "/org/freedesktop/DBus"; @@ -399,3 +432,308 @@ struct InterfaceSignalHandlers { unconditional: Option>, conditional: AHashMap>, } + +struct DbusObjectData { + path: Cow<'static, str>, + methods: CopyHashMap>, + properties: CopyHashMap>, +} + +pub struct DbusObject { + socket: Rc, + data: Rc, +} + +impl Drop for DbusObject { + fn drop(&mut self) { + self.socket.objects.remove(&self.data.path); + } +} + +impl DbusObject { + pub fn add_method(&self, handler: F) + where + T: MethodCall<'static>, + F: for<'a> Fn(T::Generic<'a>, PendingReply) + 'static, + { + let rhd = Rc::new(MethodHandlerData { + handler, + _phantom: Default::default(), + }); + let key = MemberHandlerOwnedKey { + key: MemberHandlerKey { + interface: T::INTERFACE, + member: T::MEMBER, + }, + }; + self.data.methods.set(key, rhd); + } + + pub fn set_property(&self, value: Variant<'static>) + where + T: Property + 'static, + { + self.emit_signal( + &crate::wire_dbus::org::freedesktop::dbus::properties::PropertiesChanged { + interface_name: T::INTERFACE.into(), + changed_properties: Cow::Borrowed(&[DictEntry { + key: T::PROPERTY.into(), + value: borrow_variant(&value), + }]), + invalidated_properties: Default::default(), + }, + ); + let phd = Rc::new(PropertyHandlerData:: { + data: value, + _phantom: Default::default(), + }); + let key = MemberHandlerOwnedKey { + key: MemberHandlerKey { + interface: T::INTERFACE, + member: T::PROPERTY, + }, + }; + self.data.properties.set(key, phd); + } + + pub fn emit_signal<'a, T: Signal<'a>>(&self, signal: &T) { + self.socket.emit_signal(&self.data.path, signal); + } + + pub fn path(&self) -> &str { + &self.data.path + } +} + +trait PropertyHandlerApi { + fn interface(&self) -> &'static str; + fn member(&self) -> &'static str; + fn value<'a>(&'a self) -> Variant<'a>; +} + +struct PropertyHandlerData { + data: Variant<'static>, + _phantom: PhantomData, +} + +impl PropertyHandlerApi for PropertyHandlerData +where + T: Property, +{ + fn interface(&self) -> &'static str { + T::INTERFACE + } + + fn member(&self) -> &'static str { + T::PROPERTY + } + + fn value<'a>(&'a self) -> Variant<'a> { + borrow_variant(&self.data) + } +} + +fn borrow_variant<'a>(value: &'a Variant<'static>) -> Variant<'a> { + match value { + Variant::U8(v) => Variant::U8(*v), + Variant::Bool(v) => Variant::Bool(*v), + Variant::I16(v) => Variant::I16(*v), + Variant::U16(v) => Variant::U16(*v), + Variant::I32(v) => Variant::I32(*v), + Variant::U32(v) => Variant::U32(*v), + Variant::I64(v) => Variant::I64(*v), + Variant::U64(v) => Variant::U64(*v), + Variant::F64(v) => Variant::F64(*v), + Variant::String(v) => Variant::String(Cow::Borrowed(v.as_ref())), + Variant::ObjectPath(v) => Variant::ObjectPath(ObjectPath(Cow::Borrowed(v.0.as_ref()))), + Variant::Signature(v) => Variant::Signature(Signature(Cow::Borrowed(v.0.as_ref()))), + Variant::Variant(v) => Variant::Variant(Box::new(borrow_variant(v))), + Variant::Fd(v) => Variant::Fd(v.clone()), + Variant::Array(ty, values) => { + Variant::Array(ty.clone(), values.iter().map(borrow_variant).collect()) + } + Variant::DictEntry(k, v) => { + Variant::DictEntry(Box::new(borrow_variant(k)), Box::new(borrow_variant(v))) + } + Variant::Struct(values) => Variant::Struct(values.iter().map(borrow_variant).collect()), + } +} + +pub struct PendingReply { + reply_expected: bool, + socket: Rc, + destination: String, + serial: u32, + _phantom: PhantomData, +} + +impl PendingReply { + #[expect(dead_code)] + pub fn reply_expected(&self) -> bool { + self.reply_expected + } + + pub fn err(&self, msg: &str) { + if self.reply_expected { + self.socket.send_error(&self.destination, self.serial, msg); + } + } +} + +impl PendingReply +where + T: Message<'static>, +{ + pub fn ok<'a>(&self, msg: &T::Generic<'a>) { + if self.reply_expected { + self.socket.send_reply(&self.destination, self.serial, msg); + } + } + + #[expect(dead_code)] + pub fn complete<'a>(&self, res: Result<&T::Generic<'a>, &str>) { + match res { + Ok(m) => self.ok(m), + Err(e) => self.err(e), + } + } +} + +trait MethodHandlerApi { + fn signature(&self) -> &'static str; + fn handle( + &self, + object: &DbusObjectData, + socket: &Rc, + dest: &str, + serial: u32, + reply_expected: bool, + parser: &mut Parser, + ) -> Result<(), DbusError>; +} + +struct MethodHandlerData { + handler: F, + _phantom: PhantomData, +} + +impl MethodHandlerApi for MethodHandlerData +where + T: MethodCall<'static>, + F: for<'a> Fn(T::Generic<'a>, PendingReply) + 'static, +{ + fn signature(&self) -> &'static str { + T::SIGNATURE + } + + fn handle<'a>( + &self, + _object: &DbusObjectData, + socket: &Rc, + dest: &str, + serial: u32, + reply_expected: bool, + parser: &mut Parser<'a>, + ) -> Result<(), DbusError> { + let msg = T::Generic::<'a>::unmarshal(parser)?; + let pr = PendingReply { + reply_expected, + socket: socket.clone(), + destination: dest.to_string(), + serial, + _phantom: Default::default(), + }; + (self.handler)(msg, pr); + Ok(()) + } +} + +struct PropertyGetHandlerProxy; + +impl MethodHandlerApi for PropertyGetHandlerProxy { + fn signature(&self) -> &'static str { + crate::wire_dbus::org::freedesktop::dbus::properties::Get::SIGNATURE + } + + fn handle<'a>( + &self, + object: &DbusObjectData, + socket: &Rc, + dest: &str, + serial: u32, + reply_expected: bool, + parser: &mut Parser<'a>, + ) -> Result<(), DbusError> { + if !reply_expected { + return Ok(()); + } + let msg = crate::wire_dbus::org::freedesktop::dbus::properties::Get::unmarshal(parser)?; + let key = MemberHandlerKey { + interface: msg.interface_name.deref(), + member: msg.property_name.deref(), + }; + match object.properties.get(&key) { + Some(h) => socket.send_reply( + dest, + serial, + &crate::wire_dbus::org::freedesktop::dbus::properties::GetReply { + value: h.value(), + }, + ), + _ => socket.send_error(dest, serial, "Property does not exist"), + }; + Ok(()) + } +} + +struct PropertyGetAllHandlerProxy; + +impl MethodHandlerApi for PropertyGetAllHandlerProxy { + fn signature(&self) -> &'static str { + crate::wire_dbus::org::freedesktop::dbus::properties::GetAll::SIGNATURE + } + + fn handle<'a>( + &self, + object: &DbusObjectData, + socket: &Rc, + dest: &str, + serial: u32, + reply_expected: bool, + parser: &mut Parser<'a>, + ) -> Result<(), DbusError> { + if !reply_expected { + return Ok(()); + } + let msg = crate::wire_dbus::org::freedesktop::dbus::properties::GetAll::unmarshal(parser)?; + let all_props = object.properties.lock(); + let mut props = vec![]; + for property in all_props.values() { + if property.interface() == msg.interface_name { + props.push(DictEntry { + key: property.member().into(), + value: property.value(), + }); + } + } + socket.send_reply( + dest, + serial, + &crate::wire_dbus::org::freedesktop::dbus::properties::GetAllReply { + props: props.into(), + }, + ); + Ok(()) + } +} + +pub mod prelude { + pub use { + super::{ + DbusError, DbusType, Formatter, Message, MethodCall, Parser, Property, Signal, + types::{Bool, DictEntry, ObjectPath, Variant}, + }, + std::{borrow::Cow, rc::Rc}, + uapi::OwnedFd, + }; +} diff --git a/src/dbus/holder.rs b/src/dbus/holder.rs index 491cb633..aa0ce1ba 100644 --- a/src/dbus/holder.rs +++ b/src/dbus/holder.rs @@ -68,6 +68,7 @@ async fn connect( headers: Default::default(), run_toplevel: run_toplevel.clone(), signal_handlers: Default::default(), + objects: Default::default(), }); let skt = socket.clone(); socket.call( diff --git a/src/dbus/incoming.rs b/src/dbus/incoming.rs index db0ab623..764f2164 100644 --- a/src/dbus/incoming.rs +++ b/src/dbus/incoming.rs @@ -6,7 +6,7 @@ use { crate::{ dbus::{ CallError, DbusError, DbusSocket, Headers, MSG_ERROR, MSG_METHOD_CALL, - MSG_METHOD_RETURN, MSG_SIGNAL, Parser, + MSG_METHOD_RETURN, MSG_SIGNAL, MemberHandlerKey, NO_REPLY_EXPECTED, Parser, }, utils::{ bufio::BufIoIncoming, @@ -61,6 +61,7 @@ impl Incoming { return Err(DbusError::InvalidEndianess); } let msg_ty = msg_buf[1]; + let flags = msg_buf[2]; let protocol = msg_buf[3]; if protocol != 1 { return Err(DbusError::InvalidProtocol); @@ -83,16 +84,57 @@ impl Incoming { return Err(DbusError::TooFewFds); } let fds: Vec<_> = self.incoming.fds.drain(..unix_fds).collect(); - let mut parser = - Parser::new_at(msg_buf, FIXED_HEADER_SIZE + dyn_header_len as usize, &fds); + let mut parser = Parser::new_at(msg_buf, FIXED_HEADER_SIZE + dyn_header_len as usize, &fds); match msg_ty { MSG_METHOD_CALL => { - let sender = match &headers.sender { - Some(s) => s, + let (sender, interface, member, path) = match ( + &headers.sender, + &headers.interface, + &headers.member, + &headers.path, + ) { + (Some(s), Some(i), Some(m), Some(p)) => (s, i, m, p), _ => return Err(DbusError::MissingMethodCallHeaders), }; - self.socket - .send_error(sender.deref(), serial, "Object does not exist"); + if let Some(object) = self.socket.objects.get(path.deref()) { + let key = MemberHandlerKey { + interface: interface.deref(), + member: member.deref(), + }; + if let Some(handler) = object.methods.get(&key) { + let sig = headers.signature.as_deref().unwrap_or(""); + if sig != handler.signature() { + let msg = format!( + "Method call has an invalid signature: expected: {}, actual: {}", + handler.signature(), + sig, + ); + self.socket.send_error(sender.deref(), serial, &msg); + } else { + let reply_expected = flags & NO_REPLY_EXPECTED == 0; + if let Err(e) = handler.handle( + &object, + &self.socket, + sender, + serial, + reply_expected, + &mut parser, + ) { + log::error!( + "{}: Could not handle method call: {}", + self.socket.bus_name, + ErrorFmt(e) + ); + } + } + } else { + self.socket + .send_error(sender.deref(), serial, "Method does not exist"); + } + } else { + self.socket + .send_error(sender.deref(), serial, "Object does not exist"); + } } MSG_METHOD_RETURN | MSG_ERROR => { let serial = match headers.reply_serial { diff --git a/src/dbus/socket.rs b/src/dbus/socket.rs index 6cb70373..2c733ec4 100644 --- a/src/dbus/socket.rs +++ b/src/dbus/socket.rs @@ -1,12 +1,13 @@ use { crate::{ dbus::{ - AsyncProperty, AsyncReply, AsyncReplySlot, BUS_DEST, BUS_PATH, DbusError, DbusSocket, - DbusType, ErrorMessage, Formatter, HDR_DESTINATION, HDR_ERROR_NAME, HDR_INTERFACE, - HDR_MEMBER, HDR_PATH, HDR_REPLY_SERIAL, HDR_SIGNATURE, HDR_UNIX_FDS, Headers, - InterfaceSignalHandlers, MSG_ERROR, MSG_METHOD_CALL, Message, - MethodCall, NO_REPLY_EXPECTED, Parser, Property, Reply, ReplyHandler, Signal, - SignalHandler, SignalHandlerApi, SignalHandlerData, + AsyncProperty, AsyncReply, AsyncReplySlot, BUS_DEST, BUS_PATH, DbusError, DbusObject, + DbusObjectData, DbusSocket, DbusType, ErrorMessage, Formatter, HDR_DESTINATION, + HDR_ERROR_NAME, HDR_INTERFACE, HDR_MEMBER, HDR_PATH, HDR_REPLY_SERIAL, HDR_SIGNATURE, + HDR_UNIX_FDS, Headers, InterfaceSignalHandlers, MSG_ERROR, MSG_METHOD_CALL, + MSG_METHOD_RETURN, MSG_SIGNAL, Message, MethodCall, NO_REPLY_EXPECTED, Parser, + Property, PropertyGetAllHandlerProxy, PropertyGetHandlerProxy, Reply, ReplyHandler, + Signal, SignalHandler, SignalHandlerApi, SignalHandlerData, property::Get, types::{ObjectPath, Signature, Variant}, }, @@ -14,8 +15,8 @@ use { wire_dbus::org, }, std::{ - cell::Cell, collections::hash_map::Entry, fmt::Write, marker::PhantomData, mem, - ops::DerefMut, rc::Rc, + borrow::Cow, cell::Cell, collections::hash_map::Entry, fmt::Write, marker::PhantomData, + mem, ops::DerefMut, rc::Rc, }, uapi::c, }; @@ -132,6 +133,46 @@ impl DbusSocket { } } + pub fn add_object( + self: &Rc, + object: impl Into>, + ) -> Result { + let object = object.into(); + let data = Rc::new(DbusObjectData { + path: object.clone(), + methods: Default::default(), + properties: Default::default(), + }); + match self.objects.lock().entry(object) { + Entry::Occupied(_) => Err(DbusError::AlreadyHandled), + Entry::Vacant(v) => { + v.insert(data.clone()); + data.methods.set( + crate::dbus::MemberHandlerOwnedKey { + key: crate::dbus::MemberHandlerKey { + interface: org::freedesktop::dbus::properties::Get::INTERFACE, + member: org::freedesktop::dbus::properties::Get::MEMBER, + }, + }, + Rc::new(PropertyGetHandlerProxy), + ); + data.methods.set( + crate::dbus::MemberHandlerOwnedKey { + key: crate::dbus::MemberHandlerKey { + interface: org::freedesktop::dbus::properties::GetAll::INTERFACE, + member: org::freedesktop::dbus::properties::GetAll::MEMBER, + }, + }, + Rc::new(PropertyGetAllHandlerProxy), + ); + Ok(DbusObject { + socket: self.clone(), + data, + }) + } + } + } + pub fn handle_signal( self: &Rc, sender: Option<&str>, @@ -250,6 +291,23 @@ impl DbusSocket { serial } + pub fn emit_signal<'a, T: Signal<'a>>(&self, path: &str, msg: &T) -> u32 { + let (msg, serial) = self.format_signal(path, msg); + self.bufio.send(msg); + serial + } + + pub fn send_reply<'a, T: Message<'a>>( + &self, + destination: &str, + reply_serial: u32, + msg: &T, + ) -> u32 { + let (msg, serial) = self.format_reply(destination, reply_serial, msg); + self.bufio.send(msg); + serial + } + fn send_call<'a, T: Message<'a>>( &self, path: &str, @@ -277,6 +335,29 @@ impl DbusSocket { ) } + fn format_signal<'a, T: Signal<'a>>(&self, path: &str, msg: &T) -> (BufIoMessage, u32) { + self.format_generic(MSG_SIGNAL, Some(path), None, None, 0, msg, None, true, true) + } + + fn format_reply<'a, T: Message<'a>>( + &self, + destination: &str, + reply_serial: u32, + msg: &T, + ) -> (BufIoMessage, u32) { + self.format_generic( + MSG_METHOD_RETURN, + None, + Some(reply_serial), + Some(destination), + 0, + msg, + None, + true, + true, + ) + } + fn format_call<'a, T: Message<'a>>( &self, path: &str, diff --git a/src/format.rs b/src/format.rs index a4cad104..5f812a1c 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1 +1,57 @@ pub use jay_formats::*; + +use { + crate::pipewire::pw_pod::{ + SPA_VIDEO_FORMAT_ABGR_210LE, SPA_VIDEO_FORMAT_ARGB_210LE, SPA_VIDEO_FORMAT_BGR, + SPA_VIDEO_FORMAT_BGR15, SPA_VIDEO_FORMAT_BGR16, SPA_VIDEO_FORMAT_BGRA, + SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_GRAY8, SPA_VIDEO_FORMAT_RGB, + SPA_VIDEO_FORMAT_RGB16, SPA_VIDEO_FORMAT_RGBA, SPA_VIDEO_FORMAT_RGBx, + SPA_VIDEO_FORMAT_UNKNOWN, SPA_VIDEO_FORMAT_xBGR_210LE, SPA_VIDEO_FORMAT_xRGB_210LE, + SpaVideoFormat, + }, + ahash::AHashMap, + std::sync::LazyLock, +}; + +static PW_FORMATS_MAP: LazyLock> = LazyLock::new(|| { + let mut map = AHashMap::new(); + for format in FORMATS { + let pw = pw_format(format); + if pw != SPA_VIDEO_FORMAT_UNKNOWN { + assert!(map.insert(pw, format).is_none()); + } + } + map +}); + +pub fn pw_formats() -> &'static AHashMap { + &PW_FORMATS_MAP +} + +pub fn pipewire_format(format: &'static Format) -> SpaVideoFormat { + pw_format(format) +} + +const fn fourcc_code(a: char, b: char, c: char, d: char) -> u32 { + (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24) +} + +fn pw_format(format: &Format) -> SpaVideoFormat { + match format.drm { + drm if drm == fourcc_code('A', 'R', '2', '4') => SPA_VIDEO_FORMAT_BGRA, + drm if drm == fourcc_code('X', 'R', '2', '4') => SPA_VIDEO_FORMAT_BGRx, + drm if drm == fourcc_code('A', 'B', '2', '4') => SPA_VIDEO_FORMAT_RGBA, + drm if drm == fourcc_code('X', 'B', '2', '4') => SPA_VIDEO_FORMAT_RGBx, + drm if drm == fourcc_code('R', '8', ' ', ' ') => SPA_VIDEO_FORMAT_GRAY8, + drm if drm == fourcc_code('R', 'G', '2', '4') => SPA_VIDEO_FORMAT_BGR, + drm if drm == fourcc_code('B', 'G', '2', '4') => SPA_VIDEO_FORMAT_RGB, + drm if drm == fourcc_code('R', 'G', '1', '6') => SPA_VIDEO_FORMAT_BGR16, + drm if drm == fourcc_code('B', 'G', '1', '6') => SPA_VIDEO_FORMAT_RGB16, + drm if drm == fourcc_code('X', 'R', '1', '5') => SPA_VIDEO_FORMAT_BGR15, + drm if drm == fourcc_code('A', 'R', '3', '0') => SPA_VIDEO_FORMAT_ARGB_210LE, + drm if drm == fourcc_code('X', 'R', '3', '0') => SPA_VIDEO_FORMAT_xRGB_210LE, + drm if drm == fourcc_code('A', 'B', '3', '0') => SPA_VIDEO_FORMAT_ABGR_210LE, + drm if drm == fourcc_code('X', 'B', '3', '0') => SPA_VIDEO_FORMAT_xBGR_210LE, + _ => SPA_VIDEO_FORMAT_UNKNOWN, + } +} diff --git a/src/gfx_api.rs b/src/gfx_api.rs index 4c135684..08354af8 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -570,6 +570,33 @@ impl dyn GfxFramebuffer { ) } + pub fn render_custom( + self: &Rc, + acquire_sync: AcquireSync, + release_sync: ReleaseSync, + cd: &Rc, + scale: Scale, + clear: Option<&Color>, + clear_cd: &Rc, + blend_buffer: Option<&Rc>, + blend_cd: &Rc, + f: &mut dyn FnMut(&mut RendererBase), + ) -> Result, GfxError> { + let mut ops = vec![]; + let mut renderer = self.renderer_base(&mut ops, scale, Transform::None); + f(&mut renderer); + self.render( + acquire_sync, + release_sync, + cd, + &ops, + clear, + clear_cd, + blend_buffer, + blend_cd, + ) + } + pub fn create_render_pass( &self, node: &dyn Node, @@ -930,6 +957,44 @@ pub struct GfxFormat { pub supports_shm: bool, } +impl GfxFormat { + pub fn cross_intersect(&self, other: &GfxFormat) -> GfxFormat { + assert_eq!(self.format, other.format); + let other_write_modifiers: IndexSet<_> = other.write_modifiers.keys().copied().collect(); + GfxFormat { + format: self.format, + read_modifiers: self + .read_modifiers + .intersection(&other_write_modifiers) + .copied() + .collect(), + write_modifiers: self + .write_modifiers + .iter() + .filter(|(m, _)| other.read_modifiers.contains(*m)) + .map(|(&k, v)| (k, v.clone())) + .collect(), + supports_shm: self.supports_shm && other.supports_shm, + } + } +} + +pub fn cross_intersect_formats( + local: &AHashMap, + remote: &AHashMap, +) -> AHashMap { + let mut res = AHashMap::new(); + for lf in local.values() { + if let Some(rf) = remote.get(&lf.format.drm) { + let f = lf.cross_intersect(rf); + if !f.read_modifiers.is_empty() || !f.write_modifiers.is_empty() || f.supports_shm { + res.insert(f.format.drm, f); + } + } + } + res +} + #[derive(Error)] #[error(transparent)] pub struct GfxError(pub Box); diff --git a/src/ifs.rs b/src/ifs.rs index e70a1555..85993cf3 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -28,6 +28,7 @@ pub mod jay_popup_ext_manager_v1; pub mod jay_randr; pub mod jay_reexec; pub mod jay_render_ctx; +pub mod jay_screencast; pub mod jay_screenshot; pub mod jay_seat_events; pub mod jay_select_toplevel; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 1b179165..f2dac3d4 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -15,6 +15,7 @@ use { jay_randr::JayRandr, jay_reexec::JayReexec, jay_render_ctx::JayRenderCtx, + jay_screencast::JayScreencast, jay_screenshot::JayScreenshot, jay_seat_events::JaySeatEvents, jay_select_toplevel::{JaySelectToplevel, JayToplevelSelector}, @@ -41,7 +42,9 @@ use { thiserror::Error, }; +pub const CREATE_EI_SESSION_SINCE: Version = Version(5); pub const SCREENSHOT_SPLITUP_SINCE: Version = Version(6); +pub const GET_TOPLEVEL_SINCE: Version = Version(12); pub struct JayCompositorGlobal { name: GlobalName, @@ -92,6 +95,7 @@ pub struct Cap; impl Cap { pub const NONE: u16 = 0; + pub const WINDOW_CAPTURE: u16 = 1; pub const SELECT_WORKSPACE: u16 = 2; } @@ -99,7 +103,7 @@ impl JayCompositor { fn send_capabilities(&self) { self.client.event(Capabilities { self_id: self.id, - cap: &[Cap::NONE, Cap::SELECT_WORKSPACE], + cap: &[Cap::NONE, Cap::WINDOW_CAPTURE, Cap::SELECT_WORKSPACE], }); } @@ -342,6 +346,13 @@ impl JayCompositorRequestHandler for JayCompositor { Ok(()) } + fn create_screencast(&self, req: CreateScreencast, _slf: &Rc) -> Result<(), Self::Error> { + let sc = Rc::new_cyclic(|slf| JayScreencast::new(req.id, &self.client, slf, self.version)); + track!(self.client, sc); + self.client.add_client_obj(&sc)?; + Ok(()) + } + fn select_toplevel(&self, req: SelectToplevel, _slf: &Rc) -> Result<(), Self::Error> { let obj = JaySelectToplevel::new(&self.client, req.id, self.version); track!(self.client, obj); diff --git a/src/ifs/jay_screencast.rs b/src/ifs/jay_screencast.rs new file mode 100644 index 00000000..9ec24eb1 --- /dev/null +++ b/src/ifs/jay_screencast.rs @@ -0,0 +1,796 @@ +use { + crate::{ + allocator::{AllocatorError, BO_USE_LINEAR, BO_USE_RENDERING, BufferObject}, + client::{Client, ClientError}, + cmm::cmm_description::ColorDescription, + format::XRGB8888, + gfx_api::{ + AcquireSync, BufferResv, GfxContext, GfxError, GfxFramebuffer, GfxTexture, ReleaseSync, + }, + ifs::{jay_output::JayOutput, jay_toplevel::JayToplevel, wl_buffer::WlBufferStorage}, + leaks::Tracker, + object::{Object, Version}, + scale::Scale, + state::State, + tree::{ + LatchListener, OutputNode, ToplevelNode, Transform, WorkspaceNode, WorkspaceNodeId, + }, + utils::{ + clonecell::{CloneCell, UnsafeCellCloneSafe}, + errorfmt::ErrorFmt, + event_listener::EventListener, + numcell::NumCell, + option_ext::OptionExt, + }, + video::{INVALID_MODIFIER, LINEAR_MODIFIER, dmabuf::DmaBuf}, + wire::{JayScreencastId, jay_screencast::*}, + }, + ahash::AHashSet, + std::{ + cell::{Cell, RefCell}, + ops::DerefMut, + rc::{Rc, Weak}, + }, + thiserror::Error, +}; + +pub async fn perform_toplevel_screencasts(state: Rc) { + loop { + let screencast = state.pending_toplevel_screencasts.pop().await; + screencast.perform_toplevel_screencast(); + } +} + +pub async fn perform_screencast_realloc(state: Rc) { + loop { + let screencast = state + .pending_screencast_reallocs_or_reconfigures + .pop() + .await; + screencast.realloc_or_reconfigure_scheduled.set(false); + match state.render_ctx.get() { + None => screencast.do_destroy(), + Some(ctx) => { + if let Err(e) = screencast.realloc(&ctx) { + screencast.client.error(e); + } + } + } + } +} + +pub const CLIENT_BUFFERS_SINCE: Version = Version(7); + +pub struct JayScreencast { + pub id: JayScreencastId, + pub version: Version, + pub client: Rc, + pub tracker: Tracker, + config_serial: NumCell, + config_acked: Cell, + buffers_serial: NumCell, + buffers_acked: Cell, + buffers: RefCell>, + missed_frame: Cell, + target: CloneCell>, + destroyed: Cell, + running: Cell, + show_all: Cell, + show_workspaces: RefCell>, + linear: Cell, + pending: Pending, + need_realloc_or_reconfigure: Cell, + realloc_or_reconfigure_scheduled: Cell, + latch_listener: EventListener, +} + +#[derive(Clone)] +enum Target { + Output(Rc), + Toplevel(Rc), +} + +impl LatchListener for JayScreencast { + fn after_latch(self: Rc, _on: &OutputNode, _tearing: bool) { + self.schedule_toplevel_screencast(); + } +} + +unsafe impl UnsafeCellCloneSafe for Target {} + +enum PendingTarget { + Output(Rc), + Toplevel(Rc), +} + +#[derive(Default)] +struct Pending { + linear: Cell>, + running: Cell>, + target: Cell>>, + show_all: Cell>, + show_workspaces: RefCell>>, + clear_buffers: Cell, + buffers: RefCell>>, +} + +struct ScreencastBuffer { + _bo: Option>, + dmabuf: Option, + fb: Rc, + free: bool, +} + +impl JayScreencast { + pub fn shows_ws(&self, ws: &WorkspaceNode) -> bool { + if self.show_all.get() { + return true; + } + for &id in &*self.show_workspaces.borrow() { + if id == ws.id { + return true; + } + } + false + } + + pub fn new( + id: JayScreencastId, + client: &Rc, + slf: &Weak, + version: Version, + ) -> Self { + Self { + id, + version, + client: client.clone(), + tracker: Default::default(), + config_serial: Default::default(), + config_acked: Cell::new(true), + buffers_serial: Default::default(), + buffers_acked: Cell::new(true), + buffers: Default::default(), + missed_frame: Cell::new(false), + target: Default::default(), + destroyed: Cell::new(false), + running: Cell::new(false), + show_all: Cell::new(false), + show_workspaces: Default::default(), + linear: Cell::new(false), + pending: Default::default(), + need_realloc_or_reconfigure: Cell::new(false), + realloc_or_reconfigure_scheduled: Cell::new(false), + latch_listener: EventListener::new(slf.clone()), + } + } + + fn schedule_toplevel_screencast(self: &Rc) { + if !self.running.get() { + return; + } + self.client + .state + .pending_toplevel_screencasts + .push(self.clone()); + } + + fn perform_toplevel_screencast(&self) { + if self.destroyed.get() || !self.running.get() { + return; + } + let Some(target) = self.target.get() else { + return; + }; + let Target::Toplevel(tl) = target else { + log::warn!("Tried to perform window screencast for output screencast"); + return; + }; + let scale = match tl.tl_data().workspace.get() { + None => Scale::default(), + Some(w) => w.output.get().global.persistent.scale.get(), + }; + let mut buffer = self.buffers.borrow_mut(); + for (idx, buffer) in buffer.deref_mut().iter_mut().enumerate() { + if buffer.free { + let res = buffer.fb.render_node( + AcquireSync::Implicit, + ReleaseSync::Implicit, + self.client.state.color_manager.srgb_gamma22(), + &*tl, + &self.client.state, + Some(tl.node_absolute_position()), + scale, + true, + true, + false, + false, + Transform::None, + None, + self.client.state.color_manager.srgb_linear(), + ); + match res { + Ok(_) => { + self.client.event(Ready { + self_id: self.id, + idx: idx as _, + }); + buffer.free = false; + return; + } + Err(e) => { + log::error!("Could not perform window copy: {}", ErrorFmt(e)); + break; + } + } + } + } + self.missed_frame.set(true); + self.client.event(MissedFrame { self_id: self.id }) + } + + fn send_buffers(&self) { + self.buffers_acked.set(false); + let serial = self.buffers_serial.fetch_add(1) + 1; + let buffers = self.buffers.borrow_mut(); + for buffer in buffers.iter() { + let Some(dmabuf) = &buffer.dmabuf else { + log::error!("Trying to send buffers but buffers are client allocated"); + self.do_destroy(); + return; + }; + for plane in &dmabuf.planes { + self.client.event(Plane { + self_id: self.id, + fd: plane.fd.clone(), + offset: plane.offset, + stride: plane.stride, + }); + } + self.client.event(Buffer { + self_id: self.id, + format: dmabuf.format.drm, + modifier: dmabuf.modifier, + width: dmabuf.width, + height: dmabuf.height, + }); + } + self.client.event(BuffersDone { + self_id: self.id, + serial, + }); + } + + fn send_config(&self) { + self.need_realloc_or_reconfigure.set(false); + self.config_acked.set(false); + let serial = self.config_serial.fetch_add(1) + 1; + if let Some(target) = self.target.get() { + let (width, height) = target_size(Some(&target)); + if self.version >= CLIENT_BUFFERS_SINCE { + self.client.event(ConfigSize { + self_id: self.id, + width, + height, + }); + } + if let Target::Output(output) = target { + self.client.event(ConfigOutput { + self_id: self.id, + linear_id: output.id.raw(), + }); + } + } + self.client.event(ConfigAllowAllWorkspaces { + self_id: self.id, + allow_all: self.show_all.get() as _, + }); + for &ws in self.show_workspaces.borrow_mut().iter() { + self.client.event(ConfigAllowWorkspace { + self_id: self.id, + linear_id: ws.raw(), + }); + } + self.client.event(ConfigUseLinearBuffers { + self_id: self.id, + use_linear: self.linear.get() as _, + }); + self.client.event(ConfigRunning { + self_id: self.id, + running: self.running.get() as _, + }); + self.client.event(ConfigDone { + self_id: self.id, + serial, + }); + } + + pub fn copy_texture( + &self, + on: &OutputNode, + texture: &Rc, + cd: &Rc, + resv: Option<&Rc>, + acquire_sync: &AcquireSync, + release_sync: ReleaseSync, + render_hardware_cursors: bool, + x_off: i32, + y_off: i32, + size: Option<(i32, i32)>, + ) { + if !self.running.get() { + return; + } + if !self.show_all.get() { + let ws = match on.workspace.get() { + Some(ws) => ws, + _ => return, + }; + if !self.show_workspaces.borrow_mut().contains(&ws.id) { + return; + } + } + let mut buffer = self.buffers.borrow_mut(); + for (idx, buffer) in buffer.deref_mut().iter_mut().enumerate() { + if buffer.free { + let res = self.client.state.perform_screencopy( + texture, + resv, + acquire_sync, + release_sync, + cd, + &buffer.fb, + AcquireSync::Implicit, + ReleaseSync::Implicit, + Transform::None, + self.client.state.color_manager.srgb_gamma22(), + on.global.pos.get(), + render_hardware_cursors, + x_off, + y_off, + size, + on.global.persistent.transform.get(), + on.global.persistent.scale.get(), + ); + match res { + Ok(_) => { + self.client.event(Ready { + self_id: self.id, + idx: idx as _, + }); + buffer.free = false; + return; + } + Err(e) => { + log::error!("Could not perform screencopy: {}", ErrorFmt(e)); + break; + } + } + } + } + self.missed_frame.set(true); + self.client.event(MissedFrame { self_id: self.id }) + } + + fn detach(&self) { + self.latch_listener.detach(); + if let Some(target) = self.target.take() { + match target { + Target::Output(output) => { + output.remove_screencast(self); + } + Target::Toplevel(tl) => { + let data = tl.tl_data(); + data.jay_screencasts.remove(&(self.client.id, self.id)); + } + } + } + } + + pub fn do_destroy(&self) { + self.detach(); + self.buffers.borrow_mut().clear(); + self.destroyed.set(true); + self.client.event(Destroyed { self_id: self.id }); + } + + pub fn schedule_realloc_or_reconfigure(self: &Rc) { + self.need_realloc_or_reconfigure.set(true); + if !self.realloc_or_reconfigure_scheduled.replace(true) { + self.client + .state + .pending_screencast_reallocs_or_reconfigures + .push(self.clone()); + } + } + + fn realloc(&self, ctx: &Rc) -> Result<(), JayScreencastError> { + if self.destroyed.get() { + return Ok(()); + } + if self.version < CLIENT_BUFFERS_SINCE { + if self.buffers_acked.get() { + return self.do_realloc(ctx); + } + } else { + if self.config_acked.get() { + self.send_config(); + } + } + Ok(()) + } + + fn do_realloc(&self, ctx: &Rc) -> Result<(), JayScreencastError> { + self.need_realloc_or_reconfigure.set(false); + let mut buffers = vec![]; + let formats = ctx.formats(); + let format = match formats.get(&XRGB8888.drm) { + Some(f) => f, + _ => return Err(JayScreencastError::XRGB8888), + }; + if let Some(target) = self.target.get() { + let (width, height) = target_size(Some(&target)); + let num = 3; + for _ in 0..num { + if width == 0 || height == 0 { + continue; + } + let mut usage = BO_USE_RENDERING; + let modifiers = match self.linear.get() { + true if format.write_modifiers.contains_key(&LINEAR_MODIFIER) => { + vec![LINEAR_MODIFIER] + } + true if format.write_modifiers.contains_key(&INVALID_MODIFIER) => { + usage |= BO_USE_LINEAR; + vec![INVALID_MODIFIER] + } + true => return Err(JayScreencastError::Modifier), + false if format.write_modifiers.is_empty() => { + return Err(JayScreencastError::XRGB8888Writing); + } + false => format.write_modifiers.keys().copied().collect(), + }; + let buffer = ctx.allocator().create_bo( + &self.client.state.dma_buf_ids, + width, + height, + format.format, + &modifiers, + usage, + )?; + let fb = ctx.clone().dmabuf_img(buffer.dmabuf())?.to_framebuffer()?; + buffers.push(ScreencastBuffer { + dmabuf: Some(buffer.dmabuf().clone()), + _bo: Some(buffer), + fb, + free: true, + }); + } + } + *self.buffers.borrow_mut() = buffers; + self.send_buffers(); + self.damage(); + Ok(()) + } + + fn damage(&self) { + if let Some(target) = self.target.get() { + let rect = match target { + Target::Output(o) => o.global.pos.get(), + Target::Toplevel(t) => { + if !t.node_visible() { + return; + } + t.node_absolute_position() + } + }; + self.client.state.damage(rect); + } + } + + pub fn update_latch_listener(&self) { + let Some(Target::Toplevel(tl)) = self.target.get() else { + return; + }; + let data = tl.tl_data(); + if data.visible.get() { + self.latch_listener.attach(&data.output().latch_event); + } else { + self.latch_listener.detach(); + } + } +} + +impl JayScreencastRequestHandler for JayScreencast { + type Error = JayScreencastError; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } + + fn set_output(&self, req: SetOutput, _slf: &Rc) -> Result<(), Self::Error> { + let output = if req.output.is_some() { + Some(PendingTarget::Output(self.client.lookup(req.output)?)) + } else { + None + }; + if self.destroyed.get() { + return Ok(()); + } + self.pending.target.set(Some(output)); + Ok(()) + } + + fn set_allow_all_workspaces( + &self, + req: SetAllowAllWorkspaces, + _slf: &Rc, + ) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + self.pending.show_all.set(Some(req.allow_all != 0)); + Ok(()) + } + + fn allow_workspace(&self, req: AllowWorkspace, _slf: &Rc) -> Result<(), Self::Error> { + let ws = self.client.lookup(req.workspace)?; + if self.destroyed.get() { + return Ok(()); + } + let mut sw = self.pending.show_workspaces.borrow_mut(); + let sw = sw.get_or_insert_default_ext(); + if let Some(ws) = ws.workspace.get() { + sw.insert(ws.id); + } + Ok(()) + } + + fn touch_allowed_workspaces( + &self, + _req: TouchAllowedWorkspaces, + _slf: &Rc, + ) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + self.pending + .show_workspaces + .borrow_mut() + .get_or_insert_default_ext(); + Ok(()) + } + + fn set_use_linear_buffers( + &self, + req: SetUseLinearBuffers, + _slf: &Rc, + ) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + self.pending.linear.set(Some(req.use_linear != 0)); + Ok(()) + } + + fn set_running(&self, req: SetRunning, _slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + self.pending.running.set(Some(req.running != 0)); + Ok(()) + } + + fn configure(&self, _req: Configure, slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + + let mut need_realloc_or_reconfigure = false; + + if let Some(target) = self.pending.target.take() { + self.detach(); + let mut new_target = None; + if let Some(new) = target { + match new { + PendingTarget::Output(o) => { + let Some(o) = o.output.node() else { + self.do_destroy(); + return Ok(()); + }; + o.add_screencast(slf); + new_target = Some(Target::Output(o)); + } + PendingTarget::Toplevel(t) => { + if t.destroyed.get() { + self.do_destroy(); + return Ok(()); + } + let t = t.toplevel.clone(); + let data = t.tl_data(); + data.jay_screencasts + .set((self.client.id, self.id), slf.clone()); + if data.visible.get() { + self.latch_listener.attach(&data.output().latch_event); + } + new_target = Some(Target::Toplevel(t)); + } + } + } + if target_size(new_target.as_ref()) != target_size(self.target.get().as_ref()) { + need_realloc_or_reconfigure = true; + } + self.target.set(new_target); + } + if let Some(linear) = self.pending.linear.take() { + if self.linear.replace(linear) != linear { + need_realloc_or_reconfigure = true; + } + } + if self.pending.clear_buffers.take() { + self.buffers.borrow_mut().clear(); + } + for buffer in self.pending.buffers.borrow_mut().drain(..) { + self.buffers.borrow_mut().push(ScreencastBuffer { + _bo: None, + dmabuf: None, + fb: buffer, + free: true, + }); + } + let mut capture_rules_changed = false; + if let Some(show_all) = self.pending.show_all.take() { + self.show_all.set(show_all); + capture_rules_changed = true; + } + if let Some(new_workspaces) = self.pending.show_workspaces.borrow_mut().take() { + *self.show_workspaces.borrow_mut() = new_workspaces; + capture_rules_changed = true; + } + if let Some(running) = self.pending.running.take() { + self.running.set(running); + } + + if need_realloc_or_reconfigure { + slf.schedule_realloc_or_reconfigure(); + } + + if capture_rules_changed && let Some(Target::Output(o)) = self.target.get() { + o.screencast_changed(); + } + + if self.running.get() { + self.damage(); + } + + Ok(()) + } + + fn ack_buffers(&self, req: AckBuffers, slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + if req.serial == self.buffers_serial.get() { + self.buffers_acked.set(true); + if self.need_realloc_or_reconfigure.get() { + slf.schedule_realloc_or_reconfigure(); + } + } + Ok(()) + } + + fn ack_config(&self, req: AckConfig, slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + if req.serial == self.config_serial.get() { + self.config_acked.set(true); + if self.need_realloc_or_reconfigure.get() { + slf.schedule_realloc_or_reconfigure(); + } + } + Ok(()) + } + + fn release_buffer(&self, req: ReleaseBuffer, _slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() || !self.buffers_acked.get() { + return Ok(()); + } + let idx = req.idx as usize; + if idx >= self.buffers.borrow_mut().len() { + return Err(JayScreencastError::OutOfBounds(req.idx)); + } + self.buffers.borrow_mut()[idx].free = true; + if self.missed_frame.replace(false) { + self.damage(); + } + Ok(()) + } + + fn set_toplevel(&self, req: SetToplevel, _slf: &Rc) -> Result<(), Self::Error> { + let toplevel = if req.id.is_some() { + Some(PendingTarget::Toplevel(self.client.lookup(req.id)?)) + } else { + None + }; + if self.destroyed.get() { + return Ok(()); + } + self.pending.target.set(Some(toplevel)); + Ok(()) + } + + fn clear_buffers(&self, _req: ClearBuffers, _slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + self.pending.clear_buffers.set(true); + Ok(()) + } + + fn add_buffer(&self, req: AddBuffer, _slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + let buffer = self.client.lookup(req.buffer)?; + if let Some(WlBufferStorage::Dmabuf { img, .. }) = &*buffer.storage.borrow() { + match img.clone().to_framebuffer() { + Ok(fb) => self.pending.buffers.borrow_mut().push(fb), + Err(e) => { + log::warn!( + "Could not turn GfxImage into GfxFramebuffer: {}", + ErrorFmt(e) + ); + self.do_destroy(); + } + } + return Ok(()); + } + Err(JayScreencastError::NotDmabuf) + } +} + +object_base! { + self = JayScreencast; + version = self.version; +} + +impl Object for JayScreencast { + fn break_loops(&self) { + self.detach(); + } +} + +dedicated_add_obj!(JayScreencast, JayScreencastId, screencasts); + +#[derive(Debug, Error)] +pub enum JayScreencastError { + #[error(transparent)] + ClientError(Box), + #[error("Buffer index {0} is out-of-bounds")] + OutOfBounds(u32), + #[error(transparent)] + AllocatorError(#[from] AllocatorError), + #[error(transparent)] + GfxError(#[from] GfxError), + #[error("Render context does not support XRGB8888 format")] + XRGB8888, + #[error("Render context does not support XRGB8888 format for rendering")] + XRGB8888Writing, + #[error("Render context supports neither linear or invalid modifier")] + Modifier, + #[error("Buffer is not a dmabuf")] + NotDmabuf, +} +efrom!(JayScreencastError, ClientError); + +fn target_size(target: Option<&Target>) -> (i32, i32) { + if let Some(target) = target { + return match target { + Target::Output(o) => o.global.pixel_size(), + Target::Toplevel(t) => t.tl_data().desired_pixel_size(), + }; + } + (0, 0) +} diff --git a/src/macros.rs b/src/macros.rs index 21a3f30e..e125433e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -11,6 +11,33 @@ macro_rules! efrom { }; } +macro_rules! usr_object_base { + ($self:ident = $oname:ident = $iname:ident; version = $version:expr;) => { + impl crate::wl_usr::usr_object::UsrObjectBase for $oname { + fn id(&$self) -> crate::object::ObjectId { + $self.id.into() + } + + fn version(&$self) -> crate::object::Version { + $version + } + + fn handle_event( + $self: std::rc::Rc, + con: &crate::wl_usr::UsrCon, + event: u32, + parser: crate::utils::buffd::MsgParser<'_, '_>, + ) -> Result<(), crate::wl_usr::UsrConError> { + $self.handle_event_impl(con, event, parser) + } + + fn interface(&$self) -> crate::object::Interface { + crate::wire::$iname + } + } + }; +} + macro_rules! object_base { ($self:ident = $oname:ident; version = $version:expr;) => { impl crate::object::ObjectBase for $oname { @@ -171,6 +198,40 @@ macro_rules! linear_ids { }; } +macro_rules! shared_ids { + ($id:ident) => { + shared_ids!($id, u32); + }; + ($id:ident, $ty:ty) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] + pub struct $id($ty); + + impl $id { + #[expect(dead_code)] + pub fn raw(&self) -> $ty { + self.0 + } + + #[expect(dead_code)] + pub fn from_raw(id: $ty) -> Self { + Self(id) + } + } + + impl From<$ty> for $id { + fn from(id: $ty) -> Self { + Self(id) + } + } + + impl std::fmt::Display for $id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } + } + }; +} + #[expect(unused_macros)] macro_rules! bitor { ($name:ident) => { @@ -637,6 +698,90 @@ macro_rules! ei_object_base { }; } +macro_rules! pw_opcodes { + ($name:ident; $($var:ident = $val:expr,)*) => { + #[derive(Copy, Clone, Debug)] + pub enum $name { + $( + $var, + )* + } + + #[allow(clippy::allow_attributes, dead_code)] + impl $name { + pub fn from_id(id: u8) -> Option { + let v = match id { + $($val => Self::$var,)* + _ => return None, + }; + Some(v) + } + + pub fn name(self) -> &'static str { + match self { + $(Self::$var => stringify!($var),)* + } + } + } + + impl crate::pipewire::pw_object::PwOpcode for $name { + fn id(&self) -> u8 { + match self { + $(Self::$var => $val,)* + } + } + } + } +} + +macro_rules! pw_object_base { + ($name:ident, $if:expr, $events:ident; $($event:ident => $method:ident,)*) => { + impl crate::pipewire::pw_object::PwObjectBase for $name { + fn data(&self) -> &crate::pipewire::pw_object::PwObjectData { + &self.data + } + + fn interface(&self) -> &str { + $if + } + + fn handle_msg( + self: std::rc::Rc, + opcode: u8, + parser: crate::pipewire::pw_parser::PwParser<'_>, + ) -> Result<(), crate::pipewire::pw_object::PwObjectError> { + match $events::from_id(opcode) { + None => Err(crate::pipewire::pw_object::PwObjectError { + interface: $if, + source: crate::pipewire::pw_object::PwObjectErrorType::UnknownEvent(opcode), + }), + Some(m) => { + let (res, method) = match m { + $( + $events::$event => (self.$method(parser), stringify!($event)), + )* + }; + match res { + Ok(_) => Ok(()), + Err(source) => Err(crate::pipewire::pw_object::PwObjectError { + interface: $if, + source: crate::pipewire::pw_object::PwObjectErrorType::EventError { + method, + source: Box::new(source), + }, + }), + } + } + } + } + + fn event_name(&self, opcode: u8) -> Option<&'static str> { + $events::from_id(opcode).map(|o| o.name()) + } + } + } +} + macro_rules! logical_to_client_wire_scale { ($client:expr, $($field:expr),+ $(,)?) => { #[expect(clippy::allow_attributes)] diff --git a/src/main.rs b/src/main.rs index 22c1f8bf..89a8ce7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,6 +92,8 @@ mod logind; mod object; mod output_schedule; mod pango; +mod pipewire; +mod portal; mod pr_caps; mod rect; mod renderer; @@ -119,6 +121,7 @@ mod wire; mod wire_dbus; mod wire_ei; mod wire_xcon; +mod wl_usr; mod xcon; mod xwayland; diff --git a/src/pipewire.rs b/src/pipewire.rs new file mode 100644 index 00000000..af77bc66 --- /dev/null +++ b/src/pipewire.rs @@ -0,0 +1,7 @@ +pub mod pw_con; +pub mod pw_formatter; +pub mod pw_ifs; +pub mod pw_mem; +pub mod pw_object; +pub mod pw_parser; +pub mod pw_pod; diff --git a/src/pipewire/pw_con.rs b/src/pipewire/pw_con.rs new file mode 100644 index 00000000..b994acd3 --- /dev/null +++ b/src/pipewire/pw_con.rs @@ -0,0 +1,453 @@ +use { + crate::{ + async_engine::{AsyncEngine, SpawnedFuture}, + io_uring::{IoUring, IoUringError}, + pipewire::{ + pw_formatter::{PwFormatter, format}, + pw_ifs::{ + pw_client::{PwClient, PwClientMethods}, + pw_client_node::{ + PW_CLIENT_NODE_FACTORY, PW_CLIENT_NODE_INTERFACE, PW_CLIENT_NODE_VERSION, + PwClientNode, + }, + pw_core::{PW_CORE_VERSION, PwCore, PwCoreMethods}, + pw_registry::{PW_REGISTRY_VERSION, PwRegistry}, + }, + pw_mem::PwMemPool, + pw_object::{PwObject, PwObjectData, PwObjectError, PwOpcode}, + pw_parser::{PwParser, PwParserError}, + }, + utils::{ + bitfield::Bitfield, + bufio::{BufIo, BufIoError, BufIoIncoming, BufIoMessage}, + clonecell::CloneCell, + copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, + hash_map_ext::HashMapExt, + numcell::NumCell, + oserror::{OsError, OsErrorExt2}, + xrd::xrd, + }, + }, + std::{ + cell::{Cell, RefCell}, + fmt::Display, + io::Write, + rc::{Rc, Weak}, + }, + thiserror::Error, + uapi::{OwnedFd, c}, +}; + +#[derive(Debug, Error)] +pub enum PwConError { + #[error("Could not create a unix socket")] + CreateSocket(#[source] OsError), + #[error("Could not connect to the pipewire daemon")] + ConnectSocket(#[source] IoUringError), + #[error(transparent)] + BufIoError(#[from] BufIoError), + #[error("Server did not sent a required fd")] + MissingFd, + #[error("XDG_RUNTIME_DIR is not set")] + XrdNotSet, + #[error(transparent)] + PwObjectError(#[from] PwObjectError), + #[error(transparent)] + PwParserError(#[from] PwParserError), +} + +pub struct PwConHolder { + pub con: Rc, + outgoing: Cell>>, + incoming: Cell>>, +} + +pub struct PwCon { + send_seq: NumCell, + pub io: Rc, + holder: CloneCell>, + dead: Cell, + pub objects: CopyHashMap>, + pub ids: RefCell, + pub mem: PwMemPool, + pub ring: Rc, + pub eng: Rc, + pub owner: CloneCell>>, + + registry_generation: Cell, + ack_registry_generation: Cell, +} + +pub trait PwConOwner { + fn killed(&self) {} +} + +impl PwCon { + pub fn create_client_node(self: &Rc, props: &[(String, String)]) -> Rc { + let node = Rc::new(PwClientNode { + data: self.proxy_data(), + con: self.clone(), + ios: Default::default(), + owner: CloneCell::new(None), + ports: Default::default(), + port_out_free: RefCell::new(Default::default()), + port_in_free: RefCell::new(Default::default()), + activation: Default::default(), + transport_in: Cell::new(None), + transport_out: Default::default(), + activations: Default::default(), + }); + if !self.dead.get() { + self.objects.set(node.data.id, node.clone()); + } + self.create_object( + PW_CLIENT_NODE_FACTORY, + PW_CLIENT_NODE_INTERFACE, + PW_CLIENT_NODE_VERSION, + props, + node.data.id, + ); + node.send_update(); + node + } + + pub fn destroy_obj(&self, obj: &impl PwObject) { + obj.break_loops(); + self.send2(0, "core", PwCoreMethods::Destroy, |f| { + f.write_struct(|f| { + f.write_uint(obj.data().id); + }); + }); + self.objects.remove(&obj.data().id); + } + + pub fn kill(&self) { + for obj in self.objects.lock().drain_values() { + obj.break_loops(); + } + self.io.shutdown(); + self.dead.set(true); + if let Some(con) = self.holder.get().upgrade() { + con.outgoing.take(); + con.incoming.take(); + } + if let Some(owner) = self.owner.take() { + owner.killed(); + } + } + + pub fn id(&self) -> u32 { + self.ids.borrow_mut().acquire() + } + + pub fn proxy_data(&self) -> PwObjectData { + PwObjectData { + id: self.id(), + bound_id: Cell::new(None), + sync_id: Default::default(), + } + } + + pub fn send(&self, proxy: &P, opcode: O, f: F) + where + P: PwObject, + O: PwOpcode, + F: FnOnce(&mut PwFormatter), + { + self.send2(proxy.data().id, proxy.interface(), opcode, f); + } + + pub fn send2(&self, id: u32, interface: &str, opcode: O, f: F) + where + O: PwOpcode, + F: FnOnce(&mut PwFormatter), + { + if self.dead.get() { + return; + } + let mut buf = self.io.buf(); + let mut fds = vec![]; + format( + &mut buf, + &mut fds, + id, + opcode.id(), + self.send_seq.fetch_add(1), + |fmt| { + f(fmt); + if self.ack_registry_generation.get() != self.registry_generation.get() { + let generation = self.registry_generation.get(); + fmt.write_struct(|f| { + f.write_id(FOOTER_REGISTRY_GENERATION); + f.write_struct(|f| { + f.write_ulong(generation); + }); + }); + self.ack_registry_generation.set(generation); + } + }, + ); + if log::log_enabled!(log::Level::Trace) { + log::trace!("CALL {}@{}: `{:?}`:", interface, id, opcode); + let mut parser = PwParser::new(&buf[16..buf.len()], &fds); + while parser.len() > 0 { + log::trace!("{:#?}", parser.read_pod().unwrap()); + } + } + self.io.send(BufIoMessage { + fds, + buf: buf.unwrap(), + }); + } + + #[expect(dead_code)] + pub fn sync(&self, p: &P) { + let seq = p.data().sync_id.fetch_add(1) + 1; + self.send2(0, "core", PwCoreMethods::Sync, |f| { + f.write_struct(|f| { + f.write_uint(p.data().id); + f.write_uint(seq); + }); + }); + } + + pub fn send_hello(&self) { + self.send2(0, "core", PwCoreMethods::Hello, |f| { + f.write_struct(|f| f.write_int(PW_CORE_VERSION)); + }); + } + + #[expect(dead_code)] + pub fn get_registry(self: &Rc) -> Rc { + let registry = Rc::new(PwRegistry { + data: self.proxy_data(), + _con: self.clone(), + }); + if !self.dead.get() { + self.objects.set(registry.data.id, registry.clone()); + } + self.send2(0, "core", PwCoreMethods::GetRegistry, |f| { + f.write_struct(|f| { + f.write_int(PW_REGISTRY_VERSION); + f.write_uint(registry.data.id); + }); + }); + registry + } + + pub fn create_object( + &self, + factory: &str, + ty: &str, + version: i32, + props: &[(String, String)], + new_id: u32, + ) { + self.send2(0, "core", PwCoreMethods::CreateObject, |f| { + f.write_struct(|f| { + f.write_string(factory); + f.write_string(ty); + f.write_int(version); + f.write_struct(|f| { + f.write_int(props.len() as _); + for (key, val) in props { + f.write_string(key); + f.write_string(val); + } + }); + f.write_uint(new_id); + }); + }); + } + + pub fn send_properties(&self) { + self.send2(1, "client", PwClientMethods::UpdateProperties, |f| { + f.write_struct(|f| { + f.write_struct(|f| { + f.write_int(1); + f.write_string("application.name"); + f.write_string("jay-portal"); + }); + }); + }); + } + + async fn handle_outgoing(self: Rc) { + if let Err(e) = self.io.clone().outgoing().await { + log::error!("{}", ErrorFmt(e)); + } + self.kill(); + } + + async fn handle_incoming(self: Rc) { + let incoming = Incoming { + incoming: self.io.clone().incoming(), + con: self.clone(), + buf: vec![], + fds: vec![], + }; + incoming.run().await; + } +} + +impl Drop for PwConHolder { + fn drop(&mut self) { + self.con.owner.take(); + self.con.kill(); + } +} + +impl PwConHolder { + pub async fn new(eng: &Rc, ring: &Rc) -> Result, PwConError> { + let fd = uapi::socket(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0) + .map(Rc::new) + .map_os_err(PwConError::CreateSocket)?; + let mut addr = c::sockaddr_un { + sun_family: c::AF_UNIX as _, + ..uapi::pod_zeroed() + }; + let xrd = match xrd() { + Some(xrd) => xrd, + _ => return Err(PwConError::XrdNotSet), + }; + { + let mut path = uapi::as_bytes_mut(&mut addr.sun_path[..]); + let _ = write!(path, "{}/pipewire-0", xrd); + } + if let Err(e) = ring.connect(&fd, &addr).await { + return Err(PwConError::ConnectSocket(e)); + } + let io = Rc::new(BufIo::new(&fd, ring)); + let data = Rc::new(PwCon { + send_seq: Default::default(), + io, + holder: Default::default(), + dead: Cell::new(false), + objects: Default::default(), + ids: Default::default(), + mem: Default::default(), + ring: ring.clone(), + eng: eng.clone(), + owner: Default::default(), + registry_generation: Cell::new(0), + ack_registry_generation: Cell::new(0), + }); + let core = Rc::new(PwCore { + data: data.proxy_data(), + con: data.clone(), + }); + let client = Rc::new(PwClient { + data: data.proxy_data(), + _con: data.clone(), + }); + data.objects.set(0, core.clone()); + data.objects.set(1, client.clone()); + data.send_hello(); + data.send_properties(); + let con = Rc::new(PwConHolder { + outgoing: Cell::new(Some( + eng.spawn("pw outgoing", data.clone().handle_outgoing()), + )), + incoming: Cell::new(Some( + eng.spawn("pw incoming", data.clone().handle_incoming()), + )), + con: data, + }); + con.con.holder.set(Rc::downgrade(&con)); + Ok(con) + } +} + +struct Incoming { + con: Rc, + incoming: BufIoIncoming, + buf: Vec, + fds: Vec>, +} + +impl Incoming { + async fn run(mut self) { + loop { + if let Err(e) = self.handle_msg().await { + log::error!("Could not handle incoming message: {}", ErrorFmt(e)); + self.con.kill(); + return; + } + } + } + + async fn handle_msg(&mut self) -> Result<(), PwConError> { + self.buf.clear(); + self.incoming.fill_msg_buf(16, &mut self.buf).await?; + let id: u32 = uapi::pod_read(&self.buf[0..4]).unwrap(); + let p2: u32 = uapi::pod_read(&self.buf[4..8]).unwrap(); + let opcode = (p2 >> 24) as u8; + let size = (p2 & 0xff_ffff) as usize; + let _seq: u32 = uapi::pod_read(&self.buf[8..12]).unwrap(); + let n_fds: u32 = uapi::pod_read(&self.buf[12..16]).unwrap(); + for _ in 0..n_fds { + match self.incoming.fds.pop_front() { + Some(fd) => self.fds.push(fd), + _ => return Err(PwConError::MissingFd), + } + } + self.buf.clear(); + self.incoming.fill_msg_buf(size, &mut self.buf).await?; + if let Err(e) = self.handle_msg_data(id, opcode) { + log::warn!("Could not handle incoming message: {}", ErrorFmt(e)); + } + self.fds.clear(); + Ok(()) + } + + fn handle_msg_data(&self, id: u32, opcode: u8) -> Result<(), PwConError> { + let parser = PwParser::new(&self.buf, &self.fds); + { + let mut parser = parser; + parser.skip()?; + if parser.len() > 0 { + let s1 = parser.read_struct()?; + let mut p2 = s1.fields; + while p2.len() > 0 { + let opcode = p2.read_id()?; + let s2 = p2.read_struct()?; + if opcode == FOOTER_REGISTRY_GENERATION { + let mut p3 = s2.fields; + let generation = p3.read_ulong()?; + self.con.registry_generation.set(generation); + log::debug!("registry generation = {}", generation); + } else { + log::warn!("Unknown message footer: {}", opcode); + } + } + } + } + if let Some(obj) = self.con.objects.get(&id) { + 'log: { + if log::log_enabled!(log::Level::Trace) { + let s; + let op: &dyn Display = match obj.event_name(opcode) { + Some(e) => { + s = e; + if e == "Done" && obj.interface() == "core" { + break 'log; + } + &s + } + _ => &opcode, + }; + log::trace!("EVENT {}@{}: `{}`:", obj.interface(), obj.data().id, op); + let mut parser = parser; + while parser.len() > 0 { + log::trace!("{:#?}", parser.read_pod().unwrap()); + } + } + } + obj.handle_msg(opcode, parser)?; + } + Ok(()) + } +} + +const FOOTER_REGISTRY_GENERATION: u32 = 0; diff --git a/src/pipewire/pw_formatter.rs b/src/pipewire/pw_formatter.rs new file mode 100644 index 00000000..c10eb02a --- /dev/null +++ b/src/pipewire/pw_formatter.rs @@ -0,0 +1,312 @@ +use { + crate::{ + pipewire::pw_pod::{ + PW_TYPE_Array, PW_TYPE_Bitmap, PW_TYPE_Bool, PW_TYPE_Bytes, PW_TYPE_Choice, + PW_TYPE_Double, PW_TYPE_Fd, PW_TYPE_Float, PW_TYPE_Fraction, PW_TYPE_Id, PW_TYPE_Int, + PW_TYPE_Long, PW_TYPE_None, PW_TYPE_Object, PW_TYPE_Rectangle, PW_TYPE_String, + PW_TYPE_Struct, PwChoiceType, PwPodObjectType, PwPodType, PwPropFlag, + }, + utils::buf::DynamicBuf, + }, + std::rc::Rc, + uapi::OwnedFd, +}; + +pub struct PwFormatter<'a> { + data: &'a mut DynamicBuf, + fds: &'a mut Vec>, + array: bool, + first: bool, +} + +impl<'a> PwFormatter<'a> { + pub fn write_bool(&mut self, b: bool) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&4u32)); + self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Bool.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&(b as u32))); + if !self.array { + self.data.extend_from_slice(uapi::as_bytes(&0u32)); + } + self.first = false; + } + + pub fn write_id(&mut self, id: u32) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&4u32)); + self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Id.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&id)); + if !self.array { + self.data.extend_from_slice(uapi::as_bytes(&0u32)); + } + self.first = false; + } + + pub fn write_object(&mut self, ty: PwPodObjectType, id: u32, f: F) + where + F: FnOnce(&mut PwObjectFormatter), + { + let start = self.data.len(); + self.data.extend_from_slice(uapi::as_bytes(&0u32)); + self.data + .extend_from_slice(uapi::as_bytes(&PW_TYPE_Object.0)); + self.data.extend_from_slice(uapi::as_bytes(&ty.0)); + self.data.extend_from_slice(uapi::as_bytes(&id)); + let mut fmt = PwObjectFormatter { + data: self.data, + fds: self.fds, + }; + f(&mut fmt); + let len = (self.data.len() - start - 8) as u32; + self.data[start..start + 4].copy_from_slice(uapi::as_bytes(&len)); + } + + pub fn write_uint(&mut self, int: u32) { + self.write_int(int as _) + } + + pub fn write_int(&mut self, int: i32) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&4u32)); + self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Int.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&int)); + if !self.array { + self.data.extend_from_slice(uapi::as_bytes(&0u32)); + } + self.first = false; + } + + pub fn write_ulong(&mut self, long: u64) { + self.write_long(long as _) + } + + pub fn write_long(&mut self, long: i64) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&8u32)); + self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Long.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&long)); + self.first = false; + } + + #[expect(dead_code)] + pub fn write_float(&mut self, float: f32) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&4u32)); + self.data + .extend_from_slice(uapi::as_bytes(&PW_TYPE_Float.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&float)); + if !self.array { + self.data.extend_from_slice(uapi::as_bytes(&0u32)); + } + self.first = false; + } + + #[expect(dead_code)] + pub fn write_double(&mut self, double: f64) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&8u32)); + self.data + .extend_from_slice(uapi::as_bytes(&PW_TYPE_Double.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&double)); + } + + pub fn write_string + ?Sized>(&mut self, s: &S) { + let s = s.as_ref(); + self.data + .extend_from_slice(uapi::as_bytes(&(s.len() as u32 + 1))); + self.data + .extend_from_slice(uapi::as_bytes(&PW_TYPE_String.0)); + self.data.extend_from_slice(s); + self.data.push(0); + self.pad(); + } + + #[expect(dead_code)] + pub fn write_bytes(&mut self, s: &[u8]) { + self.data + .extend_from_slice(uapi::as_bytes(&(s.len() as u32))); + self.data + .extend_from_slice(uapi::as_bytes(&PW_TYPE_Bytes.0)); + self.data.extend_from_slice(s); + self.pad(); + } + + pub fn write_rectangle(&mut self, width: u32, height: u32) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&8u32)); + self.data + .extend_from_slice(uapi::as_bytes(&PW_TYPE_Rectangle.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&width)); + self.data.extend_from_slice(uapi::as_bytes(&height)); + self.first = false; + } + + #[expect(dead_code)] + pub fn write_fraction(&mut self, num: i32, denom: i32) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&8u32)); + self.data + .extend_from_slice(uapi::as_bytes(&PW_TYPE_Fraction.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&num)); + self.data.extend_from_slice(uapi::as_bytes(&denom)); + self.first = false; + } + + pub fn write_none(&mut self) { + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&0u32)); + self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_None.0)); + } + self.first = false; + } + + #[expect(dead_code)] + pub fn write_bitmap(&mut self, s: &[u8]) { + self.data + .extend_from_slice(uapi::as_bytes(&(s.len() as u32))); + self.data + .extend_from_slice(uapi::as_bytes(&PW_TYPE_Bitmap.0)); + self.data.extend_from_slice(s); + self.pad(); + } + + pub fn write_fd(&mut self, fd: &Rc) { + let pos = self.fds.len() as u64; + self.fds.push(fd.clone()); + if !self.array || self.first { + self.data.extend_from_slice(uapi::as_bytes(&8u32)); + self.data.extend_from_slice(uapi::as_bytes(&PW_TYPE_Fd.0)); + } + self.data.extend_from_slice(uapi::as_bytes(&pos)); + self.first = false; + } + + pub fn write_struct(&mut self, f: F) + where + F: FnOnce(&mut PwFormatter), + { + self.write_compound(PW_TYPE_Struct, |fmt| { + let mut fmt = PwFormatter { + data: fmt.data, + fds: fmt.fds, + array: false, + first: false, + }; + f(&mut fmt); + }); + } + + #[expect(dead_code)] + pub fn write_array(&mut self, f: F) + where + F: FnOnce(&mut PwFormatter), + { + self.write_compound(PW_TYPE_Array, |fmt| { + fmt.write_array_body(f); + }); + self.pad(); + } + + fn write_array_body(&mut self, f: F) + where + F: FnOnce(&mut PwFormatter), + { + let mut fmt = PwFormatter { + data: self.data, + fds: self.fds, + array: true, + first: true, + }; + f(&mut fmt); + if fmt.first { + fmt.write_none(); + } + } + + pub fn write_choice(&mut self, ty: PwChoiceType, flags: u32, f: F) + where + F: FnOnce(&mut PwFormatter), + { + self.write_compound(PW_TYPE_Choice, |fmt| { + fmt.data.extend_from_slice(uapi::as_bytes(&ty.0)); + fmt.data.extend_from_slice(uapi::as_bytes(&flags)); + fmt.write_array_body(f); + }); + self.pad(); + } + + fn write_compound(&mut self, ty: PwPodType, f: F) + where + F: FnOnce(&mut PwFormatter), + { + let start = self.data.len(); + self.data.extend_from_slice(uapi::as_bytes(&0u32)); + self.data.extend_from_slice(uapi::as_bytes(&ty.0)); + f(self); + let len = (self.data.len() - start - 8) as u32; + self.data[start..start + 4].copy_from_slice(uapi::as_bytes(&len)); + } + + fn pad(&mut self) { + let todo = self.data.len().wrapping_neg() & 7; + self.data.extend_from_slice(&uapi::as_bytes(&0u64)[..todo]); + } +} + +pub struct PwObjectFormatter<'a> { + data: &'a mut DynamicBuf, + fds: &'a mut Vec>, +} + +impl<'a> PwObjectFormatter<'a> { + pub fn write_property(&mut self, key: u32, flags: PwPropFlag, f: F) + where + F: FnOnce(&mut PwFormatter), + { + self.data.extend_from_slice(uapi::as_bytes(&key)); + self.data.extend_from_slice(uapi::as_bytes(&flags.0)); + let mut fmt = PwFormatter { + data: self.data, + fds: self.fds, + array: false, + first: false, + }; + f(&mut fmt); + } +} + +pub fn format( + buf: &mut DynamicBuf, + fds: &mut Vec>, + id: u32, + opcode: u8, + seq: u32, + f: F, +) where + F: FnOnce(&mut PwFormatter), +{ + buf.clear(); + buf.extend_from_slice(uapi::as_bytes(&id)); + buf.extend_from_slice(uapi::as_bytes(&0u32)); + buf.extend_from_slice(uapi::as_bytes(&seq)); + buf.extend_from_slice(uapi::as_bytes(&0u32)); + let mut fmt = PwFormatter { + data: buf, + fds, + array: false, + first: false, + }; + f(&mut fmt); + let p2 = (buf.len() - 16) as u32 | ((opcode as u32) << 24); + buf[4..8].copy_from_slice(uapi::as_bytes(&p2)); + let nfds = fds.len() as u32; + buf[12..16].copy_from_slice(uapi::as_bytes(&nfds)); +} diff --git a/src/pipewire/pw_ifs.rs b/src/pipewire/pw_ifs.rs new file mode 100644 index 00000000..91651f17 --- /dev/null +++ b/src/pipewire/pw_ifs.rs @@ -0,0 +1,4 @@ +pub mod pw_client; +pub mod pw_client_node; +pub mod pw_core; +pub mod pw_registry; diff --git a/src/pipewire/pw_ifs/pw_client.rs b/src/pipewire/pw_ifs/pw_client.rs new file mode 100644 index 00000000..361592a4 --- /dev/null +++ b/src/pipewire/pw_ifs/pw_client.rs @@ -0,0 +1,61 @@ +use { + crate::pipewire::{ + pw_con::PwCon, + pw_object::{PwObject, PwObjectData}, + pw_parser::{PwParser, PwParserError}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pw_opcodes! { + PwClientMethods; + + Error = 1, + UpdateProperties = 2, + GetPermissions = 3, + UpdatePermissions = 4, +} + +pw_opcodes! { + PwClientEvents; + + Info = 0, + Permissions = 1, +} + +pub struct PwClient { + pub data: PwObjectData, + pub _con: Rc, +} + +impl PwClient { + fn handle_info(&self, mut p: PwParser<'_>) -> Result<(), PwClientError> { + let s1 = p.read_struct()?; + let mut p2 = s1.fields; + let _id = p2.read_int()?; + let _change_mask = p2.read_long()?; + let props = p2.read_dict_struct()?; + log::debug!("Pipewire properties: {:#?}", props); + Ok(()) + } + + fn handle_permissions(&self, _p: PwParser<'_>) -> Result<(), PwClientError> { + Ok(()) + } +} + +pw_object_base! { + PwClient, "client", PwClientEvents; + + Info => handle_info, + Permissions => handle_permissions, +} + +impl PwObject for PwClient {} + +#[derive(Debug, Error)] +pub enum PwClientError { + #[error(transparent)] + PwParserError(#[from] PwParserError), +} diff --git a/src/pipewire/pw_ifs/pw_client_node.rs b/src/pipewire/pw_ifs/pw_client_node.rs new file mode 100644 index 00000000..9b42c31f --- /dev/null +++ b/src/pipewire/pw_ifs/pw_client_node.rs @@ -0,0 +1,892 @@ +#![allow(non_upper_case_globals)] + +use { + crate::{ + async_engine::SpawnedFuture, + format::{Format, pipewire_format, pw_formats}, + pipewire::{ + pw_con::PwCon, + pw_mem::{PwMemError, PwMemMap, PwMemSlice, PwMemTyped}, + pw_object::{PwObject, PwObjectData}, + pw_parser::{PwParser, PwParserError}, + pw_pod::{ + PW_CHOICE_Enum, PW_CHOICE_Flags, PW_NODE_ACTIVATION_FINISHED, + PW_NODE_ACTIVATION_NOT_TRIGGERED, PW_NODE_ACTIVATION_TRIGGERED, PW_OBJECT_Format, + PW_OBJECT_ParamBuffers, PW_OBJECT_ParamMeta, PW_PROP_DONT_FIXATE, PW_TYPE_Long, + PwIoType, PwPod, PwPodFraction, PwPodObject, PwPodRectangle, PwPropFlag, + SPA_DATA_DmaBuf, SPA_DATA_FLAG_READABLE, SPA_DATA_MemFd, SPA_DATA_MemPtr, + SPA_DIRECTION_INPUT, SPA_DIRECTION_OUTPUT, SPA_FORMAT_VIDEO_format, + SPA_FORMAT_VIDEO_framerate, SPA_FORMAT_VIDEO_modifier, SPA_FORMAT_VIDEO_size, + SPA_FORMAT_mediaSubtype, SPA_FORMAT_mediaType, SPA_IO_Buffers, SPA_META_Bitmap, + SPA_META_Busy, SPA_META_Control, SPA_META_Cursor, SPA_META_Header, + SPA_META_VideoCrop, SPA_META_VideoDamage, SPA_NODE_BUFFERS_FLAG_ALLOC, + SPA_NODE_COMMAND_Pause, SPA_NODE_COMMAND_Start, SPA_NODE_COMMAND_Suspend, + SPA_PARAM_BUFFERS_blocks, SPA_PARAM_BUFFERS_buffers, SPA_PARAM_BUFFERS_dataType, + SPA_PARAM_Buffers, SPA_PARAM_EnumFormat, SPA_PARAM_Format, SPA_PARAM_INFO, + SPA_PARAM_INFO_READ, SPA_PARAM_INFO_SERIAL, SPA_PARAM_META_size, + SPA_PARAM_META_type, SPA_PARAM_Meta, SPA_PORT_FLAG, + SPA_PORT_FLAG_CAN_ALLOC_BUFFERS, SpaDataFlags, SpaDataType, SpaDirection, + SpaIoType, SpaMediaSubtype, SpaMediaType, SpaMetaType, SpaNodeBuffersFlags, + SpaNodeCommand, SpaParamType, SpaVideoFormat, pw_node_activation, spa_chunk, + spa_io_buffers, spa_meta_bitmap, spa_meta_busy, spa_meta_cursor, spa_meta_header, + spa_meta_region, + }, + }, + utils::{ + bitfield::Bitfield, buf::TypedBuf, clonecell::CloneCell, copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, option_ext::OptionExt, + }, + video::{Modifier, dmabuf::DmaBuf}, + }, + std::{ + cell::{Cell, RefCell}, + rc::Rc, + sync::atomic::Ordering::{Relaxed, Release}, + }, + thiserror::Error, + uapi::OwnedFd, +}; + +pw_opcodes! { + PwClientNodeMethods; + + GetNode = 1, + Update = 2, + PortUpdate = 3, + SetActive = 4, + Event = 5, + PortBuffers = 6, +} + +pw_opcodes! { + PwClientNodeEvents; + + Transport = 0, + SetParam = 1, + SetIo = 2, + Event = 3, + Command = 4, + AddPort = 5, + RemovePort = 6, + PortSetParam = 7, + PortUseBuffers = 8, + PortSetIo = 9, + SetActivation = 10, + PortSetMixInfo = 11, +} + +pub trait PwClientNodeOwner { + fn port_format_changed(&self, port: &Rc) { + let _ = port; + } + fn use_buffers(self: Rc, port: &Rc) { + let _ = port; + } + fn start(self: Rc) {} + fn pause(self: Rc) {} + fn suspend(self: Rc) {} + fn bound_id(&self, id: u32) { + let _ = id; + } +} + +bitflags! { + PwClientNodePortChanges: u32; + + CHANGED_SUPPORTED_PARAMS = 1 << 0, +} + +bitflags! { + PwClientNodePortSupportedMetas: u32; + + SUPPORTED_META_HEADER = 1 << 0, + SUPPORTED_META_BUSY = 1 << 1, + SUPPORTED_META_VIDEO_CROP = 1 << 2, +} + +pub struct PwClientNodePort { + pub node: Rc, + + pub direction: SpaDirection, + pub id: u32, + + pub _destroyed: Cell, + + pub negotiated_format: RefCell, + pub supported_formats: RefCell, + pub supported_metas: Cell, + pub can_alloc_buffers: Cell, + + pub buffers: RefCell>>, + + pub buffer_config: RefCell, + + pub io_buffers: CloneCell>>>, + + pub serial: Cell, +} + +#[derive(Copy, Clone, Debug, Default)] +pub struct PwClientNodeBufferConfig { + pub num_buffers: Option, + pub planes: Option, + pub data_type: SpaDataType, +} + +pub struct PwClientNodeBuffer { + pub _meta_header: Option>>, + pub _meta_busy: Option>>, + pub meta_video_crop: Option>>, + pub chunks: Vec>>, + pub _slices: Vec>, +} + +#[derive(Clone, Debug)] +pub struct PwClientNodePortSupportedFormat { + pub format: &'static Format, + pub modifiers: Vec, +} + +#[derive(Clone, Debug, Default)] +pub struct PwClientNodePortSupportedFormats { + pub media_type: Option, + pub media_sub_type: Option, + pub video_size: Option, + pub formats: Vec, +} + +#[derive(Clone, Debug, Default)] +pub struct PwClientNodePortFormat { + pub media_type: Option, + pub media_sub_type: Option, + pub video_size: Option, + pub format: Option<&'static Format>, + pub modifiers: Option>, + pub framerate: Option, +} + +pub struct PwClientNode { + pub data: PwObjectData, + pub con: Rc, + pub ios: CopyHashMap>, + + pub owner: CloneCell>>, + + pub ports: CopyHashMap<(SpaDirection, u32), Rc>, + + pub port_out_free: RefCell, + pub port_in_free: RefCell, + + pub activation: CloneCell>>>, + pub transport_in: Cell>>, + pub transport_out: CloneCell>>, + + pub activations: CopyHashMap>, +} + +pub struct PwNodeActivation { + pub activation: Rc>, + pub fd: Rc, +} + +// pub struct PwNodeBuffer { +// pub width: i32, +// pub height: i32, +// pub stride: i32, +// pub offset: i32, +// pub fd: Rc, +// } + +pub const PW_CLIENT_NODE_FACTORY: &str = "client-node"; +pub const PW_CLIENT_NODE_INTERFACE: &str = "PipeWire:Interface:ClientNode"; +pub const PW_CLIENT_NODE_VERSION: i32 = 4; + +#[expect(dead_code)] +const PW_CLIENT_NODE_UPDATE_PARAMS: u32 = 1 << 0; +const PW_CLIENT_NODE_UPDATE_INFO: u32 = 1 << 1; + +const SPA_NODE_CHANGE_MASK_FLAGS: u64 = 1 << 0; +#[expect(dead_code)] +const SPA_NODE_CHANGE_MASK_PROPS: u64 = 1 << 1; +const SPA_NODE_CHANGE_MASK_PARAMS: u64 = 1 << 2; + +const PW_CLIENT_NODE_PORT_UPDATE_PARAMS: u32 = 1 << 0; +const PW_CLIENT_NODE_PORT_UPDATE_INFO: u32 = 1 << 1; + +const SPA_PORT_CHANGE_MASK_FLAGS: u64 = 1 << 0; +const SPA_PORT_CHANGE_MASK_RATE: u64 = 1 << 1; +#[expect(dead_code)] +const SPA_PORT_CHANGE_MASK_PROPS: u64 = 1 << 2; +const SPA_PORT_CHANGE_MASK_PARAMS: u64 = 1 << 3; + +impl PwClientNode { + pub fn send_update(&self) { + self.con.send(self, PwClientNodeMethods::Update, |f| { + f.write_struct(|f| { + f.write_uint(PW_CLIENT_NODE_UPDATE_INFO); + f.write_uint(0); + f.write_struct(|f| { + f.write_uint(0); + f.write_uint(1); + f.write_ulong(SPA_NODE_CHANGE_MASK_PARAMS | SPA_NODE_CHANGE_MASK_FLAGS); + f.write_ulong(0); + f.write_uint(0); + f.write_uint(0); + }); + }); + }); + } + + pub fn send_active(&self, active: bool) { + self.con.send(self, PwClientNodeMethods::SetActive, |f| { + f.write_struct(|f| { + f.write_bool(active); + }); + }); + } + + pub fn create_port( + self: &Rc, + output: bool, + supported_formats: PwClientNodePortSupportedFormats, + num_buffers: Option, + ) -> Rc { + let (ids, direction) = match output { + true => (&self.port_out_free, SPA_DIRECTION_OUTPUT), + false => (&self.port_in_free, SPA_DIRECTION_INPUT), + }; + let port = Rc::new(PwClientNodePort { + node: self.clone(), + direction, + id: ids.borrow_mut().acquire(), + _destroyed: Cell::new(false), + negotiated_format: Default::default(), + supported_formats: RefCell::new(supported_formats), + supported_metas: Cell::new(PwClientNodePortSupportedMetas::none()), + can_alloc_buffers: Cell::new(false), + buffers: RefCell::new(vec![]), + buffer_config: RefCell::new(PwClientNodeBufferConfig { + num_buffers, + planes: None, + data_type: SPA_DATA_DmaBuf, + }), + io_buffers: Default::default(), + serial: Cell::new(false), + }); + self.ports.set((direction, port.id), port.clone()); + port + } + + pub fn send_port_output_buffers(&self, port: &PwClientNodePort, buffers: &[DmaBuf]) { + self.con.send(self, PwClientNodeMethods::PortBuffers, |f| { + f.write_struct(|f| { + // direction + f.write_uint(port.direction.0); + // id + f.write_uint(port.id); + // mix_id + f.write_int(-1); + // n_buffers + f.write_uint(buffers.len() as _); + for buffer in buffers { + // n_datas + f.write_uint(buffer.planes.len() as _); + for plane in &buffer.planes { + // type + f.write_id(SPA_DATA_DmaBuf.0); + // fd + f.write_fd(&plane.fd); + // flags + f.write_uint(SPA_DATA_FLAG_READABLE.0); + // offset + f.write_uint(plane.offset); + // size + f.write_uint(plane.stride * buffer.height as u32); + } + } + }); + }); + } + + pub fn send_port_update(&self, port: &PwClientNodePort, fixate: bool) { + port.serial.set(!port.serial.get()); + let serial = match port.serial.get() { + true => SPA_PARAM_INFO_SERIAL, + false => SPA_PARAM_INFO::none(), + }; + self.con.send(self, PwClientNodeMethods::PortUpdate, |f| { + f.write_struct(|f| { + // direction + f.write_uint(port.direction.0); + // id + f.write_uint(port.id); + // change flags + f.write_uint(PW_CLIENT_NODE_PORT_UPDATE_PARAMS | PW_CLIENT_NODE_PORT_UPDATE_INFO); + let sm = port.supported_metas.get(); + let mut metas = vec![]; + if sm.contains(SUPPORTED_META_HEADER) { + metas.push((SPA_META_Header, size_of::())); + } + if sm.contains(SUPPORTED_META_BUSY) { + metas.push((SPA_META_Busy, size_of::())); + } + if sm.contains(SUPPORTED_META_VIDEO_CROP) { + metas.push((SPA_META_VideoCrop, size_of::())); + } + let sf = &*port.supported_formats.borrow(); + let num_formats = sf.formats.len() as u32; + let bc = &*port.buffer_config.borrow(); + let num_params = metas.len() as u32 + num_formats + 1; + + // num params + f.write_uint(num_params); + for format in &sf.formats { + f.write_object(PW_OBJECT_Format, SPA_PARAM_EnumFormat.0, |f| { + if let Some(mt) = sf.media_type { + f.write_property(SPA_FORMAT_mediaType.0, PwPropFlag::none(), |f| { + f.write_id(mt.0); + }); + } + if let Some(mst) = sf.media_sub_type { + f.write_property(SPA_FORMAT_mediaSubtype.0, PwPropFlag::none(), |f| { + f.write_id(mst.0); + }); + } + f.write_property(SPA_FORMAT_VIDEO_format.0, PwPropFlag::none(), |f| { + f.write_choice(PW_CHOICE_Enum, 0, |f| { + f.write_id(pipewire_format(format.format).0); + f.write_id(pipewire_format(format.format).0); + }); + }); + f.write_property( + SPA_FORMAT_VIDEO_modifier.0, + if fixate { + PwPropFlag::none() + } else { + PW_PROP_DONT_FIXATE + }, + |f| { + f.write_choice(PW_CHOICE_Enum, 0, |f| { + f.write_ulong(format.modifiers[0]); + for modifier in &format.modifiers { + f.write_ulong(*modifier); + } + }); + }, + ); + if let Some(vs) = sf.video_size { + f.write_property(SPA_FORMAT_VIDEO_size.0, PwPropFlag::none(), |f| { + f.write_choice(PW_CHOICE_Enum, 0, |f| { + f.write_rectangle(vs.width, vs.height); + f.write_rectangle(vs.width, vs.height); + }); + }); + } + }); + } + f.write_object(PW_OBJECT_ParamBuffers, SPA_PARAM_Buffers.0, |f| { + if let Some(num_buffers) = bc.num_buffers { + f.write_property(SPA_PARAM_BUFFERS_buffers.0, PwPropFlag::none(), |f| { + f.write_uint(num_buffers as _); + }); + } + if let Some(planes) = bc.planes { + f.write_property(SPA_PARAM_BUFFERS_blocks.0, PwPropFlag::none(), |f| { + f.write_uint(planes as _); + }); + } + f.write_property(SPA_PARAM_BUFFERS_dataType.0, PwPropFlag::none(), |f| { + f.write_choice(PW_CHOICE_Flags, 0, |f| { + f.write_uint(1 << bc.data_type.0); + }); + }); + }); + for (key, size) in metas { + f.write_object(PW_OBJECT_ParamMeta, SPA_PARAM_Meta.0, |f| { + f.write_property(SPA_PARAM_META_type.0, PwPropFlag::none(), |f| { + f.write_id(key.0); + }); + f.write_property(SPA_PARAM_META_size.0, PwPropFlag::none(), |f| { + f.write_uint(size as u32); + }); + }); + } + f.write_struct(|f| { + // change mask + f.write_ulong( + SPA_PORT_CHANGE_MASK_FLAGS + // | SPA_PORT_CHANGE_MASK_PROPS + | SPA_PORT_CHANGE_MASK_PARAMS + | SPA_PORT_CHANGE_MASK_RATE, + ); + let mut flags = SPA_PORT_FLAG::none(); + if port.can_alloc_buffers.get() { + flags = SPA_PORT_FLAG_CAN_ALLOC_BUFFERS; + } + // flags + f.write_ulong(flags.0); + // rate num + f.write_int(0); + // rate denom + f.write_int(1); + // num props + f.write_int(0); + let num_params = 3; + // num params + f.write_uint(num_params); + f.write_id(SPA_PARAM_EnumFormat.0); + f.write_uint((SPA_PARAM_INFO_READ | serial).0); + f.write_id(SPA_PARAM_Buffers.0); + f.write_uint((SPA_PARAM_INFO_READ | serial).0); + f.write_id(SPA_PARAM_Meta.0); + f.write_uint(SPA_PARAM_INFO_READ.0); + }); + }); + }); + } + + fn handle_set_param(&self, _p: PwParser<'_>) -> Result<(), PwClientNodeError> { + Ok(()) + } + + fn handle_set_io(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { + let s = p.read_struct()?; + let mut p2 = s.fields; + let id = PwIoType(p2.read_id()?); + let memid = p2.read_uint()?; + let offset = p2.read_uint()?; + let size = p2.read_uint()?; + log::debug!("set io {:?}", id); + if memid == !0 { + self.ios.remove(&id); + } else { + let map = match self.con.mem.map(memid, offset, size) { + Ok(m) => m, + Err(e) => { + log::error!("Could not map memory from the pool: {}", ErrorFmt(e)); + return Ok(()); + } + }; + self.ios.set(id, map); + } + Ok(()) + } + + fn handle_event(&self, _p: PwParser<'_>) -> Result<(), PwClientNodeError> { + Ok(()) + } + + fn handle_command(self: &Rc, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { + let s1 = p.read_struct()?; + let mut p1 = s1.fields; + let obj = p1.read_object()?; + match SpaNodeCommand(obj.id) { + SPA_NODE_COMMAND_Start => { + if let Some(owner) = self.owner.get() { + owner.start(); + } + } + SPA_NODE_COMMAND_Pause => { + if let Some(owner) = self.owner.get() { + owner.pause(); + } + } + SPA_NODE_COMMAND_Suspend => { + if let Some(owner) = self.owner.get() { + owner.suspend(); + } + } + v => { + log::warn!("Unhandled node command {:?}", v); + } + } + Ok(()) + } + + fn handle_add_port(&self, _p: PwParser<'_>) -> Result<(), PwClientNodeError> { + Ok(()) + } + + fn handle_remove_port(&self, _p: PwParser<'_>) -> Result<(), PwClientNodeError> { + Ok(()) + } + + fn port_set_format( + &self, + port: &Rc, + obj: Option>, + ) -> Result<(), PwClientNodeError> { + let mut obj = match obj { + Some(obj) => obj, + _ => { + port.negotiated_format.take(); + return Ok(()); + } + }; + let mut format = PwClientNodePortFormat::default(); + if let Some(mt) = obj.get_param(SPA_FORMAT_mediaType.0)? { + format.media_type = Some(SpaMediaType(mt.pod.get_id()?)); + } + if let Some(mt) = obj.get_param(SPA_FORMAT_mediaSubtype.0)? { + format.media_sub_type = Some(SpaMediaSubtype(mt.pod.get_id()?)); + } + if let Some(mt) = obj.get_param(SPA_FORMAT_VIDEO_size.0)? { + format.video_size = Some(mt.pod.get_rectangle()?); + } + if let Some(mt) = obj.get_param(SPA_FORMAT_VIDEO_format.0)? + && let Some(fmt) = pw_formats().get(&SpaVideoFormat(mt.pod.get_id()?)) + { + format.format = Some(*fmt); + } + if let Some(mt) = obj.get_param(SPA_FORMAT_VIDEO_modifier.0)? + && let PwPod::Choice(mods) = mt.pod + { + let mut p1 = mods.elements.elements; + p1.read_pod_body_packed(PW_TYPE_Long, 8)?; + while p1.len() > 0 { + let modifier = p1.read_pod_body_packed(PW_TYPE_Long, 8)?; + if let PwPod::Long(modifier) = modifier { + format + .modifiers + .get_or_insert_default_ext() + .push(modifier as u64); + } + } + } + if let Some(mt) = obj.get_param(SPA_FORMAT_VIDEO_framerate.0)? { + format.framerate = Some(mt.pod.get_fraction()?); + } + *port.negotiated_format.borrow_mut() = format; + Ok(()) + } + + fn handle_port_set_param(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { + let s1 = p.read_struct()?; + let mut p1 = s1.fields; + let direction = SpaDirection(p1.read_uint()?); + let port_id = p1.read_uint()?; + let id = SpaParamType(p1.read_id()?); + let _flags = p1.read_int()?; + let obj = p1.read_object_opt()?; + let port = self.get_port(direction, port_id)?; + match id { + SPA_PARAM_Format => { + self.port_set_format(&port, obj)?; + if let Some(owner) = self.owner.get() { + owner.port_format_changed(&port); + } + } + _ => { + log::warn!( + "port_set_param: Ignoring unexpected port parameter {:?}", + id + ); + } + } + Ok(()) + } + + fn handle_port_use_buffers(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { + let s1 = p.read_struct()?; + let mut p1 = s1.fields; + let direction = SpaDirection(p1.read_uint()?); + let port_id = p1.read_uint()?; + let _mix_id = p1.read_int()?; + let buffer_flags = SpaNodeBuffersFlags(p1.read_uint()?); + let n_buffers = p1.read_uint()?; + let port = self.get_port(direction, port_id)?; + + let mut res = vec![]; + + for _ in 0..n_buffers { + let mem_id = p1.read_uint()?; + let offset = p1.read_uint()?; + let size = p1.read_uint()?; + let n_metas = p1.read_uint()?; + + let mut meta_header = Default::default(); + let mut meta_video_crop = Default::default(); + let mut meta_busy = Default::default(); + let mut chunks = vec![]; + let mut slices = vec![]; + + let mem = self.con.mem.map(mem_id, offset, size)?; + + log::debug!(" mem_id={}, offset={}, size={}", mem_id, offset, size); + log::debug!(" n_metas={}", n_metas); + + let mut offset = 0; + + for _ in 0..n_metas { + let ty = SpaMetaType(p1.read_id()?); + let size = p1.read_uint()? as usize; + + match ty { + SPA_META_Header => { + let header = mem.typed_at::(offset); + meta_header = Some(header); + } + SPA_META_VideoCrop => { + let crop = mem.typed_at::(offset); + meta_video_crop = Some(crop); + } + SPA_META_VideoDamage => { + let _video_damage = mem.typed_at::(offset); + } + SPA_META_Bitmap => { + let _bitmap = mem.typed_at::(offset); + } + SPA_META_Cursor => { + let _cursor = mem.typed_at::(offset); + } + SPA_META_Control => {} + SPA_META_Busy => { + let busy = mem.typed_at::(offset); + meta_busy = Some(busy); + } + _ => {} + } + + offset += (size + 7) & !7; + } + + let n_datas = p1.read_uint()?; + + log::debug!(" offset = {}, n_datas={}", offset, n_datas); + + for _ in 0..n_datas { + let ty = SpaDataType(p1.read_id()?); + let data_id = p1.read_uint()?; + let _flags = SpaDataFlags(p1.read_uint()?); + let mapoffset = p1.read_uint()?; + let maxsize = p1.read_uint()?; + + chunks.push(mem.typed_at(offset)); + offset += size_of::(); + + if !buffer_flags.contains(SPA_NODE_BUFFERS_FLAG_ALLOC) { + if ty == SPA_DATA_MemPtr { + let offset = data_id as usize; + slices.push(mem.slice(offset..offset + maxsize as usize)); + } else if ty == SPA_DATA_MemFd { + let mem = self.con.mem.map(data_id, mapoffset, maxsize)?; + slices.push(mem.slice(0..maxsize as usize)); + } + } + } + + res.push(Rc::new(PwClientNodeBuffer { + _meta_header: meta_header, + _meta_busy: meta_busy, + meta_video_crop, + chunks, + _slices: slices, + })); + } + + *port.buffers.borrow_mut() = res; + + if let Some(owner) = self.owner.get() { + owner.use_buffers(&port); + } + + Ok(()) + } + + fn handle_port_set_io(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { + let s = p.read_struct()?; + let mut p2 = s.fields; + let direction = SpaDirection(p2.read_uint()?); + let port_id = p2.read_uint()?; + let mix_id = p2.read_uint()?; + let id = SpaIoType(p2.read_id()?); + let mem_id = p2.read_uint()?; + let offset = p2.read_uint()?; + let size = p2.read_uint()?; + let port = self.get_port(direction, port_id)?; + match id { + SPA_IO_Buffers if mix_id == 0 => { + if mem_id == !0 { + port.io_buffers.take(); + } else { + let io_buffers = self + .con + .mem + .map(mem_id, offset, size)? + .typed::(); + unsafe { + io_buffers.read().buffer_id.store(!0, Relaxed); + io_buffers.read().status.store(0, Relaxed); + } + port.io_buffers.set(Some(io_buffers)); + } + } + _ => {} + } + Ok(()) + } + + fn handle_transport(self: &Rc, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { + let s = p.read_struct()?; + let mut p2 = s.fields; + let readfd = p2.read_fd()?; + let writefd = p2.read_fd()?; + let memid = p2.read_uint()?; + let offset = p2.read_uint()?; + let size = p2.read_uint()?; + let map = match self.con.mem.map(memid, offset, size) { + Ok(m) => m, + Err(e) => { + log::error!("Could not map memory from the pool: {}", ErrorFmt(e)); + return Ok(()); + } + }; + let typed = map.typed::(); + self.activation.set(Some(typed.clone())); + self.transport_in.set(Some( + self.con + .eng + .spawn("pw transport in", self.clone().transport_in(typed, readfd)), + )); + self.transport_out.set(Some(writefd)); + Ok(()) + } + + fn handle_set_activation( + self: &Rc, + mut p: PwParser<'_>, + ) -> Result<(), PwClientNodeError> { + let s = p.read_struct()?; + let mut p2 = s.fields; + let node = p2.read_uint()?; + let signalfd = p2.read_fd_opt()?; + if let Some(signalfd) = signalfd { + let memid = p2.read_uint()?; + let offset = p2.read_uint()?; + let size = p2.read_uint()?; + let map = match self.con.mem.map(memid, offset, size) { + Ok(m) => m, + Err(e) => { + log::error!("Could not map memory from the pool: {}", ErrorFmt(e)); + return Ok(()); + } + }; + let typed = map.typed::(); + self.activations.set( + node, + Rc::new(PwNodeActivation { + activation: typed, + fd: signalfd, + }), + ); + } else { + self.activations.remove(&node); + } + Ok(()) + } + + fn get_port( + &self, + direction: SpaDirection, + port_id: u32, + ) -> Result, PwClientNodeError> { + match self.ports.get(&(direction, port_id)) { + Some(p) => Ok(p), + _ => Err(PwClientNodeError::UnknownPort(direction, port_id)), + } + } + + fn handle_port_set_mix_info(&self, mut p: PwParser<'_>) -> Result<(), PwClientNodeError> { + let s1 = p.read_struct()?; + let mut p1 = s1.fields; + let direction = SpaDirection(p1.read_uint()?); + let port_id = p1.read_uint()?; + let mix_id = p1.read_int()?; + let peer_id = p1.read_int()?; + let dict = p1.read_dict_struct()?; + let _port = self.get_port(direction, port_id)?; + log::debug!( + "mix info: mix_id={}, peer_id={}, dict={:#?}", + mix_id, + peer_id, + dict + ); + Ok(()) + } + + async fn transport_in( + self: Rc, + _activation: Rc>, + fd: Rc, + ) { + let mut buf = TypedBuf::::new(); + loop { + if let Err(e) = self.con.ring.read(&fd, buf.buf()).await { + log::error!("Could not read from eventfd: {}", ErrorFmt(e)); + return; + } + if let Some(activation) = self.activation.get() { + let activation = unsafe { activation.read() }; + activation + .status + .store(PW_NODE_ACTIVATION_FINISHED.0, Relaxed); + } + } + } + + pub fn drive(&self) { + for activation in self.activations.lock().values() { + let a = unsafe { activation.activation.read() }; + let required = a.state[0].required.load(Relaxed); + a.state[0].pending.store(required - 1, Relaxed); + if required == 1 { + a.status.store(PW_NODE_ACTIVATION_TRIGGERED.0, Release); + let _ = uapi::eventfd_write(activation.fd.raw(), 1); + } else { + a.status.store(PW_NODE_ACTIVATION_NOT_TRIGGERED.0, Release); + } + } + } +} + +pw_object_base! { + PwClientNode, "client-node", PwClientNodeEvents; + + Transport => handle_transport, + SetParam => handle_set_param, + SetIo => handle_set_io, + Event => handle_event, + Command => handle_command, + AddPort => handle_add_port, + RemovePort => handle_remove_port, + PortSetParam => handle_port_set_param, + PortUseBuffers => handle_port_use_buffers, + PortSetIo => handle_port_set_io, + SetActivation => handle_set_activation, + PortSetMixInfo => handle_port_set_mix_info, +} + +impl PwObject for PwClientNode { + fn bound_id(&self, id: u32) { + if let Some(owner) = self.owner.get() { + owner.bound_id(id); + } + } + + fn break_loops(&self) { + self.owner.take(); + self.ports.clear(); + self.transport_in.take(); + self.transport_out.take(); + } +} + +#[derive(Debug, Error)] +pub enum PwClientNodeError { + #[error(transparent)] + PwParserError(#[from] PwParserError), + #[error(transparent)] + PwMemError(#[from] PwMemError), + #[error("Unknown port {0:?}@{1}")] + UnknownPort(SpaDirection, u32), +} diff --git a/src/pipewire/pw_ifs/pw_core.rs b/src/pipewire/pw_ifs/pw_core.rs new file mode 100644 index 00000000..b2685f26 --- /dev/null +++ b/src/pipewire/pw_ifs/pw_core.rs @@ -0,0 +1,186 @@ +#![allow(non_upper_case_globals)] + +use { + crate::{ + pipewire::{ + pw_con::PwCon, + pw_mem::{PwMem, PwMemType}, + pw_object::{PwObject, PwObjectData}, + pw_parser::{PwParser, PwParserError}, + pw_pod::{SPA_DATA_DmaBuf, SPA_DATA_MemFd, SpaDataType}, + }, + utils::bitflags::BitflagsExt, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct PwCore { + pub data: PwObjectData, + pub con: Rc, +} + +pw_opcodes! { + PwCoreMethods; + + Hello = 1, + Sync = 2, + Pong = 3, + Error = 4, + GetRegistry = 5, + CreateObject = 6, + Destroy = 7, +} + +pw_opcodes! { + PwCoreEvents; + + Info = 0, + Done = 1, + Ping = 2, + Error = 3, + RemoveId = 4, + BoundId = 5, + AddMem = 6, + RemoveMem = 7, +} + +pub const PW_CORE_VERSION: i32 = 3; + +impl PwCore { + pub fn handle_info(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { + let s1 = p1.read_struct()?; + let mut p2 = s1.fields; + let id = p2.read_int()?; + let cookie = p2.read_int()?; + let user_name = p2.read_string()?; + let host_name = p2.read_string()?; + let version_name = p2.read_string()?; + let name = p2.read_string()?; + let change_mask = p2.read_long()?; + let dict = p2.read_dict_struct()?; + log::info!( + "info: id={id}, cookie={cookie}, user_name={user_name}, host_name={host_name}, version_name={version_name}, name={name}, change_mask={change_mask}" + ); + log::info!("dict: {:#?}", dict); + Ok(()) + } + + pub fn handle_done(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { + let s1 = p1.read_struct()?; + let mut p2 = s1.fields; + let id = p2.read_uint()?; + let seq = p2.read_uint()?; + if let Some(obj) = self.con.objects.get(&id) + && obj.data().sync_id.get() <= seq + { + obj.data().sync_id.set(seq); + obj.done(); + } + Ok(()) + } + + pub fn handle_ping(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { + let s1 = p1.read_struct()?; + let mut p2 = s1.fields; + let id = p2.read_int()?; + let seq = p2.read_int()?; + self.con.send(self, PwCoreMethods::Pong, |f| { + f.write_struct(|f| { + f.write_int(id); + f.write_int(seq); + }); + }); + Ok(()) + } + + pub fn handle_error(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { + let s1 = p1.read_struct()?; + let mut p2 = s1.fields; + let id = p2.read_int()?; + let seq = p2.read_int()?; + let res = p2.read_int()?; + let error = p2.read_string()?; + log::info!("error: id={id}, seq={seq}, res={res}, error={error}"); + Ok(()) + } + + pub fn handle_remove_id(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { + let s1 = p1.read_struct()?; + let mut p2 = s1.fields; + let id = p2.read_uint()?; + self.con.objects.remove(&id); + self.con.ids.borrow_mut().release(id); + Ok(()) + } + + pub fn handle_bound_id(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { + let s1 = p1.read_struct()?; + let mut p2 = s1.fields; + let id = p2.read_uint()?; + let bound_id = p2.read_uint()?; + if let Some(obj) = self.con.objects.get(&id) { + obj.data().bound_id.set(Some(bound_id)); + obj.bound_id(bound_id); + } + Ok(()) + } + + pub fn handle_add_mem(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { + let s1 = p1.read_struct()?; + let mut p2 = s1.fields; + let id = p2.read_uint()?; + let ty = SpaDataType(p2.read_id()?); + let fd = p2.read_fd()?; + let flags = p2.read_int()?; + let read = flags.contains(1); + let write = flags.contains(2); + let ty = match ty { + SPA_DATA_MemFd => PwMemType::MemFd, + SPA_DATA_DmaBuf => PwMemType::DmaBuf, + _ => { + log::error!("Ignoring unknown mem type {:?}", ty); + return Ok(()); + } + }; + self.con.mem.mems.set( + id, + Rc::new(PwMem { + _ty: ty, + read, + write, + fd, + }), + ); + Ok(()) + } + + pub fn handle_remove_mem(&self, mut p1: PwParser<'_>) -> Result<(), PwCoreError> { + let s1 = p1.read_struct()?; + let mut p2 = s1.fields; + let id = p2.read_uint()?; + self.con.mem.mems.remove(&id); + Ok(()) + } +} + +pw_object_base! { + PwCore, "core", PwCoreEvents; + + Info => handle_info, + Done => handle_done, + Ping => handle_ping, + Error => handle_error, + RemoveId => handle_remove_id, + BoundId => handle_bound_id, + AddMem => handle_add_mem, + RemoveMem => handle_remove_mem, +} + +impl PwObject for PwCore {} + +#[derive(Debug, Error)] +pub enum PwCoreError { + #[error(transparent)] + PwParserError(#[from] PwParserError), +} diff --git a/src/pipewire/pw_ifs/pw_registry.rs b/src/pipewire/pw_ifs/pw_registry.rs new file mode 100644 index 00000000..f1c54b89 --- /dev/null +++ b/src/pipewire/pw_ifs/pw_registry.rs @@ -0,0 +1,48 @@ +use { + crate::pipewire::{ + pw_con::PwCon, + pw_object::{PwObject, PwObjectData}, + pw_parser::{PwParser, PwParserError}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub const PW_REGISTRY_VERSION: i32 = 3; + +pw_opcodes! { + PwRegistryEvents; + + Global = 0, + GlobalRemove = 1, +} + +pub struct PwRegistry { + pub data: PwObjectData, + pub _con: Rc, +} + +impl PwRegistry { + fn handle_global(&self, _p: PwParser<'_>) -> Result<(), PwRegistryError> { + Ok(()) + } + + fn handle_global_remove(&self, _p: PwParser<'_>) -> Result<(), PwRegistryError> { + Ok(()) + } +} + +pw_object_base! { + PwRegistry, "registry", PwRegistryEvents; + + Global => handle_global, + GlobalRemove => handle_global_remove, +} + +impl PwObject for PwRegistry {} + +#[derive(Debug, Error)] +pub enum PwRegistryError { + #[error(transparent)] + PwParserError(#[from] PwParserError), +} diff --git a/src/pipewire/pw_mem.rs b/src/pipewire/pw_mem.rs new file mode 100644 index 00000000..3d96b0c1 --- /dev/null +++ b/src/pipewire/pw_mem.rs @@ -0,0 +1,155 @@ +use { + crate::utils::{ + copyhashmap::CopyHashMap, + mmap::{Mmapped, mmap}, + oserror::OsError, + page_size::page_size, + ptr_ext::{MutPtrExt, PtrExt}, + }, + std::{marker::PhantomData, ops::Range, rc::Rc}, + thiserror::Error, + uapi::{OwnedFd, Pod, c}, +}; + +#[derive(Default)] +pub struct PwMemPool { + pub mems: CopyHashMap>, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum PwMemType { + MemFd, + DmaBuf, +} + +pub struct PwMem { + pub _ty: PwMemType, + pub read: bool, + pub write: bool, + pub fd: Rc, +} + +pub struct PwMemMap { + pub _mem: Rc, + pub range: Range, + pub map: Mmapped, +} + +pub struct PwMemTyped { + mem: Rc, + offset: usize, + _phantom: PhantomData, +} + +#[expect(dead_code)] +pub struct PwMemSlice { + mem: Rc, + range: Range, +} + +impl PwMemPool { + pub fn map(&self, memid: u32, offset: u32, size: u32) -> Result, PwMemError> { + match self.mems.get(&memid) { + Some(m) => m.map(offset, size), + _ => Err(PwMemError::MemidDoesNotExist(memid)), + } + } +} + +impl PwMem { + pub fn map(self: &Rc, offset: u32, size: u32) -> Result, PwMemError> { + let mask = page_size() - 1; + let offset = offset as usize; + let size = size as usize; + let start = offset & !mask; + let dist = offset - start; + let len = (size + dist + mask) & !mask; + let range = dist..dist + size; + let mut prot = 0; + if self.read { + prot |= c::PROT_READ; + } + if self.write { + prot |= c::PROT_WRITE; + } + let map = match mmap(len as _, prot, c::MAP_SHARED, self.fd.raw(), start as _) { + Ok(m) => m, + Err(e) => return Err(PwMemError::MmapFailed(e)), + }; + Ok(Rc::new(PwMemMap { + _mem: self.clone(), + range, + map, + })) + } +} + +impl PwMemMap { + #[expect(dead_code)] + pub unsafe fn read(&self) -> &T { + self.check::(0); + unsafe { (self.map.ptr.cast::().add(self.range.start) as *const T).deref() } + } + + #[expect(dead_code)] + pub unsafe fn write(&self) -> &mut T { + self.check::(0); + unsafe { (self.map.ptr.cast::().add(self.range.start) as *mut T).deref_mut() } + } + + #[expect(dead_code)] + pub unsafe fn bytes_mut(&self) -> &mut [u8] { + unsafe { + std::slice::from_raw_parts_mut( + self.map.ptr.cast::().add(self.range.start) as _, + self.range.len(), + ) + } + } + + fn check(&self, offset: usize) { + assert!(offset <= self.range.len()); + assert!(size_of::() <= self.range.len() - offset); + assert_eq!((align_of::() - 1) & (self.range.start + offset), 0); + } + + pub fn typed(self: &Rc) -> Rc> { + self.typed_at(0) + } + + pub fn typed_at(self: &Rc, offset: usize) -> Rc> { + self.check::(offset); + Rc::new(PwMemTyped { + mem: self.clone(), + offset: self.range.start + offset, + _phantom: Default::default(), + }) + } + + pub fn slice(self: &Rc, range: Range) -> Rc { + assert!(range.start <= self.range.len()); + assert!(range.len() <= self.range.len() - range.start); + Rc::new(PwMemSlice { + mem: self.clone(), + range: self.range.start + range.start..self.range.start + range.end, + }) + } +} + +impl PwMemTyped { + pub unsafe fn read(&self) -> &T { + unsafe { (self.mem.map.ptr.cast::().add(self.offset) as *const T).deref() } + } + + pub unsafe fn write(&self) -> &mut T { + unsafe { (self.mem.map.ptr.cast::().add(self.offset) as *mut T).deref_mut() } + } +} + +#[derive(Debug, Error)] +pub enum PwMemError { + #[error("mmap failed")] + MmapFailed(#[source] OsError), + #[error("memid {0} does not exist")] + MemidDoesNotExist(u32), +} diff --git a/src/pipewire/pw_object.rs b/src/pipewire/pw_object.rs new file mode 100644 index 00000000..9b487aeb --- /dev/null +++ b/src/pipewire/pw_object.rs @@ -0,0 +1,52 @@ +use { + crate::{pipewire::pw_parser::PwParser, utils::numcell::NumCell}, + std::{cell::Cell, fmt::Debug, rc::Rc}, + thiserror::Error, +}; + +pub trait PwObjectBase { + fn data(&self) -> &PwObjectData; + fn interface(&self) -> &str; + fn handle_msg(self: Rc, opcode: u8, parser: PwParser<'_>) -> Result<(), PwObjectError>; + fn event_name(&self, opcode: u8) -> Option<&'static str>; +} + +pub trait PwObject: PwObjectBase { + fn bound_id(&self, id: u32) { + let _ = id; + } + + fn done(&self) {} + + fn break_loops(&self) {} +} + +pub struct PwObjectData { + pub id: u32, + pub bound_id: Cell>, + pub sync_id: NumCell, +} + +#[derive(Debug, Error)] +#[error("An error occurred in a `{interface}`")] +pub struct PwObjectError { + pub interface: &'static str, + #[source] + pub source: PwObjectErrorType, +} + +#[derive(Debug, Error)] +pub enum PwObjectErrorType { + #[error("Unknown event {0}")] + UnknownEvent(u8), + #[error("An error occurred in event `{method}`")] + EventError { + method: &'static str, + #[source] + source: Box, + }, +} + +pub trait PwOpcode: Debug { + fn id(&self) -> u8; +} diff --git a/src/pipewire/pw_parser.rs b/src/pipewire/pw_parser.rs new file mode 100644 index 00000000..08140be5 --- /dev/null +++ b/src/pipewire/pw_parser.rs @@ -0,0 +1,312 @@ +#![allow(non_upper_case_globals)] + +use { + crate::pipewire::pw_pod::{ + PW_CHOICE_None, PW_TYPE_Array, PW_TYPE_Bitmap, PW_TYPE_Bool, PW_TYPE_Bytes, PW_TYPE_Choice, + PW_TYPE_Double, PW_TYPE_Fd, PW_TYPE_Float, PW_TYPE_Fraction, PW_TYPE_Id, PW_TYPE_Int, + PW_TYPE_Long, PW_TYPE_None, PW_TYPE_Object, PW_TYPE_Pod, PW_TYPE_Pointer, + PW_TYPE_Rectangle, PW_TYPE_Sequence, PW_TYPE_String, PW_TYPE_Struct, PwChoiceType, + PwControlType, PwPod, PwPodArray, PwPodChoice, PwPodControl, PwPodFraction, PwPodObject, + PwPodObjectType, PwPodPointer, PwPodRectangle, PwPodSequence, PwPodStruct, PwPodType, + PwPointerType, PwProp, PwPropFlag, + }, + ahash::AHashMap, + bstr::{BStr, BString, ByteSlice}, + std::{fmt::Debug, mem::MaybeUninit, rc::Rc}, + thiserror::Error, + uapi::{OwnedFd, Pod}, +}; + +#[derive(Debug, Error)] +pub enum PwParserError { + #[error("Unexpected EOF")] + UnexpectedEof, + #[error("Message references an FD that is out of bounds")] + MissingFd, + #[error("Array element type has size of 0")] + ZeroSizedArrayElementType, + #[error("Unknown POD type: {0:?}")] + UnknownType(PwPodType), + #[error("Unexpected POD type: Expected {0:?}, got {1:?}")] + UnexpectedPodType(PwPodType, PwPodType), +} + +#[derive(Copy, Clone)] +pub struct PwParser<'a> { + data: &'a [u8], + fds: &'a [Rc], + pos: usize, +} + +impl<'a> PwParser<'a> { + pub fn new(data: &'a [u8], fds: &'a [Rc]) -> Self { + Self { data, fds, pos: 0 } + } + + pub fn reset(&mut self) { + self.pos = 0; + } + + fn read_raw(&mut self, offset: usize) -> Result { + if self.pos + offset + size_of::() <= self.data.len() { + unsafe { + let mut res = MaybeUninit::uninit(); + let src = self.data[self.pos + offset..].as_ptr(); + std::ptr::copy_nonoverlapping(src, res.as_mut_ptr() as _, size_of::()); + Ok(res.assume_init()) + } + } else { + Err(PwParserError::UnexpectedEof) + } + } + + pub fn len(&self) -> usize { + self.data.len() - self.pos + } + + pub fn pos(&self) -> usize { + self.pos + } + + fn read_array(&mut self, offset: usize, len: usize) -> Result, PwParserError> { + let child_len = self.read_raw::(offset)? as usize; + if child_len == 0 { + return Err(PwParserError::ZeroSizedArrayElementType); + } + let ty = PwPodType(self.read_raw(offset + 4)?); + Ok(PwPodArray { + ty, + child_len, + n_elements: (len - 8) / child_len, + elements: PwParser::new( + &self.data[self.pos + offset + 8..self.pos + offset + len], + self.fds, + ), + }) + } + + pub fn read_dict_struct(&mut self) -> Result, PwParserError> { + let s2 = self.read_struct()?; + let mut p3 = s2.fields; + let num_dict_entries = p3.read_int()?; + let mut de = AHashMap::new(); + for _ in 0..num_dict_entries { + de.insert(p3.read_string()?.to_owned(), p3.read_string()?.to_owned()); + } + Ok(de) + } + + pub fn read_struct(&mut self) -> Result, PwParserError> { + match self.read_pod()? { + PwPod::Struct(s) => Ok(s), + v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Struct, v.ty())), + } + } + + pub fn read_uint(&mut self) -> Result { + self.read_int().map(|v| v as u32) + } + + pub fn read_int(&mut self) -> Result { + match self.read_value()? { + PwPod::Int(s) => Ok(s), + v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Int, v.ty())), + } + } + + pub fn read_object_opt(&mut self) -> Result>, PwParserError> { + match self.read_pod()? { + PwPod::Object(p) => Ok(Some(p)), + PwPod::None => Ok(None), + v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Object, v.ty())), + } + } + + pub fn read_object(&mut self) -> Result, PwParserError> { + match self.read_object_opt()? { + Some(p) => Ok(p), + _ => Err(PwParserError::UnexpectedPodType( + PW_TYPE_Object, + PW_TYPE_None, + )), + } + } + + pub fn read_id(&mut self) -> Result { + match self.read_value()? { + PwPod::Id(s) => Ok(s), + v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Id, v.ty())), + } + } + + pub fn read_fd_opt(&mut self) -> Result>, PwParserError> { + match self.read_pod()? { + PwPod::Fd(idx) if idx == !0 => Ok(None), + PwPod::Fd(idx) => match self.fds.get(idx as usize) { + Some(fd) => Ok(Some(fd.clone())), + _ => Err(PwParserError::MissingFd), + }, + v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Id, v.ty())), + } + } + + pub fn read_fd(&mut self) -> Result, PwParserError> { + match self.read_fd_opt()? { + Some(fd) => Ok(fd), + _ => Err(PwParserError::MissingFd), + } + } + + pub fn read_ulong(&mut self) -> Result { + self.read_long().map(|l| l as _) + } + + pub fn read_long(&mut self) -> Result { + match self.read_value()? { + PwPod::Long(s) => Ok(s), + v => Err(PwParserError::UnexpectedPodType(PW_TYPE_Long, v.ty())), + } + } + + pub fn read_string(&mut self) -> Result<&'a BStr, PwParserError> { + match self.read_value()? { + PwPod::String(s) => Ok(s), + v => Err(PwParserError::UnexpectedPodType(PW_TYPE_String, v.ty())), + } + } + + pub fn read_value(&mut self) -> Result, PwParserError> { + let mut v = self.read_pod(); + if let Ok(PwPod::Choice(v)) = &mut v + && v.ty == PW_CHOICE_None + && v.elements.n_elements > 0 + { + return v + .elements + .elements + .read_pod_body_packed(v.elements.ty, v.elements.child_len); + } + v + } + + pub fn read_pod(&mut self) -> Result, PwParserError> { + let len = self.read_raw::(0)? as usize; + let ty = PwPodType(self.read_raw(4)?); + self.pos += 8; + self.read_pod_body(ty, len) + } + + pub fn read_pod_body_packed( + &mut self, + ty: PwPodType, + len: usize, + ) -> Result, PwParserError> { + self.read_pod_body2(ty, len, true) + } + + pub fn read_pod_body(&mut self, ty: PwPodType, len: usize) -> Result, PwParserError> { + self.read_pod_body2(ty, len, false) + } + + fn read_pod_body2( + &mut self, + ty: PwPodType, + len: usize, + packed: bool, + ) -> Result, PwParserError> { + let size = if packed { len } else { (len + 7) & !7 }; + if self.len() < size { + return Err(PwParserError::UnexpectedEof); + } + let val = match ty { + PW_TYPE_None => PwPod::None, + PW_TYPE_Bool => PwPod::Bool(self.read_raw::(0)? != 0), + PW_TYPE_Id => PwPod::Id(self.read_raw(0)?), + PW_TYPE_Int => PwPod::Int(self.read_raw(0)?), + PW_TYPE_Long => PwPod::Long(self.read_raw(0)?), + PW_TYPE_Float => PwPod::Float(self.read_raw(0)?), + PW_TYPE_Double => PwPod::Double(self.read_raw(0)?), + PW_TYPE_String => { + let s = if len == 0 { + &[][..] + } else { + &self.data[self.pos..self.pos + len - 1] + }; + PwPod::String(s.as_bstr()) + } + PW_TYPE_Bytes => PwPod::Bytes(&self.data[self.pos..self.pos + len]), + PW_TYPE_Rectangle => PwPod::Rectangle(PwPodRectangle { + width: self.read_raw(0)?, + height: self.read_raw(4)?, + }), + PW_TYPE_Fraction => PwPod::Fraction(PwPodFraction { + num: self.read_raw(0)?, + denom: self.read_raw(4)?, + }), + PW_TYPE_Bitmap => PwPod::Bitmap(&self.data[self.pos..self.pos + len]), + PW_TYPE_Array => PwPod::Array(self.read_array(0, len)?), + PW_TYPE_Struct => PwPod::Struct(PwPodStruct { + fields: PwParser::new(&self.data[self.pos..self.pos + len], self.fds), + }), + PW_TYPE_Object => PwPod::Object(PwPodObject { + ty: PwPodObjectType(self.read_raw(0)?), + id: self.read_raw(4)?, + probs: PwParser::new(&self.data[self.pos + 8..self.pos + len], self.fds), + }), + PW_TYPE_Sequence => PwPod::Sequence(PwPodSequence { + unit: self.read_raw(0)?, + controls: PwParser::new(&self.data[self.pos + 8..self.pos + len], self.fds), + }), + PW_TYPE_Pointer => PwPod::Pointer(PwPodPointer { + _ty: PwPointerType(self.read_raw(0)?), + _value: self.read_raw(8)?, + }), + PW_TYPE_Fd => PwPod::Fd(self.read_raw(0)?), + PW_TYPE_Choice => PwPod::Choice(PwPodChoice { + ty: PwChoiceType(self.read_raw(0)?), + flags: self.read_raw(4)?, + elements: self.read_array(8, len - 8)?, + }), + PW_TYPE_Pod => { + let pos = self.pos; + let pod = self.read_pod()?; + self.pos = pos; + pod + } + _ => return Err(PwParserError::UnknownType(ty)), + }; + self.pos += size; + Ok(val) + } + + pub fn read_prop(&mut self) -> Result, PwParserError> { + let key = self.read_raw(0)?; + let flags = PwPropFlag(self.read_raw(4)?); + self.pos += 8; + Ok(PwProp { + key, + flags, + pod: self.read_pod()?, + }) + } + + pub fn read_control(&mut self) -> Result, PwParserError> { + let offset = self.read_raw(0)?; + let ty = PwControlType(self.read_raw(4)?); + self.pos += 8; + Ok(PwPodControl { + _offset: offset, + _ty: ty, + _value: self.read_pod()?, + }) + } + + pub fn skip(&mut self) -> Result<(), PwParserError> { + let size = self.read_raw::(0)? as usize; + if self.len() < size + 8 { + return Err(PwParserError::UnexpectedEof); + } + self.pos += size + 8; + Ok(()) + } +} diff --git a/src/pipewire/pw_pod.rs b/src/pipewire/pw_pod.rs new file mode 100644 index 00000000..ffb37194 --- /dev/null +++ b/src/pipewire/pw_pod.rs @@ -0,0 +1,1451 @@ +#![allow(non_upper_case_globals, non_camel_case_types)] + +mod pw_debug; + +use { + crate::pipewire::pw_parser::{PwParser, PwParserError}, + bstr::BStr, + std::{ + fmt::{Debug, Formatter}, + sync::atomic::{AtomicI32, AtomicU32}, + }, + uapi::{Pod, c}, +}; + +macro_rules! ty { + ($name:ident; $($id:ident = $val:expr,)*) => { + #[derive(Copy, Clone, Eq, PartialEq, Hash)] + #[repr(transparent)] + pub struct $name(pub u32); + + $( + pub const $id: $name = $name($val); + )* + + impl $name { + pub fn name(self) -> Option<&'static str> { + let res = match self { + $( + $id => stringify!($id), + )* + _ => return None, + }; + Some(res) + } + } + + impl Debug for $name { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.name() { + Some(n) => write!(f, "{}", n), + _ => write!(f, "{}({})", stringify!($name), self.0), + } + } + } + } +} + +ty! { + PwPodType; + + PW_TYPE_None = 0x01, + PW_TYPE_Bool = 0x02, + PW_TYPE_Id = 0x03, + PW_TYPE_Int = 0x04, + PW_TYPE_Long = 0x05, + PW_TYPE_Float = 0x06, + PW_TYPE_Double = 0x07, + PW_TYPE_String = 0x08, + PW_TYPE_Bytes = 0x09, + PW_TYPE_Rectangle = 0x0A, + PW_TYPE_Fraction = 0x0B, + PW_TYPE_Bitmap = 0x0C, + PW_TYPE_Array = 0x0D, + PW_TYPE_Struct = 0x0E, + PW_TYPE_Object = 0x0F, + PW_TYPE_Sequence = 0x10, + PW_TYPE_Pointer = 0x11, + PW_TYPE_Fd = 0x12, + PW_TYPE_Choice = 0x13, + PW_TYPE_Pod = 0x14, +} + +ty! { + PwPodObjectType; + + PW_COMMAND_Device = 0x30001, + PW_COMMAND_Node = 0x30002, + + PW_OBJECT_PropInfo = 0x40001, + PW_OBJECT_Props = 0x40002, + PW_OBJECT_Format = 0x40003, + PW_OBJECT_ParamBuffers = 0x40004, + PW_OBJECT_ParamMeta = 0x40005, + PW_OBJECT_ParamIO = 0x40006, + PW_OBJECT_ParamProfile = 0x40007, + PW_OBJECT_ParamPortConfig = 0x40008, + PW_OBJECT_ParamRoute = 0x40009, + PW_OBJECT_Profiler = 0x4000A, + PW_OBJECT_ParamLatency = 0x4000B, + PW_OBJECT_ParamProcessLatency = 0x4000C, +} + +ty! { + SpaParamType; + + SPA_PARAM_Invalid = 0, + SPA_PARAM_PropInfo = 1, + SPA_PARAM_Props = 2, + SPA_PARAM_EnumFormat = 3, + SPA_PARAM_Format = 4, + SPA_PARAM_Buffers = 5, + SPA_PARAM_Meta = 6, + SPA_PARAM_IO = 7, + SPA_PARAM_EnumProfile = 8, + SPA_PARAM_Profile = 9, + SPA_PARAM_EnumPortConfig = 10, + SPA_PARAM_PortConfig = 11, + SPA_PARAM_EnumRoute = 12, + SPA_PARAM_Route = 13, + SPA_PARAM_Control = 14, + SPA_PARAM_Latency = 15, + SPA_PARAM_ProcessLatency = 16, +} + +ty! { + SpaFormat; + + SPA_FORMAT_START = 0x00000, + + SPA_FORMAT_mediaType = 0x00001, + SPA_FORMAT_mediaSubtype = 0x00002, + + SPA_FORMAT_START_Audio = 0x10000, + SPA_FORMAT_AUDIO_format = 0x10001, + SPA_FORMAT_AUDIO_flags = 0x10002, + SPA_FORMAT_AUDIO_rate = 0x10003, + SPA_FORMAT_AUDIO_channels = 0x10004, + SPA_FORMAT_AUDIO_position = 0x10005, + SPA_FORMAT_AUDIO_iec958Codec = 0x10006, + SPA_FORMAT_AUDIO_bitorder = 0x10007, + SPA_FORMAT_AUDIO_interleave = 0x10008, + + SPA_FORMAT_START_Video = 0x20000, + SPA_FORMAT_VIDEO_format = 0x20001, + SPA_FORMAT_VIDEO_modifier = 0x20002, + SPA_FORMAT_VIDEO_size = 0x20003, + SPA_FORMAT_VIDEO_framerate = 0x20004, + SPA_FORMAT_VIDEO_maxFramerate = 0x20005, + SPA_FORMAT_VIDEO_views = 0x20006, + SPA_FORMAT_VIDEO_interlaceMode = 0x20007, + SPA_FORMAT_VIDEO_pixelAspectRatio = 0x20008, + SPA_FORMAT_VIDEO_multiviewMode = 0x20009, + SPA_FORMAT_VIDEO_multiviewFlags = 0x2000A, + SPA_FORMAT_VIDEO_chromaSite = 0x2000B, + SPA_FORMAT_VIDEO_colorRange = 0x2000C, + SPA_FORMAT_VIDEO_colorMatrix = 0x2000D, + SPA_FORMAT_VIDEO_transferFunction = 0x2000E, + SPA_FORMAT_VIDEO_colorPrimaries = 0x2000F, + SPA_FORMAT_VIDEO_profile = 0x20010, + SPA_FORMAT_VIDEO_level = 0x20011, + SPA_FORMAT_VIDEO_H264_streamFormat = 0x20012, + SPA_FORMAT_VIDEO_H264_alignment = 0x20013, + + SPA_FORMAT_START_Image = 0x30000, + SPA_FORMAT_START_Binary = 0x40000, + SPA_FORMAT_START_Stream = 0x50000, + SPA_FORMAT_START_Application = 0x60000, +} + +bitflags! { + SPA_PARAM_INFO: u32; + + SPA_PARAM_INFO_SERIAL = 1<<0, + SPA_PARAM_INFO_READ = 1<<1, + SPA_PARAM_INFO_WRITE = 1<<2, +} + +ty! { + PwControlType; + + PW_CONTROL_PropInfo = 1, + PW_CONTROL_Props = 2, + PW_CONTROL_Format = 3, +} + +ty! { + PwPointerType; + + PW_POINTER_Buffer = 0x10001, + PW_POINTER_Meta = 0x10002, + PW_POINTER_Dict = 0x10003, +} + +ty! { + PwChoiceType; + + PW_CHOICE_None = 0, + PW_CHOICE_Range = 1, + PW_CHOICE_Step = 2, + PW_CHOICE_Enum = 3, + PW_CHOICE_Flags = 4, +} + +ty! { + PwIoType; + + PW_IO_Buffers = 1, + PW_IO_Range = 2, + PW_IO_Clock = 3, + PW_IO_Latency = 4, + PW_IO_Control = 5, + PW_IO_Notify = 6, + PW_IO_Position = 7, + PW_IO_RateMatch = 8, + PW_IO_Memory = 9, +} + +bitflags! { + PwPropFlag: u32; + + PW_PROP_READONLY = 1 << 0, + PW_PROP_HARDWARE = 1 << 1, + PW_PROP_HINT_DICT = 1 << 2, + PW_PROP_MANDATORY = 1 << 3, + PW_PROP_DONT_FIXATE = 1 << 4, +} + +ty! { + SpaMediaType; + + SPA_MEDIA_TYPE_unknown = 0, + SPA_MEDIA_TYPE_audio = 1, + SPA_MEDIA_TYPE_video = 2, + SPA_MEDIA_TYPE_image = 3, + SPA_MEDIA_TYPE_binary = 4, + SPA_MEDIA_TYPE_stream = 5, + SPA_MEDIA_TYPE_application = 6, +} + +ty! { + SpaMediaSubtype; + + SPA_MEDIA_SUBTYPE_unknown = 0x00000, + SPA_MEDIA_SUBTYPE_raw = 0x00001, + SPA_MEDIA_SUBTYPE_dsp = 0x00002, + SPA_MEDIA_SUBTYPE_iec958 = 0x00003, + SPA_MEDIA_SUBTYPE_dsd = 0x00004, + + SPA_MEDIA_SUBTYPE_START_Audio = 0x10000, + SPA_MEDIA_SUBTYPE_mp3 = 0x10001, + SPA_MEDIA_SUBTYPE_aac = 0x10002, + SPA_MEDIA_SUBTYPE_vorbis = 0x10003, + SPA_MEDIA_SUBTYPE_wma = 0x10004, + SPA_MEDIA_SUBTYPE_ra = 0x10005, + SPA_MEDIA_SUBTYPE_sbc = 0x10006, + SPA_MEDIA_SUBTYPE_adpcm = 0x10007, + SPA_MEDIA_SUBTYPE_g723 = 0x10008, + SPA_MEDIA_SUBTYPE_g726 = 0x10009, + SPA_MEDIA_SUBTYPE_g729 = 0x1000A, + SPA_MEDIA_SUBTYPE_amr = 0x1000B, + SPA_MEDIA_SUBTYPE_gsm = 0x1000C, + + SPA_MEDIA_SUBTYPE_START_Video = 0x20000, + SPA_MEDIA_SUBTYPE_h264 = 0x20001, + SPA_MEDIA_SUBTYPE_mjpg = 0x20002, + SPA_MEDIA_SUBTYPE_dv = 0x20003, + SPA_MEDIA_SUBTYPE_mpegts = 0x20004, + SPA_MEDIA_SUBTYPE_h263 = 0x20005, + SPA_MEDIA_SUBTYPE_mpeg1 = 0x20006, + SPA_MEDIA_SUBTYPE_mpeg2 = 0x20007, + SPA_MEDIA_SUBTYPE_mpeg4 = 0x20008, + SPA_MEDIA_SUBTYPE_xvid = 0x20009, + SPA_MEDIA_SUBTYPE_vc1 = 0x2000A, + SPA_MEDIA_SUBTYPE_vp8 = 0x2000B, + SPA_MEDIA_SUBTYPE_vp9 = 0x2000C, + SPA_MEDIA_SUBTYPE_bayer = 0x2000D, + + SPA_MEDIA_SUBTYPE_START_Image = 0x30000, + SPA_MEDIA_SUBTYPE_jpeg = 0x30001, + + SPA_MEDIA_SUBTYPE_START_Binary = 0x40000, + + SPA_MEDIA_SUBTYPE_START_Stream = 0x50000, + SPA_MEDIA_SUBTYPE_midi = 0x50001, + + SPA_MEDIA_SUBTYPE_START_Application = 0x60000, + SPA_MEDIA_SUBTYPE_control = 0x60001, +} + +ty! { + SpaAudioFormat; + + SPA_AUDIO_FORMAT_UNKNOWN = 0x000, + SPA_AUDIO_FORMAT_ENCODED = 0x001, + + SPA_AUDIO_FORMAT_START_Interleaved = 0x100, + SPA_AUDIO_FORMAT_S8 = 0x101, + SPA_AUDIO_FORMAT_U8 = 0x102, + SPA_AUDIO_FORMAT_S16_LE = 0x103, + SPA_AUDIO_FORMAT_S16_BE = 0x104, + SPA_AUDIO_FORMAT_U16_LE = 0x105, + SPA_AUDIO_FORMAT_U16_BE = 0x106, + SPA_AUDIO_FORMAT_S24_32_LE = 0x107, + SPA_AUDIO_FORMAT_S24_32_BE = 0x108, + SPA_AUDIO_FORMAT_U24_32_LE = 0x109, + SPA_AUDIO_FORMAT_U24_32_BE = 0x10A, + SPA_AUDIO_FORMAT_S32_LE = 0x10B, + SPA_AUDIO_FORMAT_S32_BE = 0x10C, + SPA_AUDIO_FORMAT_U32_LE = 0x10D, + SPA_AUDIO_FORMAT_U32_BE = 0x10E, + SPA_AUDIO_FORMAT_S24_LE = 0x10F, + SPA_AUDIO_FORMAT_S24_BE = 0x110, + SPA_AUDIO_FORMAT_U24_LE = 0x111, + SPA_AUDIO_FORMAT_U24_BE = 0x112, + SPA_AUDIO_FORMAT_S20_LE = 0x113, + SPA_AUDIO_FORMAT_S20_BE = 0x114, + SPA_AUDIO_FORMAT_U20_LE = 0x115, + SPA_AUDIO_FORMAT_U20_BE = 0x116, + SPA_AUDIO_FORMAT_S18_LE = 0x117, + SPA_AUDIO_FORMAT_S18_BE = 0x118, + SPA_AUDIO_FORMAT_U18_LE = 0x119, + SPA_AUDIO_FORMAT_U18_BE = 0x11A, + SPA_AUDIO_FORMAT_F32_LE = 0x11B, + SPA_AUDIO_FORMAT_F32_BE = 0x11C, + SPA_AUDIO_FORMAT_F64_LE = 0x11D, + SPA_AUDIO_FORMAT_F64_BE = 0x11E, + + SPA_AUDIO_FORMAT_ULAW = 0x11F, + SPA_AUDIO_FORMAT_ALAW = 0x120, + + SPA_AUDIO_FORMAT_START_Planar = 0x200, + SPA_AUDIO_FORMAT_U8P = 0x201, + SPA_AUDIO_FORMAT_S16P = 0x202, + SPA_AUDIO_FORMAT_S24_32P = 0x203, + SPA_AUDIO_FORMAT_S32P = 0x204, + SPA_AUDIO_FORMAT_S24P = 0x205, + SPA_AUDIO_FORMAT_F32P = 0x206, + SPA_AUDIO_FORMAT_F64P = 0x207, + SPA_AUDIO_FORMAT_S8P = 0x208, + + SPA_AUDIO_FORMAT_START_Other = 0x400, +} + +ty! { + SpaVideoFormat; + + SPA_VIDEO_FORMAT_UNKNOWN = 000, + SPA_VIDEO_FORMAT_ENCODED = 001, + SPA_VIDEO_FORMAT_I420 = 002, + SPA_VIDEO_FORMAT_YV12 = 003, + SPA_VIDEO_FORMAT_YUY2 = 004, + SPA_VIDEO_FORMAT_UYVY = 005, + SPA_VIDEO_FORMAT_AYUV = 006, + SPA_VIDEO_FORMAT_RGBx = 007, + SPA_VIDEO_FORMAT_BGRx = 008, + SPA_VIDEO_FORMAT_xRGB = 009, + SPA_VIDEO_FORMAT_xBGR = 010, + SPA_VIDEO_FORMAT_RGBA = 011, + SPA_VIDEO_FORMAT_BGRA = 012, + SPA_VIDEO_FORMAT_ARGB = 013, + SPA_VIDEO_FORMAT_ABGR = 014, + SPA_VIDEO_FORMAT_RGB = 015, + SPA_VIDEO_FORMAT_BGR = 016, + SPA_VIDEO_FORMAT_Y41B = 017, + SPA_VIDEO_FORMAT_Y42B = 018, + SPA_VIDEO_FORMAT_YVYU = 019, + SPA_VIDEO_FORMAT_Y444 = 020, + SPA_VIDEO_FORMAT_v210 = 021, + SPA_VIDEO_FORMAT_v216 = 022, + SPA_VIDEO_FORMAT_NV12 = 023, + SPA_VIDEO_FORMAT_NV21 = 024, + SPA_VIDEO_FORMAT_GRAY8 = 025, + SPA_VIDEO_FORMAT_GRAY16_BE = 026, + SPA_VIDEO_FORMAT_GRAY16_LE = 027, + SPA_VIDEO_FORMAT_v308 = 028, + SPA_VIDEO_FORMAT_RGB16 = 029, + SPA_VIDEO_FORMAT_BGR16 = 030, + SPA_VIDEO_FORMAT_RGB15 = 031, + SPA_VIDEO_FORMAT_BGR15 = 032, + SPA_VIDEO_FORMAT_UYVP = 033, + SPA_VIDEO_FORMAT_A420 = 034, + SPA_VIDEO_FORMAT_RGB8P = 035, + SPA_VIDEO_FORMAT_YUV9 = 036, + SPA_VIDEO_FORMAT_YVU9 = 037, + SPA_VIDEO_FORMAT_IYU1 = 038, + SPA_VIDEO_FORMAT_ARGB64 = 039, + SPA_VIDEO_FORMAT_AYUV64 = 040, + SPA_VIDEO_FORMAT_r210 = 041, + SPA_VIDEO_FORMAT_I420_10BE = 042, + SPA_VIDEO_FORMAT_I420_10LE = 043, + SPA_VIDEO_FORMAT_I422_10BE = 044, + SPA_VIDEO_FORMAT_I422_10LE = 045, + SPA_VIDEO_FORMAT_Y444_10BE = 046, + SPA_VIDEO_FORMAT_Y444_10LE = 047, + SPA_VIDEO_FORMAT_GBR = 048, + SPA_VIDEO_FORMAT_GBR_10BE = 049, + SPA_VIDEO_FORMAT_GBR_10LE = 050, + SPA_VIDEO_FORMAT_NV16 = 051, + SPA_VIDEO_FORMAT_NV24 = 052, + SPA_VIDEO_FORMAT_NV12_64Z32 = 053, + SPA_VIDEO_FORMAT_A420_10BE = 054, + SPA_VIDEO_FORMAT_A420_10LE = 055, + SPA_VIDEO_FORMAT_A422_10BE = 056, + SPA_VIDEO_FORMAT_A422_10LE = 057, + SPA_VIDEO_FORMAT_A444_10BE = 058, + SPA_VIDEO_FORMAT_A444_10LE = 059, + SPA_VIDEO_FORMAT_NV61 = 060, + SPA_VIDEO_FORMAT_P010_10BE = 061, + SPA_VIDEO_FORMAT_P010_10LE = 062, + SPA_VIDEO_FORMAT_IYU2 = 063, + SPA_VIDEO_FORMAT_VYUY = 064, + SPA_VIDEO_FORMAT_GBRA = 065, + SPA_VIDEO_FORMAT_GBRA_10BE = 066, + SPA_VIDEO_FORMAT_GBRA_10LE = 067, + SPA_VIDEO_FORMAT_GBR_12BE = 068, + SPA_VIDEO_FORMAT_GBR_12LE = 069, + SPA_VIDEO_FORMAT_GBRA_12BE = 070, + SPA_VIDEO_FORMAT_GBRA_12LE = 071, + SPA_VIDEO_FORMAT_I420_12BE = 072, + SPA_VIDEO_FORMAT_I420_12LE = 073, + SPA_VIDEO_FORMAT_I422_12BE = 074, + SPA_VIDEO_FORMAT_I422_12LE = 075, + SPA_VIDEO_FORMAT_Y444_12BE = 076, + SPA_VIDEO_FORMAT_Y444_12LE = 077, + SPA_VIDEO_FORMAT_RGBA_F16 = 078, + SPA_VIDEO_FORMAT_RGBA_F32 = 079, + SPA_VIDEO_FORMAT_xRGB_210LE = 080, + SPA_VIDEO_FORMAT_xBGR_210LE = 081, + SPA_VIDEO_FORMAT_RGBx_102LE = 082, + SPA_VIDEO_FORMAT_BGRx_102LE = 083, + SPA_VIDEO_FORMAT_ARGB_210LE = 084, + SPA_VIDEO_FORMAT_ABGR_210LE = 085, + SPA_VIDEO_FORMAT_RGBA_102LE = 086, + SPA_VIDEO_FORMAT_BGRA_102LE = 087, +} + +ty! { + SpaVideoInterlaceMode; + + SPA_VIDEO_INTERLACE_MODE_PROGRESSIVE = 0, + SPA_VIDEO_INTERLACE_MODE_INTERLEAVED = 1, + SPA_VIDEO_INTERLACE_MODE_MIXED = 2, + SPA_VIDEO_INTERLACE_MODE_FIELDS = 3, +} + +ty! { + SpaVideoMultiviewMode; + + SPA_VIDEO_MULTIVIEW_MODE_NONE = !0, + SPA_VIDEO_MULTIVIEW_MODE_MONO = 0, + + SPA_VIDEO_MULTIVIEW_MODE_LEFT = 1, + SPA_VIDEO_MULTIVIEW_MODE_RIGHT = 2, + + SPA_VIDEO_MULTIVIEW_MODE_SIDE_BY_SIDE = 3, + SPA_VIDEO_MULTIVIEW_MODE_SIDE_BY_SIDE_QUINCUNX = 4, + SPA_VIDEO_MULTIVIEW_MODE_COLUMN_INTERLEAVED = 5, + SPA_VIDEO_MULTIVIEW_MODE_ROW_INTERLEAVED = 6, + SPA_VIDEO_MULTIVIEW_MODE_TOP_BOTTOM = 7, + SPA_VIDEO_MULTIVIEW_MODE_CHECKERBOARD = 8, + + SPA_VIDEO_MULTIVIEW_MODE_FRAME_BY_FRAME = 32, + SPA_VIDEO_MULTIVIEW_MODE_MULTIVIEW_FRAME_BY_FRAME = 33, + SPA_VIDEO_MULTIVIEW_MODE_SEPARATED = 34, +} + +bitflags! { + SpaVideoMultiviewFlags: u32; + + SPA_VIDEO_MULTIVIEW_FLAGS_NONE = 0, + SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_VIEW_FIRST = 1 << 0, + SPA_VIDEO_MULTIVIEW_FLAGS_LEFT_FLIPPED = 1 << 1, + SPA_VIDEO_MULTIVIEW_FLAGS_LEFT_FLOPPED = 1 << 2, + SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_FLIPPED = 1 << 3, + SPA_VIDEO_MULTIVIEW_FLAGS_RIGHT_FLOPPED = 1 << 4, + SPA_VIDEO_MULTIVIEW_FLAGS_HALF_ASPECT = 1 << 14, + SPA_VIDEO_MULTIVIEW_FLAGS_MIXED_MONO = 1 << 15, +} + +bitflags! { + SpaVideoChromaSite: u32; + + SPA_VIDEO_CHROMA_SITE_UNKNOWN = 0, + SPA_VIDEO_CHROMA_SITE_NONE = 1 << 0, + SPA_VIDEO_CHROMA_SITE_H_COSITED = 1 << 1, + SPA_VIDEO_CHROMA_SITE_V_COSITED = 1 << 2, + SPA_VIDEO_CHROMA_SITE_ALT_LINE = 1 << 3, +} + +ty! { + SpaVideoColorRange; + + SPA_VIDEO_COLOR_RANGE_UNKNOWN = 0, + SPA_VIDEO_COLOR_RANGE_0_255 = 1, + SPA_VIDEO_COLOR_RANGE_16_235 = 2, +} + +ty! { + SpaVideoColorMatrix; + + SPA_VIDEO_COLOR_MATRIX_UNKNOWN = 0, + SPA_VIDEO_COLOR_MATRIX_RGB = 1, + SPA_VIDEO_COLOR_MATRIX_FCC = 2, + SPA_VIDEO_COLOR_MATRIX_BT709 = 3, + SPA_VIDEO_COLOR_MATRIX_BT601 = 4, + SPA_VIDEO_COLOR_MATRIX_SMPTE240M = 5, + SPA_VIDEO_COLOR_MATRIX_BT2020 = 6, +} + +ty! { + SpaVideoTransferFunction; + + SPA_VIDEO_TRANSFER_UNKNOWN = 0, + SPA_VIDEO_TRANSFER_GAMMA10 = 1, + SPA_VIDEO_TRANSFER_GAMMA18 = 2, + SPA_VIDEO_TRANSFER_GAMMA20 = 3, + SPA_VIDEO_TRANSFER_GAMMA22 = 4, + SPA_VIDEO_TRANSFER_BT709 = 5, + SPA_VIDEO_TRANSFER_SMPTE240M = 6, + SPA_VIDEO_TRANSFER_SRGB = 7, + SPA_VIDEO_TRANSFER_GAMMA28 = 8, + SPA_VIDEO_TRANSFER_LOG100 = 9, + SPA_VIDEO_TRANSFER_LOG316 = 10, + SPA_VIDEO_TRANSFER_BT2020_12 = 11, + SPA_VIDEO_TRANSFER_ADOBERGB = 12, +} + +ty! { + SpaVideoColorPrimaries; + + SPA_VIDEO_COLOR_PRIMARIES_UNKNOWN = 0, + SPA_VIDEO_COLOR_PRIMARIES_BT709 = 1, + SPA_VIDEO_COLOR_PRIMARIES_BT470M = 2, + SPA_VIDEO_COLOR_PRIMARIES_BT470BG = 3, + SPA_VIDEO_COLOR_PRIMARIES_SMPTE170M = 4, + SPA_VIDEO_COLOR_PRIMARIES_SMPTE240M = 5, + SPA_VIDEO_COLOR_PRIMARIES_FILM = 6, + SPA_VIDEO_COLOR_PRIMARIES_BT2020 = 7, + SPA_VIDEO_COLOR_PRIMARIES_ADOBERGB = 8, +} + +ty! { + SpaH264StreamFormat; + + SPA_H264_STREAM_FORMAT_UNKNOWN = 0, + SPA_H264_STREAM_FORMAT_AVC = 1, + SPA_H264_STREAM_FORMAT_AVC3 = 2, + SPA_H264_STREAM_FORMAT_BYTESTREAM = 3, +} + +ty! { + SpaH264Alignment; + + SPA_H264_ALIGNMENT_UNKNOWN = 0, + SPA_H264_ALIGNMENT_AU = 1, + SPA_H264_ALIGNMENT_NAL = 2, +} + +ty! { + SpaParamBuffers; + + SPA_PARAM_BUFFERS_START = 0, + SPA_PARAM_BUFFERS_buffers = 1, + SPA_PARAM_BUFFERS_blocks = 2, + SPA_PARAM_BUFFERS_size = 3, + SPA_PARAM_BUFFERS_stride = 4, + SPA_PARAM_BUFFERS_align = 5, + SPA_PARAM_BUFFERS_dataType = 6, +} + +ty! { + SpaDataType; + + SPA_DATA_Invalid = 0, + SPA_DATA_MemPtr = 1, + SPA_DATA_MemFd = 2, + SPA_DATA_DmaBuf = 3, + SPA_DATA_MemId = 4, +} + +impl Default for SpaDataType { + fn default() -> Self { + SPA_DATA_Invalid + } +} + +bitflags! { + SpaNodeBuffersFlags: u32; + + SPA_NODE_BUFFERS_FLAG_ALLOC = 1 << 0, +} + +bitflags! { + SpaDataFlags: u32; + + SPA_DATA_FLAG_READABLE = 1 << 0, + SPA_DATA_FLAG_WRITABLE = 1 << 1, + SPA_DATA_FLAG_DYNAMIC = 1 << 2, +} + +bitflags! { + SpaDataTypes: u32; + + SPA_DATA_MASK_Invalid = 1, + SPA_DATA_MASK_MemPtr = 2, + SPA_DATA_MASK_MemFd = 4, + SPA_DATA_MASK_DmaBuf = 8, + SPA_DATA_MASK_MemId = 16, +} + +ty! { + SpaParamMeta; + + SPA_PARAM_META_START = 0, + SPA_PARAM_META_type = 1, + SPA_PARAM_META_size = 2, +} + +ty! { + SpaParamIo; + + SPA_PARAM_IO_START = 0, + SPA_PARAM_IO_id = 1, + SPA_PARAM_IO_size = 2, +} + +ty! { + SpaIoType; + + SPA_IO_Invalid = 0, + SPA_IO_Buffers = 1, + SPA_IO_Range = 2, + SPA_IO_Clock = 3, + SPA_IO_Latency = 4, + SPA_IO_Control = 5, + SPA_IO_Notify = 6, + SPA_IO_Position = 7, + SPA_IO_RateMatch = 8, + SPA_IO_Memory = 9, +} + +ty! { + SpaParamProfile; + + SPA_PARAM_PROFILE_START = 0, + SPA_PARAM_PROFILE_index = 1, + SPA_PARAM_PROFILE_name = 2, + SPA_PARAM_PROFILE_description = 3, + SPA_PARAM_PROFILE_priority = 4, + SPA_PARAM_PROFILE_available = 5, + SPA_PARAM_PROFILE_info = 6, + SPA_PARAM_PROFILE_classes = 7, + SPA_PARAM_PROFILE_save = 8, +} + +ty! { + SpaParamAvailability; + + SPA_PARAM_AVAILABILITY_unknown = 0, + SPA_PARAM_AVAILABILITY_no = 1, + SPA_PARAM_AVAILABILITY_yes = 2, +} + +ty! { + SpaParamPortConfig; + + SPA_PARAM_PORT_CONFIG_START = 0, + SPA_PARAM_PORT_CONFIG_direction = 1, + SPA_PARAM_PORT_CONFIG_mode = 2, + SPA_PARAM_PORT_CONFIG_monitor = 3, + SPA_PARAM_PORT_CONFIG_control = 4, + SPA_PARAM_PORT_CONFIG_format = 5, +} + +ty! { + SpaDirection; + + SPA_DIRECTION_INPUT = 0, + SPA_DIRECTION_OUTPUT = 1, +} + +ty! { + SpaParamRoute; + + SPA_PARAM_ROUTE_START = 0, + SPA_PARAM_ROUTE_index = 1, + SPA_PARAM_ROUTE_direction = 2, + SPA_PARAM_ROUTE_device = 3, + SPA_PARAM_ROUTE_name = 4, + SPA_PARAM_ROUTE_description = 5, + SPA_PARAM_ROUTE_priority = 6, + SPA_PARAM_ROUTE_available = 7, + SPA_PARAM_ROUTE_info = 8, + SPA_PARAM_ROUTE_profiles = 9, + SPA_PARAM_ROUTE_props = 10, + SPA_PARAM_ROUTE_devices = 11, + SPA_PARAM_ROUTE_profile = 12, + SPA_PARAM_ROUTE_save = 13, +} + +ty! { + SpaProfiler; + + SPA_PROFILER_START = 0x0000000, + + SPA_PROFILER_START_Driver = 0x0010000, + SPA_PROFILER_info = 0x0010001, + SPA_PROFILER_clock = 0x0010002, + SPA_PROFILER_driverBlock = 0x0010003, + SPA_PROFILER_START_Follower = 0x0020000, + SPA_PROFILER_followerBlock = 0x0020001, + SPA_PROFILER_START_CUSTOM = 0x1000000, +} + +ty! { + SpaParamLatency; + + SPA_PARAM_LATENCY_START = 0, + SPA_PARAM_LATENCY_direction = 1, + SPA_PARAM_LATENCY_minQuantum = 2, + SPA_PARAM_LATENCY_maxQuantum = 3, + SPA_PARAM_LATENCY_minRate = 4, + SPA_PARAM_LATENCY_maxRate = 5, + SPA_PARAM_LATENCY_minNs = 6, + SPA_PARAM_LATENCY_maxNs = 7, +} + +ty! { + SpaParamProcessLatency; + + SPA_PARAM_PROCESS_LATENCY_START = 0, + SPA_PARAM_PROCESS_LATENCY_quantum = 1, + SPA_PARAM_PROCESS_LATENCY_rate = 2, + SPA_PARAM_PROCESS_LATENCY_ns = 3, +} + +ty! { + SpaParamPortConfigMode; + + SPA_PARAM_PORT_CONFIG_MODE_none = 0, + SPA_PARAM_PORT_CONFIG_MODE_passthrough = 1, + SPA_PARAM_PORT_CONFIG_MODE_convert = 2, + SPA_PARAM_PORT_CONFIG_MODE_dsp = 3, +} + +bitflags! { + SpaMetaHeaderFlags: u32; + + SPA_META_HEADER_FLAG_DISCONT = 1 << 0, + SPA_META_HEADER_FLAG_CORRUPTED = 1 << 1, + SPA_META_HEADER_FLAG_MARKER = 1 << 2, + SPA_META_HEADER_FLAG_HEADER = 1 << 3, + SPA_META_HEADER_FLAG_GAP = 1 << 4, + SPA_META_HEADER_FLAG_DELTA_UNIT = 1 << 5, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_meta_header { + pub flags: SpaMetaHeaderFlags, + pub offset: u32, + pub pts: i64, + pub dts_offset: i64, + pub seq: u64, +} + +unsafe impl Pod for spa_meta_header {} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_point { + pub x: i32, + pub y: i32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_region { + pub position: spa_point, + pub size: spa_rectangle, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_meta_region { + pub region: spa_region, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_meta_bitmap { + pub format: SpaVideoFormat, + pub size: spa_rectangle, + pub stride: i32, + pub offset: u32, +} + +unsafe impl Pod for spa_meta_bitmap {} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_meta_cursor { + pub id: u32, + pub flags: u32, + pub position: spa_point, + pub hotspot: spa_point, + pub bitmap_offset: u32, +} + +unsafe impl Pod for spa_meta_cursor {} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_meta_busy { + pub flags: u32, + pub count: u32, +} + +unsafe impl Pod for spa_meta_busy {} + +unsafe impl Pod for spa_meta_region {} + +ty! { + SpaMetaType; + + SPA_META_Invalid = 0, + SPA_META_Header = 1, + SPA_META_VideoCrop = 2, + SPA_META_VideoDamage = 3, + SPA_META_Bitmap = 4, + SPA_META_Cursor = 5, + SPA_META_Control = 6, + SPA_META_Busy = 7, +} + +ty! { + SpaPropInfo; + + SPA_PROP_INFO_START = 0, + SPA_PROP_INFO_id = 1, + SPA_PROP_INFO_name = 2, + SPA_PROP_INFO_type = 3, + SPA_PROP_INFO_labels = 4, + SPA_PROP_INFO_container = 5, + SPA_PROP_INFO_params = 6, + SPA_PROP_INFO_description = 7, +} + +ty! { + SpaProp; + + SPA_PROP_START = 0x0000000, + SPA_PROP_unknown = 0x0000001, + SPA_PROP_START_Device = 0x0000100, + SPA_PROP_device = 0x0000101, + SPA_PROP_deviceName = 0x0000102, + SPA_PROP_deviceFd = 0x0000103, + SPA_PROP_card = 0x0000104, + SPA_PROP_cardName = 0x0000105, + SPA_PROP_minLatency = 0x0000106, + SPA_PROP_maxLatency = 0x0000107, + SPA_PROP_periods = 0x0000108, + SPA_PROP_periodSize = 0x0000109, + SPA_PROP_periodEvent = 0x000010A, + SPA_PROP_live = 0x000010B, + SPA_PROP_rate = 0x000010C, + SPA_PROP_quality = 0x000010D, + SPA_PROP_bluetoothAudioCodec = 0x000010E, + SPA_PROP_START_Audio = 0x0010000, + SPA_PROP_waveType = 0x0010001, + SPA_PROP_frequency = 0x0010002, + SPA_PROP_volume = 0x0010003, + SPA_PROP_mute = 0x0010004, + SPA_PROP_patternType = 0x0010005, + SPA_PROP_ditherType = 0x0010006, + SPA_PROP_truncate = 0x0010007, + SPA_PROP_channelVolumes = 0x0010008, + SPA_PROP_volumeBase = 0x0010009, + SPA_PROP_volumeStep = 0x001000A, + SPA_PROP_channelMap = 0x001000B, + SPA_PROP_monitorMute = 0x001000C, + SPA_PROP_monitorVolumes = 0x001000D, + SPA_PROP_latencyOffsetNsec = 0x001000E, + SPA_PROP_softMute = 0x001000F, + SPA_PROP_softVolumes = 0x0010010, + SPA_PROP_iec958Codecs = 0x0010011, + SPA_PROP_START_Video = 0x0020000, + SPA_PROP_brightness = 0x0020001, + SPA_PROP_contrast = 0x0020002, + SPA_PROP_saturation = 0x0020003, + SPA_PROP_hue = 0x0020004, + SPA_PROP_gamma = 0x0020005, + SPA_PROP_exposure = 0x0020006, + SPA_PROP_gain = 0x0020007, + SPA_PROP_sharpness = 0x0020008, + SPA_PROP_START_Other = 0x0080000, + SPA_PROP_params = 0x0080001, + SPA_PROP_START_CUSTOM = 0x1000000, +} + +ty! { + SpaAudioChannel; + + SPA_AUDIO_CHANNEL_UNKNOWN = 0x00000, + SPA_AUDIO_CHANNEL_NA = 0x00001, + + SPA_AUDIO_CHANNEL_MONO = 0x00002, + + SPA_AUDIO_CHANNEL_FL = 0x00003, + SPA_AUDIO_CHANNEL_FR = 0x00004, + SPA_AUDIO_CHANNEL_FC = 0x00005, + SPA_AUDIO_CHANNEL_LFE = 0x00006, + SPA_AUDIO_CHANNEL_SL = 0x00007, + SPA_AUDIO_CHANNEL_SR = 0x00008, + SPA_AUDIO_CHANNEL_FLC = 0x00009, + SPA_AUDIO_CHANNEL_FRC = 0x0000A, + SPA_AUDIO_CHANNEL_RC = 0x0000B, + SPA_AUDIO_CHANNEL_RL = 0x0000C, + SPA_AUDIO_CHANNEL_RR = 0x0000D, + SPA_AUDIO_CHANNEL_TC = 0x0000E, + SPA_AUDIO_CHANNEL_TFL = 0x0000F, + SPA_AUDIO_CHANNEL_TFC = 0x00010, + SPA_AUDIO_CHANNEL_TFR = 0x00011, + SPA_AUDIO_CHANNEL_TRL = 0x00012, + SPA_AUDIO_CHANNEL_TRC = 0x00013, + SPA_AUDIO_CHANNEL_TRR = 0x00014, + SPA_AUDIO_CHANNEL_RLC = 0x00015, + SPA_AUDIO_CHANNEL_RRC = 0x00016, + SPA_AUDIO_CHANNEL_FLW = 0x00017, + SPA_AUDIO_CHANNEL_FRW = 0x00018, + SPA_AUDIO_CHANNEL_LFE2 = 0x00019, + SPA_AUDIO_CHANNEL_FLH = 0x0001A, + SPA_AUDIO_CHANNEL_FCH = 0x0001B, + SPA_AUDIO_CHANNEL_FRH = 0x0001C, + SPA_AUDIO_CHANNEL_TFLC = 0x0001D, + SPA_AUDIO_CHANNEL_TFRC = 0x0001E, + SPA_AUDIO_CHANNEL_TSL = 0x0001F, + SPA_AUDIO_CHANNEL_TSR = 0x00020, + SPA_AUDIO_CHANNEL_LLFE = 0x00021, + SPA_AUDIO_CHANNEL_RLFE = 0x00022, + SPA_AUDIO_CHANNEL_BC = 0x00023, + SPA_AUDIO_CHANNEL_BLC = 0x00024, + SPA_AUDIO_CHANNEL_BRC = 0x00025, + + SPA_AUDIO_CHANNEL_AUX0 = 0x01000, + SPA_AUDIO_CHANNEL_AUX1 = 0x01001, + SPA_AUDIO_CHANNEL_AUX2 = 0x01002, + SPA_AUDIO_CHANNEL_AUX3 = 0x01003, + SPA_AUDIO_CHANNEL_AUX4 = 0x01004, + SPA_AUDIO_CHANNEL_AUX5 = 0x01005, + SPA_AUDIO_CHANNEL_AUX6 = 0x01006, + SPA_AUDIO_CHANNEL_AUX7 = 0x01007, + SPA_AUDIO_CHANNEL_AUX8 = 0x01008, + SPA_AUDIO_CHANNEL_AUX9 = 0x01009, + SPA_AUDIO_CHANNEL_AUX10 = 0x0100A, + SPA_AUDIO_CHANNEL_AUX11 = 0x0100B, + SPA_AUDIO_CHANNEL_AUX12 = 0x0100C, + SPA_AUDIO_CHANNEL_AUX13 = 0x0100D, + SPA_AUDIO_CHANNEL_AUX14 = 0x0100E, + SPA_AUDIO_CHANNEL_AUX15 = 0x0100F, + SPA_AUDIO_CHANNEL_AUX16 = 0x01010, + SPA_AUDIO_CHANNEL_AUX17 = 0x01011, + SPA_AUDIO_CHANNEL_AUX18 = 0x01012, + SPA_AUDIO_CHANNEL_AUX19 = 0x01013, + SPA_AUDIO_CHANNEL_AUX20 = 0x01014, + SPA_AUDIO_CHANNEL_AUX21 = 0x01015, + SPA_AUDIO_CHANNEL_AUX22 = 0x01016, + SPA_AUDIO_CHANNEL_AUX23 = 0x01017, + SPA_AUDIO_CHANNEL_AUX24 = 0x01018, + SPA_AUDIO_CHANNEL_AUX25 = 0x01019, + SPA_AUDIO_CHANNEL_AUX26 = 0x0101A, + SPA_AUDIO_CHANNEL_AUX27 = 0x0101B, + SPA_AUDIO_CHANNEL_AUX28 = 0x0101C, + SPA_AUDIO_CHANNEL_AUX29 = 0x0101D, + SPA_AUDIO_CHANNEL_AUX30 = 0x0101E, + SPA_AUDIO_CHANNEL_AUX31 = 0x0101F, + SPA_AUDIO_CHANNEL_AUX32 = 0x01020, + SPA_AUDIO_CHANNEL_AUX33 = 0x01021, + SPA_AUDIO_CHANNEL_AUX34 = 0x01022, + SPA_AUDIO_CHANNEL_AUX35 = 0x01023, + SPA_AUDIO_CHANNEL_AUX36 = 0x01024, + SPA_AUDIO_CHANNEL_AUX37 = 0x01025, + SPA_AUDIO_CHANNEL_AUX38 = 0x01026, + SPA_AUDIO_CHANNEL_AUX39 = 0x01027, + SPA_AUDIO_CHANNEL_AUX40 = 0x01028, + SPA_AUDIO_CHANNEL_AUX41 = 0x01029, + SPA_AUDIO_CHANNEL_AUX42 = 0x0102A, + SPA_AUDIO_CHANNEL_AUX43 = 0x0102B, + SPA_AUDIO_CHANNEL_AUX44 = 0x0102C, + SPA_AUDIO_CHANNEL_AUX45 = 0x0102D, + SPA_AUDIO_CHANNEL_AUX46 = 0x0102E, + SPA_AUDIO_CHANNEL_AUX47 = 0x0102F, + SPA_AUDIO_CHANNEL_AUX48 = 0x01030, + SPA_AUDIO_CHANNEL_AUX49 = 0x01031, + SPA_AUDIO_CHANNEL_AUX50 = 0x01032, + SPA_AUDIO_CHANNEL_AUX51 = 0x01033, + SPA_AUDIO_CHANNEL_AUX52 = 0x01034, + SPA_AUDIO_CHANNEL_AUX53 = 0x01035, + SPA_AUDIO_CHANNEL_AUX54 = 0x01036, + SPA_AUDIO_CHANNEL_AUX55 = 0x01037, + SPA_AUDIO_CHANNEL_AUX56 = 0x01038, + SPA_AUDIO_CHANNEL_AUX57 = 0x01039, + SPA_AUDIO_CHANNEL_AUX58 = 0x0103A, + SPA_AUDIO_CHANNEL_AUX59 = 0x0103B, + SPA_AUDIO_CHANNEL_AUX60 = 0x0103C, + SPA_AUDIO_CHANNEL_AUX61 = 0x0103D, + SPA_AUDIO_CHANNEL_AUX62 = 0x0103E, + SPA_AUDIO_CHANNEL_AUX63 = 0x0103F, + + SPA_AUDIO_CHANNEL_LAST_Aux = 0x01fff, + + SPA_AUDIO_CHANNEL_START_Custom = 0x10000, +} + +ty! { + SpaAudioIec958Codec; + + SPA_AUDIO_IEC958_CODEC_UNKNOWN = 0, + + SPA_AUDIO_IEC958_CODEC_PCM = 1, + SPA_AUDIO_IEC958_CODEC_DTS = 2, + SPA_AUDIO_IEC958_CODEC_AC3 = 3, + SPA_AUDIO_IEC958_CODEC_MPEG = 4, + SPA_AUDIO_IEC958_CODEC_MPEG2_AAC = 5, + + SPA_AUDIO_IEC958_CODEC_EAC3 = 6, + + SPA_AUDIO_IEC958_CODEC_TRUEHD = 7, + SPA_AUDIO_IEC958_CODEC_DTSHD = 8, +} + +ty! { + SpaParamBitorder; + + SPA_PARAM_BITORDER_unknown = 0, + SPA_PARAM_BITORDER_msb = 1, + SPA_PARAM_BITORDER_lsb = 2, +} +ty! { + SpaNodeCommand; + + SPA_NODE_COMMAND_Suspend = 0, + SPA_NODE_COMMAND_Pause = 1, + SPA_NODE_COMMAND_Start = 2, + SPA_NODE_COMMAND_Enable = 3, + SPA_NODE_COMMAND_Disable = 4, + SPA_NODE_COMMAND_Flush = 5, + SPA_NODE_COMMAND_Drain = 6, + SPA_NODE_COMMAND_Marker = 7, + SPA_NODE_COMMAND_ParamBegin = 8, + SPA_NODE_COMMAND_ParamEnd = 9, + SPA_NODE_COMMAND_RequestProcess = 10, +} + +#[derive(Copy, Clone)] +pub enum PwPod<'a> { + None, + Bool(bool), + Id(u32), + Int(i32), + Long(i64), + Float(f32), + Double(f64), + String(&'a BStr), + Bytes(&'a [u8]), + Rectangle(PwPodRectangle), + Fraction(PwPodFraction), + Bitmap(&'a [u8]), + Array(PwPodArray<'a>), + Struct(PwPodStruct<'a>), + Object(PwPodObject<'a>), + Sequence(PwPodSequence<'a>), + Pointer(PwPodPointer), + Fd(u64), + Choice(PwPodChoice<'a>), +} + +#[derive(Copy, Clone, Debug)] +pub struct PwPodRectangle { + pub width: u32, + pub height: u32, +} + +#[derive(Copy, Clone, Debug)] +pub struct PwPodFraction { + pub num: u32, + pub denom: u32, +} + +#[derive(Copy, Clone)] +pub struct PwPodArray<'a> { + pub ty: PwPodType, + pub child_len: usize, + pub n_elements: usize, + pub elements: PwParser<'a>, +} + +#[derive(Copy, Clone)] +pub struct PwPodStruct<'a> { + pub fields: PwParser<'a>, +} + +#[derive(Copy, Clone)] +pub struct PwPodObject<'a> { + pub ty: PwPodObjectType, + pub id: u32, + pub probs: PwParser<'a>, +} + +impl<'a> PwPodObject<'a> { + pub fn get_param(&mut self, key: u32) -> Result>, PwParserError> { + let start = self.probs.pos(); + loop { + if self.probs.len() == 0 { + self.probs.reset(); + } else { + let prob = self.probs.read_prop()?; + if prob.key == key { + return Ok(Some(prob)); + } + } + if self.probs.pos() == start { + return Ok(None); + } + } + } +} + +#[derive(Copy, Clone)] +pub struct PwPodSequence<'a> { + pub unit: u32, + pub controls: PwParser<'a>, +} + +#[derive(Copy, Clone, Debug)] +pub struct PwPodControl<'a> { + pub _offset: u32, + pub _ty: PwControlType, + pub _value: PwPod<'a>, +} + +#[derive(Copy, Clone, Debug)] +pub struct PwPodPointer { + pub _ty: PwPointerType, + pub _value: usize, +} + +#[derive(Copy, Clone, Debug)] +pub struct PwPodChoice<'a> { + pub ty: PwChoiceType, + pub flags: u32, + pub elements: PwPodArray<'a>, +} + +#[derive(Copy, Clone, Debug)] +pub struct PwProp<'a> { + pub key: u32, + pub flags: PwPropFlag, + pub pod: PwPod<'a>, +} + +impl<'a> PwPod<'a> { + pub fn ty(&self) -> PwPodType { + match self { + PwPod::None => PW_TYPE_None, + PwPod::Bool(_) => PW_TYPE_Bool, + PwPod::Id(_) => PW_TYPE_Id, + PwPod::Int(_) => PW_TYPE_Int, + PwPod::Long(_) => PW_TYPE_Long, + PwPod::Float(_) => PW_TYPE_Float, + PwPod::Double(_) => PW_TYPE_Double, + PwPod::String(_) => PW_TYPE_String, + PwPod::Bytes(_) => PW_TYPE_Bytes, + PwPod::Rectangle(_) => PW_TYPE_Rectangle, + PwPod::Fraction(_) => PW_TYPE_Fraction, + PwPod::Bitmap(_) => PW_TYPE_Bitmap, + PwPod::Array(_) => PW_TYPE_Array, + PwPod::Struct(_) => PW_TYPE_Struct, + PwPod::Object(_) => PW_TYPE_Object, + PwPod::Sequence(_) => PW_TYPE_Sequence, + PwPod::Pointer(_) => PW_TYPE_Pointer, + PwPod::Fd(_) => PW_TYPE_Fd, + PwPod::Choice(_) => PW_TYPE_Choice, + } + } + + pub fn get_fraction(&self) -> Result { + match self.get_value()? { + PwPod::Fraction(i) => Ok(i), + _ => Err(PwParserError::UnexpectedPodType( + PW_TYPE_Fraction, + self.ty(), + )), + } + } + + pub fn get_rectangle(&self) -> Result { + match self.get_value()? { + PwPod::Rectangle(i) => Ok(i), + _ => Err(PwParserError::UnexpectedPodType( + PW_TYPE_Rectangle, + self.ty(), + )), + } + } + + pub fn get_id(&self) -> Result { + match self.get_value()? { + PwPod::Id(i) => Ok(i), + _ => Err(PwParserError::UnexpectedPodType(PW_TYPE_Id, self.ty())), + } + } + + pub fn get_value(mut self) -> Result, PwParserError> { + if let PwPod::Choice(v) = &mut self + && v.ty == PW_CHOICE_None + && v.elements.n_elements > 0 + { + return v + .elements + .elements + .read_pod_body_packed(v.elements.ty, v.elements.child_len); + } + Ok(self) + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_fraction { + pub num: u32, + pub denom: u32, +} + +bitflags! { + SPA_IO_SEGMENT_VIDEO_FLAG: u32; + + SPA_IO_SEGMENT_VIDEO_FLAG_VALID = 1<<0, + SPA_IO_SEGMENT_VIDEO_FLAG_DROP_FRAME = 1<<1, + SPA_IO_SEGMENT_VIDEO_FLAG_PULL_DOWN = 1<<2, + SPA_IO_SEGMENT_VIDEO_FLAG_INTERLACED = 1<<3, +} + +#[repr(C)] +#[derive(Debug)] +pub struct spa_io_segment_video { + pub flags: SPA_IO_SEGMENT_VIDEO_FLAG, + pub offset: u32, + pub framerate: spa_fraction, + pub hours: u32, + pub minutes: u32, + pub seconds: u32, + pub frames: u32, + pub field_count: u32, + pub padding: [u32; 11], +} + +bitflags! { + SPA_IO_SEGMENT_BAR_FLAG: u32; + + SPA_IO_SEGMENT_BAR_FLAG_VALID = 1<<0, +} + +#[repr(C)] +#[derive(Debug)] +pub struct spa_io_segment_bar { + pub flags: SPA_IO_SEGMENT_BAR_FLAG, + pub offset: u32, + pub signature_num: f32, + pub signature_denom: f32, + pub bpm: f64, + pub beat: f64, + pub padding: [u32; 8], +} + +bitflags! { + SPA_IO_SEGMENT_FLAG: u32; + + SPA_IO_SEGMENT_FLAG_LOOPING = 1<<0, + SPA_IO_SEGMENT_FLAG_NO_POSITION = 1<<1, +} + +#[repr(C)] +#[derive(Debug)] +pub struct spa_io_segment { + pub version: u32, + pub flags: SPA_IO_SEGMENT_FLAG, + pub start: u64, + pub duration: u64, + pub rate: f64, + pub position: u64, + pub bar: spa_io_segment_bar, + pub video: spa_io_segment_video, +} + +bitflags! { + SPA_IO_CLOCK_FLAG: u32; + + SPA_IO_CLOCK_FLAG_FREEWHEEL = 1<<0, +} + +#[repr(C)] +#[derive(Debug)] +pub struct spa_io_clock { + pub flags: SPA_IO_CLOCK_FLAG, + pub id: u32, + pub name: [u8; 64], + pub nsec: u64, + pub rate: spa_fraction, + pub position: u64, + pub duration: u64, + pub delay: i64, + pub rate_diff: f64, + pub next_nsec: u64, + pub padding: [u32; 8], +} + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct spa_rectangle { + pub width: u32, + pub height: u32, +} + +bitflags! { + SPA_IO_VIDEO_SIZE: u32; + + SPA_IO_VIDEO_SIZE_VALID = 1<<0, +} + +#[repr(C)] +#[derive(Debug)] +pub struct spa_io_video_size { + pub flags: SPA_IO_VIDEO_SIZE, + pub stride: u32, + pub size: spa_rectangle, + pub framerate: spa_fraction, + pub padding: [u32; 4], +} + +pub const SPA_IO_POSITION_MAX_SEGMENTS: usize = 8; + +#[repr(C)] +#[derive(Debug)] +pub struct spa_io_position { + pub clock: spa_io_clock, + pub video: spa_io_video_size, + pub offset: i64, + pub state: u32, + pub n_segments: u32, + pub segments: [spa_io_segment; SPA_IO_POSITION_MAX_SEGMENTS], +} + +#[repr(C)] +#[derive(Debug)] +pub struct pw_node_activation_state { + pub status: c::c_int, + pub required: AtomicI32, + pub pending: AtomicI32, +} + +ty! { + PW_NODE_ACTIVATION; + + PW_NODE_ACTIVATION_NOT_TRIGGERED = 0, + PW_NODE_ACTIVATION_TRIGGERED = 1, + PW_NODE_ACTIVATION_AWAKE = 2, + PW_NODE_ACTIVATION_FINISHED = 3, +} + +ty! { + PW_NODE_ACTIVATION_COMMAND; + + PW_NODE_ACTIVATION_COMMAND_NONE = 0, + PW_NODE_ACTIVATION_COMMAND_START = 1, + PW_NODE_ACTIVATION_COMMAND_STOP = 2, +} + +#[repr(C)] +#[derive(Debug)] +pub struct pw_node_activation { + pub status: AtomicU32, + + pub flags: c::c_uint, + + pub state: [pw_node_activation_state; 2], + + pub signal_time: u64, + pub awake_time: u64, + pub finish_time: u64, + pub prev_signal_time: u64, + + pub reposition: spa_io_segment, + pub segment: spa_io_segment, + + pub segment_owner: [u32; 32], + pub position: spa_io_position, + + pub sync_timeout: u64, + pub sync_left: u64, + + pub cpu_load: [f32; 3], + pub xrun_count: u32, + pub xrun_time: u64, + pub xrun_delay: u64, + pub max_delay: u64, + + pub command: PW_NODE_ACTIVATION_COMMAND, + pub reposition_owner: u32, +} + +unsafe impl Pod for pw_node_activation {} + +bitflags! { + SPA_PORT_FLAG: u64; + + SPA_PORT_FLAG_REMOVABLE = 1<<0, + SPA_PORT_FLAG_OPTIONAL = 1<<1, + SPA_PORT_FLAG_CAN_ALLOC_BUFFERS = 1<<2, + SPA_PORT_FLAG_IN_PLACE = 1<<3, + SPA_PORT_FLAG_NO_REF = 1<<4, + SPA_PORT_FLAG_LIVE = 1<<5, + SPA_PORT_FLAG_PHYSICAL = 1<<6, + SPA_PORT_FLAG_TERMINAL = 1<<7, + SPA_PORT_FLAG_DYNAMIC_DATA = 1<<8, +} + +bitflags! { + SpaStatus: u32; + + SPA_STATUS_NEED_DATA = 1 << 0, + SPA_STATUS_HAVE_DATA = 1 << 1, + SPA_STATUS_STOPPED = 1 << 2, + SPA_STATUS_DRAINED = 1 << 3, +} + +#[repr(C)] +#[derive(Debug)] +pub struct spa_io_buffers { + pub status: AtomicU32, + pub buffer_id: AtomicU32, +} + +unsafe impl Pod for spa_io_buffers {} + +bitflags! { + SpaChunkFlags: u32; + + SPA_CHUNK_FLAG_CORRUPTED = 1, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct spa_chunk { + pub offset: u32, + pub size: u32, + pub stride: u32, + pub flags: SpaChunkFlags, +} + +unsafe impl Pod for spa_chunk {} diff --git a/src/pipewire/pw_pod/pw_debug.rs b/src/pipewire/pw_pod/pw_debug.rs new file mode 100644 index 00000000..feb98420 --- /dev/null +++ b/src/pipewire/pw_pod/pw_debug.rs @@ -0,0 +1,464 @@ +use { + crate::{ + pipewire::{ + pw_parser::PwParser, + pw_pod::{ + PW_COMMAND_Node, PW_OBJECT_Format, PW_OBJECT_ParamBuffers, PW_OBJECT_ParamIO, + PW_OBJECT_ParamLatency, PW_OBJECT_ParamMeta, PW_OBJECT_ParamPortConfig, + PW_OBJECT_ParamProcessLatency, PW_OBJECT_ParamProfile, PW_OBJECT_ParamRoute, + PW_OBJECT_Profiler, PW_OBJECT_PropInfo, PW_OBJECT_Props, PW_TYPE_Id, PwPod, + PwPodArray, PwPodObject, PwPodObjectType, PwPodSequence, PwPodStruct, PwPodType, + PwProp, SPA_FORMAT_AUDIO_bitorder, SPA_FORMAT_AUDIO_format, + SPA_FORMAT_AUDIO_iec958Codec, SPA_FORMAT_AUDIO_position, + SPA_FORMAT_VIDEO_H264_alignment, SPA_FORMAT_VIDEO_H264_streamFormat, + SPA_FORMAT_VIDEO_chromaSite, SPA_FORMAT_VIDEO_colorMatrix, + SPA_FORMAT_VIDEO_colorPrimaries, SPA_FORMAT_VIDEO_colorRange, + SPA_FORMAT_VIDEO_format, SPA_FORMAT_VIDEO_interlaceMode, + SPA_FORMAT_VIDEO_multiviewFlags, SPA_FORMAT_VIDEO_multiviewMode, + SPA_FORMAT_VIDEO_transferFunction, SPA_FORMAT_mediaSubtype, SPA_FORMAT_mediaType, + SPA_PARAM_BUFFERS_dataType, SPA_PARAM_IO_id, SPA_PARAM_META_type, + SPA_PARAM_PORT_CONFIG_direction, SPA_PARAM_PORT_CONFIG_mode, + SPA_PARAM_PROFILE_available, SPA_PARAM_ROUTE_available, SPA_PARAM_ROUTE_direction, + SPA_PROP_channelMap, SPA_PROP_iec958Codecs, SpaAudioChannel, SpaAudioFormat, + SpaAudioIec958Codec, SpaDataTypes, SpaDirection, SpaFormat, SpaH264Alignment, + SpaH264StreamFormat, SpaIoType, SpaMediaSubtype, SpaMediaType, SpaMetaType, + SpaNodeCommand, SpaParamAvailability, SpaParamBitorder, SpaParamBuffers, + SpaParamIo, SpaParamLatency, SpaParamMeta, SpaParamPortConfig, + SpaParamPortConfigMode, SpaParamProcessLatency, SpaParamProfile, SpaParamRoute, + SpaParamType, SpaProfiler, SpaProp, SpaPropInfo, SpaVideoChromaSite, + SpaVideoColorMatrix, SpaVideoColorPrimaries, SpaVideoColorRange, SpaVideoFormat, + SpaVideoInterlaceMode, SpaVideoMultiviewFlags, SpaVideoMultiviewMode, + SpaVideoTransferFunction, + }, + }, + utils::errorfmt::ErrorFmt, + }, + std::{ + fmt, + fmt::{Debug, DebugList, Formatter, Write}, + }, +}; + +trait PwPodObjectDebugger: Sync { + fn debug_property(&self, fmt: &mut Formatter<'_>, value: PwProp<'_>) -> std::fmt::Result; + fn id_name(&self, id: u32) -> Option<&'static str>; +} + +struct PwPodObjectDebuggerSimple { + key_name: F, + debug_pod: G, + id_name: H, +} + +impl PwPodObjectDebugger for PwPodObjectDebuggerSimple +where + F: Fn(u32) -> Option<&'static str> + Sync, + G: Fn(u32, &mut Formatter<'_>, PwPod<'_>) -> std::fmt::Result + Sync, + H: Fn(u32) -> Option<&'static str> + Sync, +{ + fn debug_property(&self, fmt: &mut Formatter<'_>, value: PwProp<'_>) -> std::fmt::Result { + let mut s = fmt.debug_struct("PwProp"); + match (self.key_name)(value.key) { + Some(n) => s.field("key", &n), + _ => s.field("key", &value.key), + }; + s.field("flags", &value.flags) + .field( + "pod", + &fmt::from_fn(|f| (self.debug_pod)(value.key, f, value.pod)), + ) + .finish() + } + + fn id_name(&self, id: u32) -> Option<&'static str> { + (self.id_name)(id) + } +} + +fn choice_debug(fmt: &mut Formatter<'_>, p: PwPod<'_>, ty: PwPodType, f: F) -> std::fmt::Result +where + F: Fn(&mut Formatter<'_>, PwPod<'_>) -> std::fmt::Result, +{ + match p { + PwPod::Choice(c) if c.elements.ty == ty => fmt + .debug_struct("choice") + .field("ty", &c.ty) + .field("flags", &c.flags) + .field( + "elements", + &fmt::from_fn(|fmt| { + array_body_debug(fmt, c.elements, |l, p| { + match p.read_pod_body_packed(ty, c.elements.child_len) { + Ok(p) => { + l.entry(&fmt::from_fn(|fmt| f(fmt, p))); + true + } + Err(e) => { + let e = ErrorFmt(e); + l.entry(&fmt::from_fn(|fmt| { + write!(fmt, "Could not read choice element: {}", e) + })); + false + } + } + }) + }), + ) + .finish(), + _ if p.ty() == ty => f(fmt, p), + _ => p.fmt(fmt), + } +} + +fn id_debug(fmt: &mut Formatter<'_>, p: PwPod<'_>, f: F) -> std::fmt::Result +where + F: Fn(&mut Formatter<'_>, u32) -> std::fmt::Result, +{ + choice_debug(fmt, p, PW_TYPE_Id, |fmt, p| match p { + PwPod::Id(id) => f(fmt, id), + _ => p.fmt(fmt), + }) +} + +fn array_body_debug(fmt: &mut Formatter<'_>, mut a: PwPodArray<'_>, f: F) -> std::fmt::Result +where + F: Fn(&mut DebugList, &mut PwParser<'_>) -> bool, +{ + let mut l = fmt.debug_list(); + for _ in 0..a.n_elements { + if !f(&mut l, &mut a.elements) { + break; + } + } + l.finish() +} + +fn array_debug(fmt: &mut Formatter<'_>, p: PwPod<'_>, ty: PwPodType, f: F) -> std::fmt::Result +where + F: Fn(&mut DebugList, &mut PwParser<'_>) -> bool, +{ + match p { + PwPod::Array(a) if a.ty == ty => array_body_debug(fmt, a, f), + _ => p.fmt(fmt), + } +} + +fn array_id_debug(fmt: &mut Formatter<'_>, p: PwPod<'_>, f: F) -> std::fmt::Result +where + F: Fn(&mut DebugList, u32) -> T, +{ + array_debug(fmt, p, PW_TYPE_Id, |l, p| match p.read_id() { + Ok(a) => { + f(l, a); + true + } + Err(e) => { + let e = ErrorFmt(e); + l.entry(&fmt::from_fn(|f| write!(f, "Could not read id: {}", e))); + false + } + }) +} + +fn object_id_name(id: u32) -> Option<&'static str> { + SpaParamType(id).name() +} + +fn command_id_name(id: u32) -> Option<&'static str> { + SpaNodeCommand(id).name() +} + +static PROP_INFO_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaPropInfo(key).name(), + debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), + id_name: object_id_name, +}; + +static PROPS_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaProp(key).name(), + id_name: object_id_name, + debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaProp(key) { + SPA_PROP_channelMap => array_id_debug(f, p, |l, a| { + l.entry(&SpaAudioChannel(a)); + }), + SPA_PROP_iec958Codecs => array_id_debug(f, p, |l, a| { + l.entry(&SpaAudioIec958Codec(a)); + }), + _ => p.fmt(f), + }, +}; + +static FORMAT_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaFormat(key).name(), + id_name: object_id_name, + debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaFormat(key) { + SPA_FORMAT_mediaType => id_debug(f, p, |f, a| SpaMediaType(a).fmt(f)), + SPA_FORMAT_mediaSubtype => id_debug(f, p, |f, a| SpaMediaSubtype(a).fmt(f)), + SPA_FORMAT_AUDIO_format => id_debug(f, p, |f, a| SpaAudioFormat(a).fmt(f)), + SPA_FORMAT_AUDIO_position => array_id_debug(f, p, |l, a| { + l.entry(&SpaAudioChannel(a)); + }), + SPA_FORMAT_AUDIO_iec958Codec => id_debug(f, p, |f, a| SpaAudioIec958Codec(a).fmt(f)), + SPA_FORMAT_AUDIO_bitorder => id_debug(f, p, |f, a| SpaParamBitorder(a).fmt(f)), + SPA_FORMAT_VIDEO_format => id_debug(f, p, |f, a| SpaVideoFormat(a).fmt(f)), + SPA_FORMAT_VIDEO_interlaceMode => id_debug(f, p, |f, a| SpaVideoInterlaceMode(a).fmt(f)), + SPA_FORMAT_VIDEO_multiviewMode => id_debug(f, p, |f, a| SpaVideoMultiviewMode(a).fmt(f)), + SPA_FORMAT_VIDEO_multiviewFlags => id_debug(f, p, |f, a| SpaVideoMultiviewFlags(a).fmt(f)), + SPA_FORMAT_VIDEO_chromaSite => id_debug(f, p, |f, a| SpaVideoChromaSite(a).fmt(f)), + SPA_FORMAT_VIDEO_colorRange => id_debug(f, p, |f, a| SpaVideoColorRange(a).fmt(f)), + SPA_FORMAT_VIDEO_colorMatrix => id_debug(f, p, |f, a| SpaVideoColorMatrix(a).fmt(f)), + SPA_FORMAT_VIDEO_transferFunction => { + id_debug(f, p, |f, a| SpaVideoTransferFunction(a).fmt(f)) + } + SPA_FORMAT_VIDEO_colorPrimaries => id_debug(f, p, |f, a| SpaVideoColorPrimaries(a).fmt(f)), + SPA_FORMAT_VIDEO_H264_streamFormat => id_debug(f, p, |f, a| SpaH264StreamFormat(a).fmt(f)), + SPA_FORMAT_VIDEO_H264_alignment => id_debug(f, p, |f, a| SpaH264Alignment(a).fmt(f)), + _ => p.fmt(f), + }, +}; + +static PARAM_BUFFERS_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaParamBuffers(key).name(), + id_name: object_id_name, + debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamBuffers(key) { + SPA_PARAM_BUFFERS_dataType => match p { + PwPod::Int(v) => SpaDataTypes(v as _).fmt(f), + _ => p.fmt(f), + }, + _ => p.fmt(f), + }, +}; + +static PARAM_META_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaParamMeta(key).name(), + id_name: object_id_name, + debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamMeta(key) { + SPA_PARAM_META_type => id_debug(f, p, |f, b| SpaMetaType(b).fmt(f)), + _ => p.fmt(f), + }, +}; + +static PARAM_IO_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaParamIo(key).name(), + id_name: object_id_name, + debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamIo(key) { + SPA_PARAM_IO_id => id_debug(f, p, |f, b| SpaIoType(b).fmt(f)), + _ => p.fmt(f), + }, +}; + +static PARAM_PROFILE_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaParamProfile(key).name(), + id_name: object_id_name, + debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamProfile(key) { + SPA_PARAM_PROFILE_available => id_debug(f, p, |f, b| SpaParamAvailability(b).fmt(f)), + _ => p.fmt(f), + }, +}; + +static PARAM_PORT_CONFIG_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaParamPortConfig(key).name(), + id_name: object_id_name, + debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamPortConfig(key) { + SPA_PARAM_PORT_CONFIG_direction => id_debug(f, p, |f, b| SpaDirection(b).fmt(f)), + SPA_PARAM_PORT_CONFIG_mode => id_debug(f, p, |f, b| SpaParamPortConfigMode(b).fmt(f)), + _ => p.fmt(f), + }, +}; + +static PARAM_ROUTE_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaParamRoute(key).name(), + id_name: object_id_name, + debug_pod: |key, f: &mut Formatter<'_>, p: PwPod<'_>| match SpaParamRoute(key) { + SPA_PARAM_ROUTE_direction => id_debug(f, p, |f, b| SpaDirection(b).fmt(f)), + SPA_PARAM_ROUTE_available => id_debug(f, p, |f, b| SpaParamAvailability(b).fmt(f)), + _ => p.fmt(f), + }, +}; + +static PROFILER_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaProfiler(key).name(), + id_name: object_id_name, + debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), +}; + +static PARAM_LATENCY_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaParamLatency(key).name(), + id_name: object_id_name, + debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), +}; + +static PARAM_PROCESS_LATENCY_DEBUGGER: &'static dyn PwPodObjectDebugger = + &PwPodObjectDebuggerSimple { + key_name: |key| SpaParamProcessLatency(key).name(), + id_name: object_id_name, + debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), + }; + +static COMMAND_NODE_DEBUGGER: &'static dyn PwPodObjectDebugger = &PwPodObjectDebuggerSimple { + key_name: |key| SpaNodeCommand(key).name(), + id_name: command_id_name, + debug_pod: |_, f: &mut Formatter<'_>, p: PwPod<'_>| p.fmt(f), +}; + +fn object_debugger(obj: PwPodObjectType) -> Option<&'static dyn PwPodObjectDebugger> { + let res: &dyn PwPodObjectDebugger = match obj { + PW_OBJECT_PropInfo => PROP_INFO_DEBUGGER, + PW_OBJECT_Props => PROPS_DEBUGGER, + PW_OBJECT_Format => FORMAT_DEBUGGER, + PW_OBJECT_ParamBuffers => PARAM_BUFFERS_DEBUGGER, + PW_OBJECT_ParamMeta => PARAM_META_DEBUGGER, + PW_OBJECT_ParamIO => PARAM_IO_DEBUGGER, + PW_OBJECT_ParamProfile => PARAM_PROFILE_DEBUGGER, + PW_OBJECT_ParamPortConfig => PARAM_PORT_CONFIG_DEBUGGER, + PW_OBJECT_ParamRoute => PARAM_ROUTE_DEBUGGER, + PW_OBJECT_Profiler => PROFILER_DEBUGGER, + PW_OBJECT_ParamLatency => PARAM_LATENCY_DEBUGGER, + PW_OBJECT_ParamProcessLatency => PARAM_PROCESS_LATENCY_DEBUGGER, + PW_COMMAND_Node => COMMAND_NODE_DEBUGGER, + _ => return None, + }; + Some(res) +} + +impl<'a> Debug for PwPodObject<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let debugger = object_debugger(self.ty); + let mut s = f.debug_struct("object"); + s.field("type", &self.ty); + let name; + let mut id: &dyn Debug = &self.id; + if let Some(d) = debugger + && let Some(n) = d.id_name(self.id) + { + name = n; + id = &name; + } + s.field("id", id); + s.field( + "props", + &fmt::from_fn(|f| { + let mut l = f.debug_list(); + let mut parser = self.probs; + while parser.len() > 0 { + match parser.read_prop() { + Ok(p) => match debugger { + Some(d) => l.entry(&fmt::from_fn(|fmt| d.debug_property(fmt, p))), + _ => l.entry(&p), + }, + Err(e) => { + let e = ErrorFmt(e); + l.entry(&fmt::from_fn(|f| { + write!(f, "Could not read object property: {}", &e) + })); + break; + } + }; + } + l.finish() + }), + ); + s.finish() + } +} + +impl<'a> Debug for PwPodSequence<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut s = f.debug_struct("sequence"); + s.field("unit", &self.unit); + s.field( + "controls", + &fmt::from_fn(|f| { + let mut l = f.debug_list(); + let mut parser = self.controls; + while parser.len() > 0 { + match parser.read_control() { + Ok(c) => l.entry(&c), + Err(e) => { + let e = ErrorFmt(e); + l.entry(&fmt::from_fn(|f| { + write!(f, "Could not read control element: {}", &e) + })); + break; + } + }; + } + l.finish() + }), + ); + s.finish() + } +} + +impl<'a> Debug for PwPodStruct<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut parser = self.fields; + let mut s = f.debug_struct("struct"); + let mut field = String::new(); + for i in 0.. { + if parser.len() == 0 { + break; + } + field.clear(); + let _ = write!(&mut field, "\"{}\"", i); + match parser.read_pod() { + Ok(p) => s.field(&field, &p), + Err(e) => { + let e = ErrorFmt(e); + s.field( + &field, + &fmt::from_fn(|f| write!(f, "Could not parse struct field: {}", &e)), + ); + break; + } + }; + } + s.finish() + } +} + +impl<'a> Debug for PwPodArray<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut list = f.debug_list(); + let mut parser = self.elements; + for _ in 0..self.n_elements { + match parser.read_pod_body_packed(self.ty, self.child_len) { + Ok(e) => list.entry(&e), + Err(e) => { + let e = ErrorFmt(e); + list.entry(&fmt::from_fn(|f| { + write!(f, "Could not parse array element: {}", &e) + })); + break; + } + }; + } + list.finish() + } +} + +impl<'a> Debug for PwPod<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PwPod::None => write!(f, "None"), + PwPod::Bool(b) => write!(f, "{}", b), + PwPod::Id(id) => write!(f, "id({})", id), + PwPod::Int(i) => write!(f, "int({})", i), + PwPod::Long(l) => write!(f, "long({})", l), + PwPod::Float(v) => write!(f, "float({})", v), + PwPod::Double(d) => write!(f, "double({})", d), + PwPod::String(s) => write!(f, "string({:?})", s), + PwPod::Bytes(b) => write!(f, "bytes(len = {})", b.len()), + PwPod::Rectangle(r) => write!(f, "rectangle({}x{})", r.width, r.height), + PwPod::Fraction(v) => write!(f, "fraction({}/{})", v.num, v.denom), + PwPod::Bitmap(b) => write!(f, "bitmap(len = {})", b.len()), + PwPod::Array(a) => a.fmt(f), + PwPod::Struct(s) => s.fmt(f), + PwPod::Object(o) => o.fmt(f), + PwPod::Sequence(s) => s.fmt(f), + PwPod::Pointer(p) => p.fmt(f), + PwPod::Fd(v) => write!(f, "fd({})", v), + PwPod::Choice(c) => c.fmt(f), + } + } +} diff --git a/src/portal.rs b/src/portal.rs new file mode 100644 index 00000000..1b02f148 --- /dev/null +++ b/src/portal.rs @@ -0,0 +1,352 @@ +mod ptl_display; +mod ptl_remote_desktop; +mod ptl_render_ctx; +mod ptl_screencast; +mod ptl_session; +mod ptl_text; +mod ptr_gui; + +use { + crate::{ + async_engine::AsyncEngine, + cli::GlobalArgs, + cmm::cmm_manager::ColorManager, + dbus::{ + BUS_DEST, BUS_PATH, DBUS_NAME_FLAG_DO_NOT_QUEUE, DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER, + Dbus, DbusSocket, + }, + eventfd_cache::EventfdCache, + forker::ForkerError, + io_uring::IoUring, + logger::{LogLevel, Logger}, + pipewire::pw_con::{PwCon, PwConHolder, PwConOwner}, + portal::{ + ptl_display::{PortalDisplay, PortalDisplayId, watch_displays}, + ptl_remote_desktop::add_remote_desktop_dbus_members, + ptl_render_ctx::PortalRenderCtx, + ptl_screencast::add_screencast_dbus_members, + ptl_session::PortalSession, + }, + utils::{ + clone3::{Forked, fork_with_pidfd}, + copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, + line_logger::log_lines, + numcell::NumCell, + oserror::{OsError, OsErrorExt}, + pipe::{Pipe, pipe}, + process_name::set_process_name, + run_toplevel::RunToplevel, + xrd::xrd, + }, + version::VERSION, + video::dmabuf::DmaBufIds, + wheel::Wheel, + wire_dbus::org, + }, + std::{ + ffi::OsStr, + io::{BufReader, BufWriter}, + os::unix::{ffi::OsStrExt, process::CommandExt}, + process::{Command, exit}, + rc::{Rc, Weak}, + sync::Arc, + }, + thiserror::Error, + uapi::{OwnedFd, WEXITSTATUS, c, getpid}, +}; + +const PORTAL_SUCCESS: u32 = 0; +#[expect(dead_code)] +const PORTAL_CANCELLED: u32 = 1; +#[expect(dead_code)] +const PORTAL_ENDED: u32 = 2; + +pub fn run_freestanding(global: GlobalArgs) { + let logger = Logger::install_stderr(global.log_level); + run(logger, true); +} + +#[derive(Debug, Error)] +pub enum PortalError { + #[error("Could not create pipe")] + CreatePipe(#[source] OsError), + #[error("Could not fork")] + Fork(#[source] ForkerError), +} + +pub struct PortalStartup { + logs: Rc, + pid: c::pid_t, + pidfd: Rc, +} + +impl PortalStartup { + pub async fn spawn(self, eng: Rc, ring: Rc, logger: Arc) { + let f1 = eng.spawn("check portal exit code", { + let ring = ring.clone(); + async move { + if let Err(e) = ring.readable(&self.pidfd).await { + log::error!( + "Could not wait for portal pidfd to become readable: {}", + ErrorFmt(e) + ); + return; + } + let (_, status) = match uapi::waitpid(self.pid, 0).to_os_error() { + Ok(r) => r, + Err(e) => { + log::error!( + "Could not retrieve exit status of portal ({}): {}", + self.pid, + ErrorFmt(e), + ); + return; + } + }; + let status = WEXITSTATUS(status); + if status != 0 { + log::error!("Portal exited with non-0 exit code: {status}"); + } + } + }); + let f2 = eng.spawn("portal logger", { + let ring = ring.clone(); + let logger = logger.clone(); + async move { + let res = log_lines(&ring, &self.logs, |left, right| { + logger.write_raw(left); + logger.write_raw(right); + logger.write_raw(b" (portal)\n"); + }) + .await; + if let Err(e) = res { + log::error!("Could not read portal logs: {}", ErrorFmt(e)); + } + } + }); + f1.await; + f2.await; + } +} + +pub fn run_from_compositor(level: LogLevel) -> Result { + let Pipe { read, write } = match pipe() { + Ok(p) => p, + Err(e) => return Err(PortalError::CreatePipe(e)), + }; + let fork = match fork_with_pidfd(false) { + Ok(f) => f, + Err(e) => return Err(PortalError::Fork(e)), + }; + match fork { + Forked::Parent { pidfd, pid } => Ok(PortalStartup { + logs: Rc::new(read), + pid, + pidfd: Rc::new(pidfd), + }), + Forked::Child { .. } => { + drop(read); + let logger = Logger::install_pipe(write, level); + run(logger, false); + } + } +} + +fn run(logger: Arc, freestanding: bool) -> ! { + let Pipe { read, write } = match pipe() { + Ok(p) => p, + Err(e) => { + fatal!("Could not create a pipe: {}", ErrorFmt(e)); + } + }; + let fork = match fork_with_pidfd(false) { + Ok(f) => f, + Err(e) => { + fatal!("Could not fork: {}", ErrorFmt(e)); + } + }; + let Forked::Parent { pid, .. } = fork else { + drop(read); + run2(logger, write); + exit(0); + }; + drop(write); + let read = BufReader::new(read); + let Ok(log_file) = bincode::deserialize_from::<_, Vec>(read) else { + let (_, status) = match uapi::waitpid(pid, 0).to_os_error() { + Ok(r) => r, + Err(e) => { + fatal!( + "Could not retrieve exit status of portal ({pid}): {}", + ErrorFmt(e), + ); + } + }; + exit(WEXITSTATUS(status)); + }; + if freestanding { + let e = Command::new("tail") + .arg("-f") + .arg("-n") + .arg("+1") + .arg(OsStr::from_bytes(&log_file)) + .exec(); + fatal!("Could not exec `tail`: {}", ErrorFmt(e)); + } + exit(0); +} + +fn run2(logger: Arc, path_sink: OwnedFd) { + let eng = AsyncEngine::new(); + let ring = match IoUring::new(&eng, 32) { + Ok(r) => r, + Err(e) => { + fatal!("Could not create an IO-uring: {}", ErrorFmt(e)); + } + }; + let _f = eng.spawn( + "portal", + run_async(eng.clone(), ring.clone(), logger, path_sink), + ); + if let Err(e) = ring.run() { + fatal!("The IO-uring returned an error: {}", ErrorFmt(e)); + } +} + +async fn run_async( + eng: Rc, + ring: Rc, + logger: Arc, + path_sink: OwnedFd, +) { + let (_rtl_future, rtl) = RunToplevel::install(&eng); + let dbus = Dbus::new(&eng, &ring, &rtl); + let dbus = init_dbus_session(&dbus, logger, path_sink).await; + let xrd = match xrd() { + Some(xrd) => xrd, + _ => { + fatal!("XDG_RUNTIME_DIR is not set"); + } + }; + let wheel = match Wheel::new(&eng, &ring) { + Ok(w) => w, + Err(e) => { + fatal!("Could not create a timer wheel: {}", ErrorFmt(e)); + } + }; + let pw_con = match PwConHolder::new(&eng, &ring).await { + Ok(p) => Some(p), + Err(e) => { + log::error!("Could not connect to pipewire: {}", ErrorFmt(e)); + None + } + }; + let eventfd_cache = EventfdCache::new(&ring, &eng); + let state = Rc::new(PortalState { + xrd, + ring, + eventfd_cache, + eng, + wheel, + displays: Default::default(), + dbus, + sessions: Default::default(), + next_id: NumCell::new(1), + render_ctxs: Default::default(), + dma_buf_ids: Default::default(), + pw_con: pw_con.as_ref().map(|c| c.con.clone()), + color_manager: ColorManager::new(), + }); + if let Some(pw_con) = &pw_con { + pw_con.con.owner.set(Some(state.clone())); + } + let _root = { + let obj = state + .dbus + .add_object("/org/freedesktop/portal/desktop") + .unwrap(); + if let Some(pw_con) = &pw_con { + add_screencast_dbus_members(&state, &pw_con.con, &obj); + } + add_remote_desktop_dbus_members(&state, &obj); + obj + }; + watch_displays(state.clone()).await; +} + +const UNIQUE_NAME: &str = "org.freedesktop.impl.portal.desktop.jay"; + +async fn init_dbus_session(dbus: &Dbus, logger: Arc, path_sink: OwnedFd) -> Rc { + let session = match dbus.session().await { + Ok(s) => s, + Err(e) => { + fatal!("Could not connect to dbus session daemon: {}", ErrorFmt(e)); + } + }; + let rv = session + .call_async( + BUS_DEST, + BUS_PATH, + org::freedesktop::dbus::RequestName { + name: UNIQUE_NAME.into(), + flags: DBUS_NAME_FLAG_DO_NOT_QUEUE, + }, + ) + .await; + match rv { + Ok(r) if r.get().rv == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER => { + log::info!("Acquired unique name {}", UNIQUE_NAME); + let log_file = logger.redirect("portal"); + log::info!("version = {VERSION}"); + let sink = BufWriter::new(path_sink); + if let Err(e) = bincode::serialize_into(sink, log_file.as_bytes()) { + log::error!("Could not send log file to parent: {}", ErrorFmt(e)); + } + if let Err(e) = uapi::setsid().to_os_error() { + log::error!("setsid failed: {}", ErrorFmt(e)); + } + log::info!("pid = {}", getpid()); + set_process_name("jay portal"); + session + } + Ok(_) => { + log::info!("Portal is already running"); + exit(0); + } + Err(e) => { + fatal!( + "Could not communicate with the session bus: {}", + ErrorFmt(e) + ); + } + } +} + +struct PortalState { + xrd: String, + ring: Rc, + eventfd_cache: Rc, + eng: Rc, + wheel: Rc, + displays: CopyHashMap>, + dbus: Rc, + sessions: CopyHashMap>, + next_id: NumCell, + render_ctxs: CopyHashMap>, + dma_buf_ids: Rc, + pw_con: Option>, + color_manager: Rc, +} + +impl PortalState { + pub fn id>(&self) -> T { + T::from(self.next_id.fetch_add(1)) + } +} + +impl PwConOwner for PortalState { + fn killed(&self) { + fatal!("The pipewire connection has been closed"); + } +} diff --git a/src/portal/ptl_display.rs b/src/portal/ptl_display.rs new file mode 100644 index 00000000..f3c30db4 --- /dev/null +++ b/src/portal/ptl_display.rs @@ -0,0 +1,562 @@ +use { + crate::{ + gfx_api::{GfxApi, GfxFormat, cross_intersect_formats}, + gfx_apis::create_gfx_context, + globals::GlobalName, + ifs::wl_seat::POINTER, + object::Version, + portal::{ + PortalState, + ptl_render_ctx::{PortalRenderCtx, PortalServerRenderCtx}, + ptl_session::PortalSession, + ptr_gui::WindowData, + }, + utils::{ + bitflags::BitflagsExt, + clonecell::CloneCell, + copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, + hash_map_ext::HashMapExt, + opaque::{Opaque, opaque}, + oserror::OsErrorExt, + }, + video::drm::Drm, + wire::{ + JayCompositor, WlCompositor, WlOutput, WlSeat, WlSurfaceId, WpFractionalScaleManagerV1, + WpViewporter, ZwlrLayerShellV1, ZwpLinuxDmabufV1, wl_pointer, + }, + wl_usr::{ + UsrCon, UsrConOwner, + usr_ifs::{ + usr_jay_compositor::UsrJayCompositor, + usr_jay_output::{UsrJayOutput, UsrJayOutputOwner}, + usr_jay_pointer::UsrJayPointer, + usr_jay_render_ctx::UsrJayRenderCtxOwner, + usr_jay_workspace::{UsrJayWorkspace, UsrJayWorkspaceOwner}, + usr_jay_workspace_watcher::{UsrJayWorkspaceWatcher, UsrJayWorkspaceWatcherOwner}, + usr_linux_dmabuf::UsrLinuxDmabuf, + usr_wl_compositor::UsrWlCompositor, + usr_wl_output::{UsrWlOutput, UsrWlOutputOwner}, + usr_wl_pointer::{UsrWlPointer, UsrWlPointerOwner}, + usr_wl_registry::{UsrWlRegistry, UsrWlRegistryOwner}, + usr_wl_seat::{UsrWlSeat, UsrWlSeatOwner}, + usr_wlr_layer_shell::UsrWlrLayerShell, + usr_wp_fractional_scale_manager::UsrWpFractionalScaleManager, + usr_wp_viewporter::UsrWpViewporter, + }, + }, + }, + ahash::AHashMap, + std::{ + cell::{Cell, RefCell}, + ops::Deref, + os::unix::ffi::OsStrExt, + rc::Rc, + str::FromStr, + }, + uapi::{AsUstr, OwnedFd, c}, +}; + +struct PortalDisplayPrelude { + con: Rc, + state: Rc, + registry: Rc, + globals: RefCell>>, +} + +shared_ids!(PortalDisplayId); +pub struct PortalDisplay { + pub id: PortalDisplayId, + pub unique_id: Opaque, + pub con: Rc, + pub(super) state: Rc, + registry: Rc, + _workspace_watcher: Rc, + pub dmabuf: CloneCell>>, + + pub jc: Rc, + pub ls: Rc, + pub comp: Rc, + pub fsm: Rc, + pub vp: Rc, + pub render_ctx: CloneCell>>, + + pub outputs: CopyHashMap>, + pub seats: CopyHashMap>, + pub workspaces: CopyHashMap>, + + pub windows: CopyHashMap>, + pub sessions: CopyHashMap>, +} + +pub struct PortalOutput { + pub global_id: GlobalName, + pub dpy: Rc, + pub wl: Rc, + pub jay: Rc, +} + +pub struct PortalSeat { + pub global_id: GlobalName, + pub dpy: Rc, + pub wl: Rc, + pub jay_pointer: Rc, + pub pointer: CloneCell>>, + pub name: RefCell, + pub capabilities: Cell, + pub pointer_focus: CloneCell>>, +} + +impl UsrWlSeatOwner for PortalSeat { + fn name(&self, name: &str) { + *self.name.borrow_mut() = name.to_string(); + } + + fn capabilities(self: Rc, value: u32) { + let old = self.capabilities.replace(value); + if old.contains(POINTER) != value.contains(POINTER) { + if old.contains(POINTER) { + if let Some(pointer) = self.pointer.take() { + pointer.con.remove_obj(pointer.deref()); + } + } else { + let pointer = self.wl.get_pointer(); + pointer.owner.set(Some(self.clone())); + self.pointer.set(Some(pointer)); + } + } + } +} + +impl UsrWlPointerOwner for PortalSeat { + fn enter(self: Rc, ev: &wl_pointer::Enter) { + if let Some(window) = self.dpy.windows.get(&ev.surface) { + self.pointer_focus.set(Some(window.clone())); + window.motion(&self, ev.surface_x, ev.surface_y, true); + } + } + + fn leave(self: Rc, _ev: &wl_pointer::Leave) { + self.pointer_focus.take(); + } + + fn motion(self: Rc, ev: &wl_pointer::Motion) { + if let Some(window) = self.pointer_focus.get() { + window.motion(&self, ev.surface_x, ev.surface_y, false); + } + } + + fn button(self: Rc, ev: &wl_pointer::Button) { + if let Some(window) = self.pointer_focus.get() { + window.button(&self, ev.button, ev.state); + } + } +} + +impl UsrWlRegistryOwner for PortalDisplayPrelude { + fn global(self: Rc, name: GlobalName, interface: &str, version: u32) { + self.globals + .borrow_mut() + .entry(interface.to_string()) + .or_default() + .push((name, version)); + } +} + +impl UsrJayRenderCtxOwner for PortalDisplay { + fn no_device(&self) { + self.render_ctx.take(); + } + + fn device(&self, fd: Rc, server_formats: Option>) { + self.render_ctx.take(); + let drm = match Drm::open_existing(fd) { + Ok(d) => d, + Err(e) => { + log::error!("Could not open the drm device: {}", ErrorFmt(e)); + return; + } + }; + let dev_id = drm.dev(); + let mut render_ctx = None; + if let Some(ctx) = self.state.render_ctxs.get(&dev_id) + && let Some(ctx) = ctx.upgrade() + { + render_ctx = Some(ctx); + } + if render_ctx.is_none() { + let ctx = match create_gfx_context( + &self.state.eng, + &self.state.ring, + &self.state.eventfd_cache, + &drm, + GfxApi::OpenGl, + None, + ) { + Ok(c) => c, + Err(e) => { + log::error!( + "Could not create render context from drm device: {}", + ErrorFmt(e) + ); + return; + } + }; + let ctx = Rc::new(PortalRenderCtx { + _dev_id: dev_id, + ctx, + }); + self.state.render_ctxs.set(dev_id, Rc::downgrade(&ctx)); + render_ctx = Some(ctx); + } + if let Some(ctx) = render_ctx { + let client_formats = ctx.ctx.formats(); + let usable_formats = match &server_formats { + None => client_formats.clone(), + Some(server_formats) => { + Rc::new(cross_intersect_formats(client_formats, server_formats)) + } + }; + self.render_ctx.set(Some(Rc::new(PortalServerRenderCtx { + ctx, + usable_formats, + server_formats, + }))); + } + } +} + +impl UsrConOwner for PortalDisplay { + fn killed(&self) { + log::info!("Removing display {}", self.id); + for sc in self.sessions.lock().drain_values() { + sc.kill(); + } + self.windows.clear(); + self.state.displays.remove(&self.id); + } +} + +impl UsrWlRegistryOwner for PortalDisplay { + fn global(self: Rc, name: GlobalName, interface: &str, version: u32) { + if interface == WlOutput.name() { + add_output(&self, name, version); + } else if interface == WlSeat.name() { + add_seat(&self, name, version); + } else if interface == ZwpLinuxDmabufV1.name() { + let ls = Rc::new(UsrLinuxDmabuf { + id: self.con.id(), + con: self.con.clone(), + owner: Default::default(), + version: Version(version.min(5)), + }); + self.con.add_object(ls.clone()); + self.registry.bind(name, ls.deref()); + self.dmabuf.set(Some(ls)); + } + } +} + +impl UsrJayWorkspaceWatcherOwner for PortalDisplay { + fn new(self: Rc, ev: Rc, linear_id: u32) { + ev.owner.set(Some(self.clone())); + self.workspaces.set(linear_id, ev); + } +} + +impl UsrJayWorkspaceOwner for PortalDisplay { + fn destroyed(&self, ws: &UsrJayWorkspace) { + self.workspaces.remove(&ws.linear_id.get()); + self.con.remove_obj(ws); + } +} + +impl UsrJayOutputOwner for PortalOutput { + fn destroyed(&self) { + log::info!( + "Display {}: Output {} removed", + self.dpy.con.server_id, + self.global_id, + ); + self.dpy.outputs.remove(&self.global_id); + self.dpy.con.remove_obj(self.wl.deref()); + self.dpy.con.remove_obj(self.jay.deref()); + } +} + +impl UsrWlOutputOwner for PortalOutput {} + +async fn maybe_add_display(state: &Rc, name: &str) { + let tail = match name.strip_prefix("wayland-") { + Some(t) => t, + _ => return, + }; + let head = match tail.strip_suffix(".jay") { + Some(h) => h, + _ => return, + }; + let num = match u32::from_str(head) { + Ok(n) => n, + _ => return, + }; + let path = format!("{}/{}", state.xrd, name); + let con = match UsrCon::new( + &state.ring, + &state.wheel, + &state.eng, + &state.dma_buf_ids, + &path, + num, + ) + .await + { + Ok(c) => c, + Err(e) => { + log::error!( + "Could not connect to wayland display {}: {}", + name, + ErrorFmt(e) + ); + return; + } + }; + let registry = con.get_registry(); + let dpy = Rc::new(PortalDisplayPrelude { + con: con.clone(), + state: state.clone(), + registry, + globals: Default::default(), + }); + dpy.registry.owner.set(Some(dpy.clone())); + con.sync(move || { + finish_display_connect(dpy); + }); + log::info!("Connected to wayland display {num}: {name}"); +} + +fn finish_display_connect(dpy: Rc) { + let mut jc_opt = None; + let mut ls_opt = None; + let mut fsm_opt = None; + let mut comp_opt = None; + let mut vp_opt = None; + let mut dmabuf_opt = None; + let mut outputs = vec![]; + let mut seats = vec![]; + for (interface, instances) in dpy.globals.borrow_mut().deref() { + for &(name, version) in instances { + if interface == JayCompositor.name() { + let jc = Rc::new(UsrJayCompositor { + id: dpy.con.id(), + con: dpy.con.clone(), + owner: Default::default(), + caps: Default::default(), + version: Version(version.min(12)), + }); + dpy.con.add_object(jc.clone()); + dpy.registry.bind(name, jc.deref()); + jc_opt = Some(jc); + } else if interface == WpFractionalScaleManagerV1.name() { + let ls = Rc::new(UsrWpFractionalScaleManager { + id: dpy.con.id(), + con: dpy.con.clone(), + version: Version(version.min(1)), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.bind(name, ls.deref()); + fsm_opt = Some(ls); + } else if interface == ZwlrLayerShellV1.name() { + let ls = Rc::new(UsrWlrLayerShell { + id: dpy.con.id(), + con: dpy.con.clone(), + version: Version(version.min(5)), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.bind(name, ls.deref()); + ls_opt = Some(ls); + } else if interface == WpViewporter.name() { + let ls = Rc::new(UsrWpViewporter { + id: dpy.con.id(), + con: dpy.con.clone(), + version: Version(version.min(1)), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.bind(name, ls.deref()); + vp_opt = Some(ls); + } else if interface == WlCompositor.name() { + let ls = Rc::new(UsrWlCompositor { + id: dpy.con.id(), + con: dpy.con.clone(), + version: Version(version.min(6)), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.bind(name, ls.deref()); + comp_opt = Some(ls); + } else if interface == ZwpLinuxDmabufV1.name() { + let ls = Rc::new(UsrLinuxDmabuf { + id: dpy.con.id(), + con: dpy.con.clone(), + owner: Default::default(), + version: Version(version.min(5)), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.bind(name, ls.deref()); + dmabuf_opt = Some(ls); + } else if interface == WlOutput.name() { + outputs.push((name, version)); + } else if interface == WlSeat.name() { + seats.push((name, version)); + } + } + } + macro_rules! get { + ($opt:expr, $ty:expr) => { + match $opt { + Some(c) => c, + _ => { + log::error!("Compositor did not advertise a {}", $ty.name()); + dpy.con.kill(); + return; + } + } + }; + } + let jc = get!(jc_opt, JayCompositor); + let ls = get!(ls_opt, ZwlrLayerShellV1); + let comp = get!(comp_opt, WlCompositor); + let fsm = get!(fsm_opt, WpFractionalScaleManagerV1); + let vp = get!(vp_opt, WpViewporter); + let ww = jc.watch_workspaces(); + + let dpy = Rc::new(PortalDisplay { + id: dpy.state.id(), + unique_id: opaque(), + con: dpy.con.clone(), + state: dpy.state.clone(), + registry: dpy.registry.clone(), + _workspace_watcher: ww.clone(), + dmabuf: CloneCell::new(dmabuf_opt), + jc, + outputs: Default::default(), + render_ctx: Default::default(), + seats: Default::default(), + ls, + comp, + fsm, + vp, + windows: Default::default(), + sessions: Default::default(), + workspaces: Default::default(), + }); + + dpy.state.displays.set(dpy.id, dpy.clone()); + dpy.con.owner.set(Some(dpy.clone())); + dpy.registry.owner.set(Some(dpy.clone())); + ww.owner.set(Some(dpy.clone())); + + let jrc = dpy.jc.get_render_context(); + jrc.owner.set(Some(dpy.clone())); + + for (name, version) in outputs { + add_output(&dpy, name, version); + } + for (name, version) in seats { + add_seat(&dpy, name, version); + } + log::info!("Display {} initialized", dpy.id); +} + +fn add_seat(dpy: &Rc, name: GlobalName, version: u32) { + let wl = Rc::new(UsrWlSeat { + id: dpy.con.id(), + con: dpy.con.clone(), + owner: Default::default(), + version: Version(version.min(9)), + }); + dpy.con.add_object(wl.clone()); + dpy.registry.bind(name, wl.deref()); + let jay_pointer = dpy.jc.get_pointer(&wl); + let js = Rc::new(PortalSeat { + global_id: name, + dpy: dpy.clone(), + wl, + jay_pointer, + pointer: Default::default(), + name: RefCell::new("".to_string()), + capabilities: Cell::new(0), + pointer_focus: Default::default(), + }); + js.wl.owner.set(Some(js.clone())); + dpy.seats.set(name, js); +} + +fn add_output(dpy: &Rc, name: GlobalName, version: u32) { + let wl = Rc::new(UsrWlOutput { + id: dpy.con.id(), + con: dpy.con.clone(), + owner: Default::default(), + version: Version(version.min(4)), + name: Default::default(), + }); + dpy.con.add_object(wl.clone()); + dpy.registry.bind(name, wl.deref()); + let jo = dpy.jc.get_output(&wl); + let po = Rc::new(PortalOutput { + global_id: name, + dpy: dpy.clone(), + wl: wl.clone(), + jay: jo.clone(), + }); + po.wl.owner.set(Some(po.clone())); + po.jay.owner.set(Some(po.clone())); + dpy.outputs.set(name, po); +} + +pub(super) async fn watch_displays(state: Rc) { + let inotify = Rc::new(uapi::inotify_init1(c::IN_CLOEXEC).unwrap()); + if let Err(e) = + uapi::inotify_add_watch(inotify.raw(), state.xrd.as_str(), c::IN_CREATE).to_os_error() + { + log::error!("Cannot watch directory `{}`: {}", state.xrd, ErrorFmt(e)); + return; + } + let rd = match std::fs::read_dir(&state.xrd) { + Ok(rd) => rd, + Err(e) => { + log::error!("Cannot enumerate `{}`: {}", state.xrd, ErrorFmt(e)); + return; + } + }; + for entry in rd { + let entry = match entry { + Ok(e) => e, + Err(e) => { + log::error!("Cannot enumerate `{}`: {}", state.xrd, ErrorFmt(e)); + return; + } + }; + if let Ok(s) = std::str::from_utf8(entry.file_name().as_bytes()) { + maybe_add_display(&state, s).await; + } + } + let mut buf = vec![0u8; 4096]; + loop { + if let Err(e) = state.ring.readable(&inotify).await { + log::error!("Cannot wait for `{}` to change: {}", state.xrd, ErrorFmt(e)); + } + let events = match uapi::inotify_read(inotify.raw(), &mut buf[..]) { + Ok(s) => s, + Err(e) => { + log::error!("Could not read from inotify fd: {}", ErrorFmt(e)); + return; + } + }; + for event in events { + if event.mask.contains(c::IN_CREATE) + && let Ok(s) = std::str::from_utf8(event.name().as_ustr().as_bytes()) + { + maybe_add_display(&state, s).await; + } + } + } +} diff --git a/src/portal/ptl_remote_desktop.rs b/src/portal/ptl_remote_desktop.rs new file mode 100644 index 00000000..0cb465d9 --- /dev/null +++ b/src/portal/ptl_remote_desktop.rs @@ -0,0 +1,328 @@ +mod remote_desktop_gui; + +use { + crate::{ + dbus::{DbusObject, PendingReply, prelude::Variant}, + ifs::jay_compositor::CREATE_EI_SESSION_SINCE, + portal::{ + PORTAL_SUCCESS, PortalState, + ptl_display::{PortalDisplay, PortalDisplayId}, + ptl_remote_desktop::remote_desktop_gui::SelectionGui, + ptl_screencast::ScreencastPhase, + ptl_session::{PortalSession, PortalSessionReply}, + }, + utils::{ + clonecell::{CloneCell, UnsafeCellCloneSafe}, + copyhashmap::CopyHashMap, + }, + wire_dbus::{ + org, + org::freedesktop::impl_::portal::{ + remote_desktop::{ + ConnectToEIS, ConnectToEISReply, CreateSession, CreateSessionReply, + SelectDevices, SelectDevicesReply, Start, StartReply, + }, + session::CloseReply as SessionCloseReply, + }, + }, + wl_usr::usr_ifs::usr_jay_ei_session::{UsrJayEiSession, UsrJayEiSessionOwner}, + }, + std::{cell::Cell, ops::Deref, rc::Rc}, + uapi::OwnedFd, +}; + +#[derive(Clone)] +pub enum RemoteDesktopPhase { + Init, + DevicesSelected, + Selecting(Rc), + Starting(Rc), + Started(Rc), + Terminated, +} + +unsafe impl UnsafeCellCloneSafe for RemoteDesktopPhase {} + +pub struct SelectingDisplay { + pub session: Rc, + pub request_obj: Rc, + pub guis: CopyHashMap>, +} + +pub struct StartingRemoteDesktop { + pub session: Rc, + pub request_obj: Rc, + pub dpy: Rc, + pub ei_session: Rc, +} + +pub struct StartedRemoteDesktop { + pub session: Rc, + pub dpy: Rc, + pub ei_session: Rc, + pub ei_fd: Cell>>, +} + +bitflags! { + DeviceTypes: u32; + + KEYBOARD = 1, + POINTER = 2, + TOUCHSCREEN = 4, +} + +impl UsrJayEiSessionOwner for StartingRemoteDesktop { + fn created(&self, fd: &Rc) { + let started = Rc::new(StartedRemoteDesktop { + session: self.session.clone(), + dpy: self.dpy.clone(), + ei_session: self.ei_session.clone(), + ei_fd: Cell::new(Some(fd.clone())), + }); + self.session + .rd_phase + .set(RemoteDesktopPhase::Started(started.clone())); + started.ei_session.owner.set(Some(started.clone())); + if let ScreencastPhase::SourcesSelected(s) = self.session.sc_phase.get() { + self.session.screencast_restore( + &self.request_obj, + s.restore_data.take(), + Some(self.dpy.clone()), + ); + } else { + self.session.send_start_reply(None, None, None); + } + } + + fn failed(&self, reason: &str) { + log::error!("Could not create session: {}", reason); + self.session.reply_err(reason); + self.session.kill(); + } +} + +impl SelectingDisplay { + pub fn starting(&self, dpy: &Rc) { + let builder = dpy.jc.create_ei_session(); + builder.set_app_id(&self.session.app); + let ei_session = builder.commit(); + let starting = Rc::new(StartingRemoteDesktop { + session: self.session.clone(), + request_obj: self.request_obj.clone(), + dpy: dpy.clone(), + ei_session, + }); + self.session + .rd_phase + .set(RemoteDesktopPhase::Starting(starting.clone())); + starting.ei_session.owner.set(Some(starting.clone())); + dpy.sessions.set( + self.session.session_obj.path().to_owned(), + self.session.clone(), + ); + } +} + +impl PortalSession { + fn dbus_select_devices( + self: &Rc, + _req: SelectDevices, + reply: PendingReply>, + ) { + match self.rd_phase.get() { + RemoteDesktopPhase::Init => {} + _ => { + self.kill(); + reply.err("Devices have already been selected"); + return; + } + } + self.rd_phase.set(RemoteDesktopPhase::DevicesSelected); + reply.ok(&SelectDevicesReply { + response: PORTAL_SUCCESS, + results: Default::default(), + }); + } + + fn dbus_start_remote_desktop( + self: &Rc, + req: Start<'_>, + reply: PendingReply>, + ) { + match self.rd_phase.get() { + RemoteDesktopPhase::DevicesSelected => {} + _ => { + self.kill(); + reply.err("Session is not in the correct phase for starting"); + return; + } + } + let request_obj = match self.state.dbus.add_object(req.handle.to_string()) { + Ok(r) => r, + Err(_) => { + self.kill(); + reply.err("Request handle is not unique"); + return; + } + }; + { + use org::freedesktop::impl_::portal::request::*; + request_obj.add_method::({ + let slf = self.clone(); + move |_, pr| { + slf.kill(); + pr.ok(&CloseReply); + } + }); + } + let guis = CopyHashMap::new(); + for dpy in self.state.displays.lock().values() { + if dpy.outputs.len() > 0 && dpy.jc.version >= CREATE_EI_SESSION_SINCE { + guis.set(dpy.id, SelectionGui::new(self, dpy)); + } + } + if guis.is_empty() { + self.kill(); + reply.err("There are no running displays"); + return; + } + self.start_reply + .set(Some(PortalSessionReply::RemoteDesktop(reply))); + self.rd_phase + .set(RemoteDesktopPhase::Selecting(Rc::new(SelectingDisplay { + session: self.clone(), + request_obj: Rc::new(request_obj), + guis, + }))); + } + + fn dbus_connect_to_eis( + self: &Rc, + _req: ConnectToEIS, + reply: PendingReply, + ) { + let RemoteDesktopPhase::Started(started) = self.rd_phase.get() else { + self.kill(); + reply.err("Sources have already been selected"); + return; + }; + let Some(fd) = started.ei_fd.take() else { + self.kill(); + reply.err("EI file descriptor has already been consumed"); + return; + }; + reply.ok(&ConnectToEISReply { fd }); + } +} + +impl UsrJayEiSessionOwner for StartedRemoteDesktop { + fn destroyed(&self) { + self.session.kill(); + } +} + +pub(super) fn add_remote_desktop_dbus_members(state_: &Rc, object: &DbusObject) { + use org::freedesktop::impl_::portal::remote_desktop::*; + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_create_session(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_select_devices(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_start(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_connect_to_eis(&state, req, pr); + }); + object.set_property::(Variant::U32(DeviceTypes::all().0)); + object.set_property::(Variant::U32(2)); +} + +fn dbus_create_session( + state: &Rc, + req: CreateSession, + reply: PendingReply>, +) { + log::info!("Create remote desktop session {:#?}", req); + if state.sessions.contains(req.session_handle.0.deref()) { + reply.err("Session already exists"); + return; + } + let obj = match state.dbus.add_object(req.session_handle.0.to_string()) { + Ok(obj) => obj, + Err(_) => { + reply.err("Session path is not unique"); + return; + } + }; + let session = Rc::new(PortalSession { + _id: state.id(), + state: state.clone(), + pw_con: state.pw_con.clone(), + app: req.app_id.to_string(), + session_obj: obj, + sc_phase: CloneCell::new(ScreencastPhase::Init), + rd_phase: CloneCell::new(RemoteDesktopPhase::Init), + start_reply: Default::default(), + }); + { + use org::freedesktop::impl_::portal::session::*; + let ses = session.clone(); + session.session_obj.add_method::(move |_, pr| { + ses.kill(); + pr.ok(&SessionCloseReply); + }); + session.session_obj.set_property::(Variant::U32(2)); + } + state + .sessions + .set(req.session_handle.0.to_string(), session); + reply.ok(&CreateSessionReply { + response: PORTAL_SUCCESS, + results: Default::default(), + }); +} + +fn dbus_select_devices( + state: &Rc, + req: SelectDevices, + reply: PendingReply>, +) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_select_devices(req, reply); + } +} + +fn dbus_start(state: &Rc, req: Start, reply: PendingReply>) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_start_remote_desktop(req, reply); + } +} + +fn dbus_connect_to_eis( + state: &Rc, + req: ConnectToEIS, + reply: PendingReply, +) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_connect_to_eis(req, reply); + } +} + +fn get_session( + state: &Rc, + reply: &PendingReply, + handle: &str, +) -> Option> { + let res = state.sessions.get(handle); + if res.is_none() { + let msg = format!("Remote desktop session `{}` does not exist", handle); + reply.err(&msg); + } + res +} diff --git a/src/portal/ptl_remote_desktop/remote_desktop_gui.rs b/src/portal/ptl_remote_desktop/remote_desktop_gui.rs new file mode 100644 index 00000000..f9b52189 --- /dev/null +++ b/src/portal/ptl_remote_desktop/remote_desktop_gui.rs @@ -0,0 +1,160 @@ +use { + crate::{ + globals::GlobalName, + ifs::wl_seat::{BTN_LEFT, wl_pointer::PRESSED}, + portal::{ + ptl_display::{PortalDisplay, PortalOutput, PortalSeat}, + ptl_remote_desktop::{PortalSession, RemoteDesktopPhase}, + ptr_gui::{ + Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow, + OverlayWindowOwner, + }, + }, + theme::Color, + utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt}, + }, + std::rc::Rc, +}; + +const H_MARGIN: f32 = 30.0; +const V_MARGIN: f32 = 20.0; + +pub struct SelectionGui { + remote_desktop_session: Rc, + dpy: Rc, + surfaces: CopyHashMap>, +} + +pub struct SelectionGuiSurface { + gui: Rc, + output: Rc, + overlay: Rc, +} + +struct StaticButton { + surface: Rc, + role: ButtonRole, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum ButtonRole { + Accept, + Reject, +} + +impl SelectionGui { + pub fn kill(&self, upwards: bool) { + for surface in self.surfaces.lock().drain_values() { + surface.overlay.data.kill(false); + } + if let RemoteDesktopPhase::Selecting(s) = self.remote_desktop_session.rd_phase.get() { + s.guis.remove(&self.dpy.id); + if upwards && s.guis.is_empty() { + self.remote_desktop_session.kill(); + } + } + } +} + +fn create_accept_gui(surface: &Rc) -> Rc { + let app = &surface.gui.remote_desktop_session.app; + let text = if app.is_empty() { + format!("An application wants to generate/monitor input") + } else { + format!("`{}` wants to generate/monitor input", app) + }; + let label = Rc::new(Label::default()); + *label.text.borrow_mut() = text; + let accept_button = static_button(surface, ButtonRole::Accept, "Allow"); + let reject_button = static_button(surface, ButtonRole::Reject, "Reject"); + for button in [&accept_button, &reject_button] { + button.border_color.set(Color::from_gray_srgb(100)); + button.border.set(2.0); + button.padding.set(5.0); + } + accept_button.bg_color.set(Color::from_srgb(170, 200, 170)); + accept_button + .bg_hover_color + .set(Color::from_srgb(170, 255, 170)); + reject_button.bg_color.set(Color::from_srgb(200, 170, 170)); + reject_button + .bg_hover_color + .set(Color::from_srgb(255, 170, 170)); + let flow = Rc::new(Flow::default()); + flow.orientation.set(Orientation::Vertical); + flow.cross_align.set(Align::Center); + flow.in_margin.set(V_MARGIN); + flow.cross_margin.set(H_MARGIN); + *flow.elements.borrow_mut() = vec![label, accept_button, reject_button]; + flow +} + +impl OverlayWindowOwner for SelectionGuiSurface { + fn kill(&self, upwards: bool) { + self.gui.dpy.windows.remove(&self.overlay.data.surface.id); + self.gui.surfaces.remove(&self.output.global_id); + if upwards && self.gui.surfaces.is_empty() { + self.gui.kill(true); + } + } +} + +impl SelectionGui { + pub fn new(ss: &Rc, dpy: &Rc) -> Rc { + let gui = Rc::new(SelectionGui { + remote_desktop_session: ss.clone(), + dpy: dpy.clone(), + surfaces: Default::default(), + }); + for output in dpy.outputs.lock().values() { + let sgs = Rc::new(SelectionGuiSurface { + gui: gui.clone(), + output: output.clone(), + overlay: OverlayWindow::new(output), + }); + let element = create_accept_gui(&sgs); + sgs.overlay.data.content.set(Some(element)); + gui.dpy + .windows + .set(sgs.overlay.data.surface.id, sgs.overlay.data.clone()); + gui.surfaces.set(output.global_id, sgs); + } + gui + } +} + +impl ButtonOwner for StaticButton { + fn button(&self, _seat: &PortalSeat, button: u32, state: u32) { + if button != BTN_LEFT || state != PRESSED { + return; + } + match self.role { + ButtonRole::Accept => { + log::info!("User has accepted the request"); + let selecting = match self.surface.gui.remote_desktop_session.rd_phase.get() { + RemoteDesktopPhase::Selecting(selecting) => selecting, + _ => return, + }; + for gui in selecting.guis.lock().drain_values() { + gui.kill(false); + } + selecting.starting(&self.surface.output.dpy); + } + ButtonRole::Reject => { + log::info!("User has rejected the remote desktop request"); + self.surface.gui.remote_desktop_session.kill(); + } + } + } +} + +fn static_button(surface: &Rc, role: ButtonRole, text: &str) -> Rc