diff --git a/Cargo.lock b/Cargo.lock index df95cf3..59e7f27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1759,6 +1759,7 @@ version = "0.0.1" dependencies = [ "anyhow", "bitflags 2.5.0", + "bytemuck", "chrono", "clap", "cliclack", diff --git a/Cargo.toml b/Cargo.toml index f4c5626..60826f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ nix = { version = "0.28.0", features = ["user", "resource"] } pinnacle-api-defs = { workspace = true } dircpy = "0.3.16" chrono = "0.4.37" +bytemuck = "1.15.0" [dependencies.smithay] git = "https://github.com/Smithay/smithay" diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 87097f9..6115a6b 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later mod drm_util; +mod gamma; use std::{ collections::{HashMap, HashSet}, @@ -57,7 +58,7 @@ use smithay::{ reexports::{ ash::vk::ExtPhysicalDeviceDrmFn, calloop::{ - self, generic::Generic, EventLoop, Idle, Interest, LoopHandle, PostAction, + self, generic::Generic, Dispatcher, EventLoop, Idle, Interest, LoopHandle, PostAction, RegistrationToken, }, drm::control::{connector, crtc, ModeTypeFlags}, @@ -81,7 +82,7 @@ use smithay::{ }, }; use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; -use tracing::{error, info, trace, warn}; +use tracing::{debug, error, info, trace, warn}; use crate::{ backend::Backend, @@ -89,7 +90,7 @@ use crate::{ output::OutputName, render::{ pointer::PointerElement, pointer_render_elements, take_presentation_feedback, - OutputRenderElements, + OutputRenderElement, }, state::{State, SurfaceDmabufFeedback, WithState}, }; @@ -118,7 +119,7 @@ type UdevRenderFrameResult<'a> = RenderFrameResult< 'a, BufferObject<()>, GbmFramebuffer, - OutputRenderElements, WaylandSurfaceRenderElement>>, + OutputRenderElement, WaylandSurfaceRenderElement>>, >; /// Udev state attached to each [`Output`]. @@ -133,6 +134,7 @@ struct UdevOutputData { // TODO: document desperately pub struct Udev { pub session: LibSeatSession, + udev_dispatcher: Dispatcher<'static, UdevBackend, State>, display_handle: DisplayHandle, pub(super) dmabuf_state: Option<(DmabufState, DmabufGlobal)>, pub(super) primary_gpu: DrmNode, @@ -192,45 +194,52 @@ impl State { /// Does nothing when called on the winit backend. pub fn switch_vt(&mut self, vt: i32) { if let Backend::Udev(udev) = &mut self.backend { - for backend in udev.backends.values_mut() { - for surface in backend.surfaces.values_mut() { - // Clear the overlay planes on tty switch. - // - // On my machine, switching a tty would leave the topmost window on the - // screen. Smithay will render the topmost window on the overlay plane, - // so we clear it here. - let planes = surface.compositor.surface().planes().clone(); - tracing::debug!("Clearing overlay planes"); - for overlay_plane in planes.overlay { - if let Err(err) = surface - .compositor - .surface() - .clear_plane(overlay_plane.handle) - { - warn!("Failed to clear overlay planes: {err}"); - } - } - } + if let Err(err) = udev.session.change_vt(vt) { + error!("Failed to switch to vt {vt}: {err}"); } + // TODO: uncomment this when `RenderFrameResult::blit_frame_result` is fixed for + // | overlay/cursor planes + + // for backend in udev.backends.values_mut() { + // for surface in backend.surfaces.values_mut() { + // // Clear the overlay planes on tty switch. + // // + // // On my machine, switching a tty would leave the topmost window on the + // // screen. Smithay will render the topmost window on the overlay plane, + // // so we clear it here. + // let planes = surface.compositor.surface().planes().clone(); + // tracing::debug!("Clearing overlay planes"); + // for overlay_plane in planes.overlay { + // if let Err(err) = surface + // .compositor + // .surface() + // .clear_plane(overlay_plane.handle) + // { + // warn!("Failed to clear overlay planes: {err}"); + // } + // } + // } + // } + // Wait for the clear to commit before switching - self.schedule( - |state| { - let udev = state.backend.udev(); - !udev - .backends - .values() - .flat_map(|backend| backend.surfaces.values()) - .map(|surface| surface.compositor.surface()) - .any(|drm_surf| drm_surf.commit_pending()) - }, - move |state| { - let udev = state.backend.udev_mut(); - if let Err(err) = udev.session.change_vt(vt) { - error!("Failed to switch to vt {vt}: {err}"); - } - }, - ); + // self.schedule( + // |state| { + // let udev = state.backend.udev(); + // !udev + // .backends + // .values() + // .flat_map(|backend| backend.surfaces.values()) + // .map(|surface| surface.compositor.surface()) + // .any(|drm_surf| drm_surf.commit_pending()) + // }, + // move |state| { + // let udev = state.backend.udev_mut(); + // if let Err(err) = udev.session.change_vt(vt) { + // error!("Failed to switch to vt {vt}: {err}"); + // } + // }, + // ); } } @@ -338,8 +347,43 @@ pub fn setup_udev( // Ok(unsafe { GlesRenderer::with_capabilities(ctx, supported) }?) // }))?; + // Initialize the udev backend + let udev_backend = UdevBackend::new(session.seat())?; + + let udev_dispatcher = + Dispatcher::new( + udev_backend, + move |event, _, state: &mut State| match event { + // GPU connected + UdevEvent::Added { device_id, path } => { + if let Err(err) = DrmNode::from_dev_id(device_id) + .map_err(DeviceAddError::DrmNode) + .and_then(|node| state.device_added(node, &path)) + { + error!("Skipping device {device_id}: {err}"); + } + } + UdevEvent::Changed { device_id } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + state.device_changed(node) + } + } + // GPU disconnected + UdevEvent::Removed { device_id } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + state.device_removed(node) + } + } + }, + ); + + event_loop + .handle() + .register_dispatcher(udev_dispatcher.clone())?; + let data = Udev { display_handle: display.handle(), + udev_dispatcher, dmabuf_state: None, session, primary_gpu, @@ -365,14 +409,20 @@ pub fn setup_udev( config_dir, )?; - // Initialize the udev backend - let udev_backend = UdevBackend::new(state.seat.name())?; + let things = state + .backend + .udev() + .udev_dispatcher + .as_source_ref() + .device_list() + .map(|(id, path)| (id, path.to_path_buf())) + .collect::>(); // Create DrmNodes from already connected GPUs - for (device_id, path) in udev_backend.device_list() { + for (device_id, path) in things { if let Err(err) = DrmNode::from_dev_id(device_id) .map_err(DeviceAddError::DrmNode) - .and_then(|node| state.device_added(node, path)) + .and_then(|node| state.device_added(node, &path)) { error!("Skipping device {device_id}: {err}"); } @@ -380,32 +430,6 @@ pub fn setup_udev( let udev = state.backend.udev_mut(); - event_loop - .handle() - .insert_source(udev_backend, move |event, _, state| match event { - // GPU connected - UdevEvent::Added { device_id, path } => { - if let Err(err) = DrmNode::from_dev_id(device_id) - .map_err(DeviceAddError::DrmNode) - .and_then(|node| state.device_added(node, &path)) - { - error!("Skipping device {device_id}: {err}"); - } - } - UdevEvent::Changed { device_id } => { - if let Ok(node) = DrmNode::from_dev_id(device_id) { - state.device_changed(node) - } - } - // GPU disconnected - UdevEvent::Removed { device_id } => { - if let Ok(node) = DrmNode::from_dev_id(device_id) { - state.device_removed(node) - } - } - }) - .expect("failed to insert udev_backend into event loop"); - // Initialize libinput backend let mut libinput_context = Libinput::new_with_udev::>( udev.session.clone().into(), @@ -431,10 +455,9 @@ pub fn setup_udev( event_loop .handle() .insert_source(notifier, move |event, _, state| { - let udev = state.backend.udev_mut(); - match event { session::Event::PauseSession => { + let udev = state.backend.udev_mut(); libinput_context.suspend(); info!("pausing session"); @@ -445,44 +468,117 @@ pub fn setup_udev( session::Event::ActivateSession => { info!("resuming session"); - if let Err(err) = libinput_context.resume() { - error!("Failed to resume libinput context: {:?}", err); + if libinput_context.resume().is_err() { + error!("Failed to resume libinput context"); } - for backend in udev.backends.values_mut() { - // TODO: this is false because i'm too lazy to remove the code directly - // | below it - backend.drm.activate(false).expect("failed to activate drm"); - for surface in backend.surfaces.values_mut() { - if let Err(err) = surface.compositor.surface().reset_state() { - warn!("Failed to reset drm surface state: {}", err); + + // TODO: All this dance around borrowing is a consequence of the fact that I haven't + // | split the State struct into a main State and a substruct like + // | Niri and cosmic-comp have done + + let (mut device_list, connected_devices, disconnected_devices) = { + let udev = state.backend.udev(); + let device_list = udev + .udev_dispatcher + .as_source_ref() + .device_list() + .flat_map(|(id, path)| { + Some((DrmNode::from_dev_id(id).ok()?, path.to_path_buf())) + }) + .collect::>(); + + let (connected_devices, disconnected_devices) = udev + .backends + .keys() + .copied() + .partition::, _>(|node| device_list.contains_key(node)); + + (device_list, connected_devices, disconnected_devices) + }; + + for node in disconnected_devices { + device_list.remove(&node); + state.device_removed(node); + } + + for node in connected_devices { + device_list.remove(&node); + + // TODO: split off the big State struct to avoid this bs + + { + let udev = state.backend.udev_mut(); + + let Some(backend) = udev.backends.get_mut(&node) else { + unreachable!(); + }; + + if let Err(err) = backend.drm.activate(true) { + error!("Error activating DRM device: {err}"); + } + } + + state.device_changed(node); + + // Apply pending gammas + // + // Also welcome to some really doodoo code + + let udev = state.backend.udev_mut(); + let Some(backend) = udev.backends.get_mut(&node) else { + unreachable!(); + }; + for (crtc, surface) in backend.surfaces.iter_mut() { + match std::mem::take(&mut surface.pending_gamma_change) { + PendingGammaChange::Idle => { + debug!("Restoring from previous gamma"); + if let Err(err) = Udev::set_gamma_internal( + &backend.drm, + crtc, + surface.previous_gamma.clone(), + ) { + warn!("Failed to reset gamma: {err}"); + surface.previous_gamma = None; + } + } + PendingGammaChange::Restore => { + debug!("Restoring to original gamma"); + if let Err(err) = Udev::set_gamma_internal( + &backend.drm, + crtc, + None::<[&[u16]; 3]>, + ) { + warn!("Failed to reset gamma: {err}"); + } + surface.previous_gamma = None; + } + PendingGammaChange::Change(gamma) => { + debug!("Changing to pending gamma"); + match Udev::set_gamma_internal( + &backend.drm, + crtc, + Some([&gamma[0], &gamma[1], &gamma[2]]), + ) { + Ok(()) => { + surface.previous_gamma = Some(gamma); + } + Err(err) => { + warn!("Failed to set pending gamma: {err}"); + surface.previous_gamma = None; + } + } + } } - // reset the buffers after resume to trigger a full redraw - // this is important after a vt switch as the primary plane - // has no content and damage tracking may prevent a redraw - // otherwise - surface.compositor.reset_buffers(); } } - let connectors = udev - .backends - .iter() - .map(|(node, backend)| { - let connectors = backend - .drm_scanner - .crtcs() - .map(|(info, crtc)| (info.clone(), crtc)) - .collect::>(); - (*node, connectors) - }) - .collect::>(); - - for (node, connectors) in connectors { - for (connector, crtc) in connectors { - state.connector_disconnected(node, connector.clone(), crtc); - state.connector_connected(node, connector, crtc); + // Newly connected devices + for (node, path) in device_list.into_iter() { + if let Err(err) = state.device_added(node, &path) { + error!("Error adding device: {err}"); } } + for output in state.space.outputs().cloned().collect::>() { state.schedule_render(&output); } @@ -727,6 +823,20 @@ struct RenderSurface { dmabuf_feedback: Option, render_state: RenderState, screencopy_commit_state: ScreencopyCommitState, + + previous_gamma: Option<[Box<[u16]>; 3]>, + pending_gamma_change: PendingGammaChange, +} + +#[derive(Debug, Clone, Default)] +enum PendingGammaChange { + /// No pending gamma + #[default] + Idle, + /// Restore the original gamma + Restore, + /// Change the gamma + Change([Box<[u16]>; 3]), } #[derive(Default, Debug, Clone, Copy)] @@ -758,7 +868,7 @@ type GbmDrmCompositor = DrmCompositor< fn render_frame<'a>( compositor: &mut GbmDrmCompositor, renderer: &mut UdevRenderer<'a>, - elements: &'a [OutputRenderElements< + elements: &'a [OutputRenderElement< UdevRenderer<'a>, WaylandSurfaceRenderElement>, >], @@ -1006,6 +1116,8 @@ impl State { dmabuf_feedback, render_state: RenderState::Idle, screencopy_commit_state: ScreencopyCommitState::default(), + previous_gamma: None, + pending_gamma_change: PendingGammaChange::Idle, }; device.surfaces.insert(crtc, surface); @@ -1074,6 +1186,7 @@ impl State { }, ); self.space.unmap_output(&output); + self.gamma_control_manager_state.output_removed(&output); } } diff --git a/src/backend/udev/gamma.rs b/src/backend/udev/gamma.rs new file mode 100644 index 0000000..d870cd1 --- /dev/null +++ b/src/backend/udev/gamma.rs @@ -0,0 +1,118 @@ +// Parts ripped out from Niri like that time Omni-man almost ripped out Donald's spine +// Not that I'm Omni-man, of course. + +use anyhow::{ensure, Context}; +use smithay::backend::drm::DrmDevice; +use smithay::reexports::drm::control::{crtc, Device}; +use smithay::{backend::session::Session, output::Output}; + +use crate::backend::udev::{render_surface_for_output, PendingGammaChange}; + +use super::{Udev, UdevOutputData}; + +impl Udev { + pub fn set_gamma(&mut self, output: &Output, gamma: Option<[&[u16]; 3]>) -> anyhow::Result<()> { + if !self.session.is_active() { + render_surface_for_output(output, &mut self.backends) + .context("no render surface for output")? + .pending_gamma_change = match gamma { + Some([r, g, b]) => { + PendingGammaChange::Change([Box::from(r), Box::from(g), Box::from(b)]) + } + None => PendingGammaChange::Restore, + }; + tracing::info!("SET PENDING GAMMA WOO"); + return Ok(()); + } + + let UdevOutputData { device_id, crtc } = output + .user_data() + .get() + .context("no udev output data for output")?; + + let drm_device = &self + .backends + .get(device_id) + .context("no udev backend data for output")? + .drm; + + let ret = Udev::set_gamma_internal(drm_device, crtc, gamma); + + render_surface_for_output(output, &mut self.backends) + .context("no render surface for output")? + .previous_gamma = match ret.is_ok() { + true => gamma.map(|[r, g, b]| [r.into(), g.into(), b.into()]), + false => None, + }; + + ret + } + + pub(super) fn set_gamma_internal( + drm_device: &DrmDevice, + crtc: &crtc::Handle, + gamma: Option<[impl AsRef<[u16]>; 3]>, + ) -> anyhow::Result<()> { + let gamma = gamma + .as_ref() + .map(|[r, g, b]| [r.as_ref(), g.as_ref(), b.as_ref()]); + + let crtc_info = drm_device.get_crtc(*crtc)?; + let gamma_size = crtc_info.gamma_length() as usize; + + ensure!(gamma_size != 0, "setting gamma is not supported"); + + let mut temp_red; + let mut temp_green; + let mut temp_blue; + + let [red, green, blue] = match gamma { + Some([red, green, blue]) => { + ensure!(red.len() == gamma_size, "wrong red gamma size"); + ensure!(green.len() == gamma_size, "wrong green gamma size"); + ensure!(blue.len() == gamma_size, "wrong blue gamma size"); + [red, green, blue] + } + None => { + temp_red = vec![0u16; gamma_size]; + temp_green = vec![0u16; gamma_size]; + temp_blue = vec![0u16; gamma_size]; + + let denom = gamma_size as u64 - 1; + + for i in 0..gamma_size { + let value = (0xFFFF * i as u64 / denom) as u16; + temp_red[i] = value; + temp_green[i] = value; + temp_blue[i] = value; + } + + [ + temp_red.as_slice(), + temp_green.as_slice(), + temp_blue.as_slice(), + ] + } + }; + + drm_device.set_gamma(*crtc, red, green, blue)?; + + Ok(()) + } + + pub fn gamma_size(&self, output: &Output) -> anyhow::Result { + let UdevOutputData { device_id, crtc } = output + .user_data() + .get() + .context("no udev output data for output")?; + + let drm_device = &self + .backends + .get(device_id) + .context("no udev backend data for output")? + .drm; + + let crtc_info = drm_device.get_crtc(*crtc)?; + Ok(crtc_info.gamma_length()) + } +} diff --git a/src/handlers.rs b/src/handlers.rs index 6b6d2ae..4d67afc 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -56,12 +56,16 @@ use smithay::{ }, xwayland::{X11Wm, XWaylandClientData}, }; -use tracing::error; +use tracing::{error, warn}; use crate::{ - delegate_screencopy, + backend::Backend, + delegate_gamma_control, delegate_screencopy, focus::{keyboard::KeyboardFocusTarget, pointer::PointerFocusTarget}, - protocol::screencopy::{Screencopy, ScreencopyHandler}, + protocol::{ + gamma_control::{GammaControlHandler, GammaControlManagerState}, + screencopy::{Screencopy, ScreencopyHandler}, + }, state::{ClientState, State, WithState}, }; @@ -555,3 +559,54 @@ impl ScreencopyHandler for State { } } delegate_screencopy!(State); + +impl GammaControlHandler for State { + fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState { + &mut self.gamma_control_manager_state + } + + fn get_gamma_size(&mut self, output: &Output) -> Option { + let Backend::Udev(udev) = &self.backend else { + return None; + }; + + match udev.gamma_size(output) { + Ok(0) => None, // Setting gamma is not supported + Ok(size) => Some(size), + Err(err) => { + warn!( + "Failed to get gamma size for output {}: {err}", + output.name() + ); + None + } + } + } + + fn set_gamma(&mut self, output: &Output, gammas: [&[u16]; 3]) -> bool { + let Backend::Udev(udev) = &mut self.backend else { + warn!("Setting gamma is not supported on the winit backend"); + return false; + }; + + match udev.set_gamma(output, Some(gammas)) { + Ok(_) => true, + Err(err) => { + warn!("Failed to set gamma for output {}: {err}", output.name()); + false + } + } + } + + fn gamma_control_destroyed(&mut self, output: &Output) { + let Backend::Udev(udev) = &mut self.backend else { + warn!("Resetting gamma is not supported on the winit backend"); + return; + }; + + if let Err(err) = udev.set_gamma(output, None) { + warn!("Failed to set gamma for output {}: {err}", output.name()); + } + } +} +delegate_gamma_control!(State); diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index beb8c83..01ccff9 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -154,6 +154,29 @@ impl XdgShellHandler for State { .unwrap_or_else(|| (0, 0).into()); tracing::debug!(?loc); break; + } else { + let layer_and_op = self.space.outputs().find_map(|op| { + let layer_map = layer_map_for_output(op); + + let ret = layer_map + .layers() + .find(|l| l.wl_surface() == s) + .cloned() + .map(|layer| { + ( + layer_map.layer_geometry(&layer).unwrap_or_default(), + op.clone(), + ) + }); + + ret + }); + + if let Some((layer_geo, op)) = layer_and_op { + let op_loc = self.space.output_geometry(&op).unwrap_or_default().loc; + loc += layer_geo.loc + op_loc; + break; + } } } diff --git a/src/protocol.rs b/src/protocol.rs index ba61889..436a673 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1 +1,2 @@ +pub mod gamma_control; pub mod screencopy; diff --git a/src/protocol/gamma_control.rs b/src/protocol/gamma_control.rs new file mode 100644 index 0000000..645dd9e --- /dev/null +++ b/src/protocol/gamma_control.rs @@ -0,0 +1,295 @@ +use std::{collections::HashMap, fs::File, io::Read}; + +use smithay::{ + output::Output, + reexports::{ + wayland_protocols_wlr::gamma_control::v1::server::{ + zwlr_gamma_control_manager_v1::{self, ZwlrGammaControlManagerV1}, + zwlr_gamma_control_v1::{self, ZwlrGammaControlV1}, + }, + wayland_server::{ + self, backend::ClientId, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, + Resource, + }, + }, +}; +use tracing::warn; + +const VERSION: u32 = 1; + +pub struct GammaControlManagerState { + pub gamma_controls: HashMap, +} + +pub struct GammaControlManagerGlobalData { + filter: Box bool + Send + Sync>, +} + +impl GammaControlManagerState { + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch + + Dispatch + + Dispatch + + GammaControlHandler + + 'static, + F: Fn(&Client) -> bool + Send + Sync + 'static, + { + let global_data = GammaControlManagerGlobalData { + filter: Box::new(filter), + }; + display.create_global::(VERSION, global_data); + Self { + gamma_controls: HashMap::new(), + } + } + + pub fn output_removed(&mut self, output: &Output) { + if let Some(gamma_control) = self.gamma_controls.remove(output) { + gamma_control.failed(); + } + } +} + +pub struct GammaControlState { + gamma_size: u32, +} + +impl GlobalDispatch + for GammaControlManagerState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + GammaControlHandler + + 'static, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: wayland_server::New, + _global_data: &GammaControlManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &GammaControlManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for GammaControlManagerState +where + D: Dispatch + + Dispatch + + GammaControlHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _manager: &ZwlrGammaControlManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + let (id, output) = match request { + zwlr_gamma_control_manager_v1::Request::GetGammaControl { id, output } => (id, output), + zwlr_gamma_control_manager_v1::Request::Destroy => return, + _ => unreachable!(), + }; + + let output = Output::from_resource(&output).expect("no output for resource"); + + match state + .gamma_control_manager_state() + .gamma_controls + .contains_key(&output) + { + true => { + // This wl_output already has exclusive access by another client + let gamma_control_state = GammaControlState { gamma_size: 0 }; + let gamma_control = data_init.init(id, gamma_control_state); + gamma_control.failed(); + } + false => { + let Some(gamma_size) = state.get_gamma_size(&output) else { + let gamma_control = data_init.init(id, GammaControlState { gamma_size: 0 }); + gamma_control.failed(); + return; + }; + + let gamma_control = data_init.init(id, GammaControlState { gamma_size }); + + gamma_control.gamma_size(gamma_size); + state + .gamma_control_manager_state() + .gamma_controls + .insert(output, gamma_control); + } + } + } +} + +pub trait GammaControlHandler { + fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState; + /// A new gamma control was requested on the given output. + /// + /// This should return the length of the gamma on the output, if available. + fn get_gamma_size(&mut self, output: &Output) -> Option; + /// A client requested that the gamma be set on the given output. + /// + /// `gammas` are the gammas for the red, green, and blue channels respectively. + /// + /// Returns whether or not the operation completed successfully. + fn set_gamma(&mut self, output: &Output, gammas: [&[u16]; 3]) -> bool; + /// A client destroyed its gamma control object for the given output. + fn gamma_control_destroyed(&mut self, output: &Output); +} + +#[allow(missing_docs)] +#[macro_export] +macro_rules! delegate_gamma_control { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1: $crate::protocol::gamma_control::GammaControlManagerGlobalData + ] => $crate::protocol::gamma_control::GammaControlManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1: () + ] => $crate::protocol::gamma_control::GammaControlManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::zwlr_gamma_control_v1::ZwlrGammaControlV1: $crate::protocol::gamma_control::GammaControlState + ] => $crate::protocol::gamma_control::GammaControlManagerState); + }; +} + +impl Dispatch for GammaControlManagerState +where + D: Dispatch + GammaControlHandler + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ZwlrGammaControlV1, + request: ::Request, + data: &GammaControlState, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + if matches!(request, zwlr_gamma_control_v1::Request::Destroy) { + return; + } + + let Some(output) = state + .gamma_control_manager_state() + .gamma_controls + .iter() + .find(|(_, res)| *res == resource) + .map(|(output, _)| output) + .cloned() + else { + resource.failed(); + return; + }; + + let GammaControlState { gamma_size } = data; + + let gamma_size = *gamma_size as usize; + + let fd = match request { + zwlr_gamma_control_v1::Request::SetGamma { fd } => fd, + zwlr_gamma_control_v1::Request::Destroy => return, + _ => unreachable!(), + }; + + let mut gammas = vec![0u16; gamma_size * 3]; + + { + let buf = bytemuck::cast_slice_mut(&mut gammas); + + let mut file = File::from(fd); + + let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls; + + if let Err(err) = file.read_exact(buf) { + warn!( + "Failed to read {} u16s from client gamma control fd: {err}", + gamma_size * 3 + ); + resource.failed(); + gamma_controls.remove(&output); + state.gamma_control_destroyed(&output); + return; + } + + #[allow(clippy::unused_io_amount)] + { + match file.read(&mut [0]) { + Ok(0) => (), + Ok(_) => { + warn!( + "Client gamma control sent more data than expected (expected {} u16s)", + gamma_size * 3, + ); + resource.failed(); + gamma_controls.remove(&output); + state.gamma_control_destroyed(&output); + return; + } + Err(err) => { + warn!( + "Failed to ensure client gamma control fd was the correct size: {err}" + ); + resource.failed(); + gamma_controls.remove(&output); + state.gamma_control_destroyed(&output); + return; + } + } + } + } + + assert_eq!(gammas.len(), gamma_size * 3); + + let gammas = gammas.chunks_exact(gamma_size).collect::>(); + let [red_gamma, green_gamma, blue_gamma] = gammas.as_slice() else { + unreachable!(); + }; + + if !state.set_gamma(&output, [red_gamma, green_gamma, blue_gamma]) { + resource.failed(); + state + .gamma_control_manager_state() + .gamma_controls + .remove(&output); + state.gamma_control_destroyed(&output); + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ZwlrGammaControlV1, + _data: &GammaControlState, + ) { + let gamma_controls = &mut state.gamma_control_manager_state().gamma_controls; + + let Some(output) = gamma_controls + .iter() + .find(|(_, res)| *res == resource) + .map(|(output, _)| output) + .cloned() + else { + return; + }; + + gamma_controls.remove(&output); + + state.gamma_control_destroyed(&output); + } +} diff --git a/src/render.rs b/src/render.rs index c6182fe..4c1cd4f 100644 --- a/src/render.rs +++ b/src/render.rs @@ -46,7 +46,7 @@ render_elements! { } render_elements! { - pub OutputRenderElements where R: ImportAll + ImportMem; + pub OutputRenderElement where R: ImportAll + ImportMem; Custom = Wrap, Surface = WaylandSurfaceRenderElement, Pointer = PointerRenderElement, @@ -137,16 +137,13 @@ fn window_render_elements( renderer: &mut R, scale: Scale, ) -> ( - Vec>>, - Vec>>, + Vec>>, + Vec>>, ) where R: Renderer + ImportAll + ImportMem, ::TextureId: 'static, { - // bot wwwwwFFww top - // rev wwFFwwwww - let mut last_fullscreen_split_at = 0; let mut elements = windows @@ -180,9 +177,9 @@ where Some(rect) => { elems.into_iter().filter_map(|elem| { CropRenderElement::from_element(elem, scale, rect.to_physical_precise_round(scale)) - }).map(TransformRenderElement::from).map(OutputRenderElements::from).collect::>() + }).map(TransformRenderElement::from).map(OutputRenderElement::from).collect::>() }, - None => elems.into_iter().map(OutputRenderElements::from).collect(), + None => elems.into_iter().map(OutputRenderElement::from).collect(), } }).collect::>(); @@ -203,7 +200,7 @@ pub fn pointer_render_elements( cursor_status: &mut CursorImageStatus, dnd_icon: Option<&WlSurface>, pointer_element: &PointerElement<::TextureId>, -) -> Vec>> +) -> Vec>> where R: Renderer + ImportAll, ::TextureId: Clone + 'static, @@ -264,7 +261,7 @@ pub fn generate_render_elements( renderer: &mut R, space: &Space, windows: &[WindowElement], -) -> Vec>> +) -> Vec>> where R: Renderer + ImportAll + ImportMem, ::TextureId: 'static, @@ -272,7 +269,7 @@ where { let scale = Scale::from(output.current_scale().fractional_scale()); - let mut output_render_elements: Vec> = Vec::new(); + let mut output_render_elements: Vec> = Vec::new(); let (windows, override_redirect_windows) = windows .iter() @@ -309,7 +306,7 @@ where // TODO: don't unconditionally render OR windows above fullscreen ones, // | base it on if it's a descendant or not - output_render_elements.extend(o_r_elements.map(OutputRenderElements::from)); + output_render_elements.extend(o_r_elements.map(OutputRenderElement::from)); let LayerRenderElements { background, @@ -329,7 +326,7 @@ where overlay .into_iter() .chain(top) - .map(OutputRenderElements::from), + .map(OutputRenderElement::from), ); output_render_elements.extend(rest_of_window_elements); @@ -338,7 +335,7 @@ where bottom .into_iter() .chain(background) - .map(OutputRenderElements::from), + .map(OutputRenderElement::from), ); output_render_elements diff --git a/src/state.rs b/src/state.rs index 721a797..0512395 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,9 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later use crate::{ - api::signal::SignalState, backend::Backend, config::Config, cursor::Cursor, - focus::OutputFocusStack, grab::resize_grab::ResizeSurfaceState, layout::LayoutState, - protocol::screencopy::ScreencopyManagerState, window::WindowElement, + api::signal::SignalState, + backend::Backend, + config::Config, + cursor::Cursor, + focus::OutputFocusStack, + grab::resize_grab::ResizeSurfaceState, + layout::LayoutState, + protocol::{gamma_control::GammaControlManagerState, screencopy::ScreencopyManagerState}, + window::WindowElement, }; use anyhow::Context; use smithay::{ @@ -69,6 +75,7 @@ pub struct State { pub layer_shell_state: WlrLayerShellState, pub data_control_state: DataControlState, pub screencopy_manager_state: ScreencopyManagerState, + pub gamma_control_manager_state: GammaControlManagerState, /// The state of key and mousebinds along with libinput settings pub input_state: InputState, @@ -252,6 +259,10 @@ impl State { &display_handle, |_| true, ), + gamma_control_manager_state: GammaControlManagerState::new::( + &display_handle, + |_| true, + ), input_state: InputState::new(),