diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/backend/tty.rs | 61 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 60 | ||||
| -rw-r--r-- | src/niri.rs | 12 | ||||
| -rw-r--r-- | src/protocols/gamma_control.rs | 238 | ||||
| -rw-r--r-- | src/protocols/mod.rs | 1 |
7 files changed, 372 insertions, 2 deletions
@@ -2119,6 +2119,7 @@ dependencies = [ "async-channel", "async-io 1.13.0", "bitflags 2.4.2", + "bytemuck", "calloop 0.13.0", "clap", "directories", @@ -73,6 +73,7 @@ tracy-client.workspace = true url = { version = "2.5.0", optional = true } xcursor = "0.3.5" zbus = { version = "~3.15.2", optional = true } +bytemuck = "1.14.3" [dependencies.smithay] workspace = true diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 2b6ee89e..6d954a62 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{io, mem}; -use anyhow::{anyhow, bail, Context}; +use anyhow::{anyhow, bail, ensure, Context}; use libc::dev_t; use niri_config::Config; use smithay::backend::allocator::dmabuf::Dmabuf; @@ -1244,6 +1244,65 @@ impl Tty { } } + pub fn get_gamma_length(&self, output: &Output) -> Option<u32> { + let tty_state = output.user_data().get::<TtyOutputState>().unwrap(); + let device = self.devices.get(&tty_state.node)?; + let crtc_info = device.drm.get_crtc(tty_state.crtc.clone()).ok()?; + return Some(crtc_info.gamma_length()); + } + + pub fn get_gamma(&self, output: &Output) -> Option<Vec<u16>> { + let tty_state = output.user_data().get::<TtyOutputState>().unwrap(); + let device = self.devices.get(&tty_state.node)?; + let crtc_info = device.drm.get_crtc(tty_state.crtc.clone()).ok()?; + let crtc = tty_state.crtc.clone(); + let gamma_length = crtc_info.gamma_length() as usize; + let mut red = vec![0; gamma_length]; + let mut green = vec![0; gamma_length]; + let mut blue = vec![0; gamma_length]; + if let Err(err) = device.drm.get_gamma(crtc, &mut red, &mut green, &mut blue) { + warn!("error getting gamma for crtc {crtc:?}: {err:?}"); + return None; + } + red.extend(green); + red.extend(blue); + return Some(red); + } + + pub fn set_gamma( + &self, + niri: &mut Niri, + output: &Output, + ramp: Vec<u16>, + ) -> anyhow::Result<()> { + let tty_state = output.user_data().get::<TtyOutputState>().unwrap(); + let device = self + .devices + .get(&tty_state.node) + .context("missing device")?; + + let crtc_info = device.drm.get_crtc(tty_state.crtc.clone())?; + let crtc = tty_state.crtc.clone(); + let gamma_length = crtc_info.gamma_length() as usize; + + ensure!(ramp.len() == gamma_length * 3, "wrong gamma length"); + let mut red = ramp.clone(); + red.truncate(gamma_length); + let mut green = ramp.clone(); + green.drain(0..gamma_length); + green.truncate(gamma_length); + let mut blue = ramp; + blue.drain(0..gamma_length * 2); + blue.truncate(gamma_length); + + device + .drm + .set_gamma(crtc, &mut red, &mut green, &mut blue) + .context("error setting gamma")?; + niri.queue_redraw(output.clone()); + Ok(()) + } + fn refresh_ipc_outputs(&self) { let _span = tracy_client::span!("Tty::refresh_ipc_outputs"); diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index f97129c6..54a9b451 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -8,6 +8,7 @@ use std::os::fd::OwnedFd; use std::sync::Arc; use std::thread; +use anyhow::anyhow; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::drm::DrmNode; use smithay::desktop::{PopupKind, PopupManager}; @@ -15,6 +16,7 @@ use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle}; use smithay::input::{keyboard, Seat, SeatHandler, SeatState}; use smithay::output::Output; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; +use smithay::reexports::wayland_server::backend::ObjectId; use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; @@ -58,9 +60,10 @@ use crate::niri::{ClientState, State}; use crate::protocols::foreign_toplevel::{ self, ForeignToplevelHandler, ForeignToplevelManagerState, }; +use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerState}; use crate::protocols::screencopy::{Screencopy, ScreencopyHandler}; use crate::utils::output_size; -use crate::{delegate_foreign_toplevel, delegate_screencopy}; +use crate::{delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy}; impl SeatHandler for State { type KeyboardFocus = WlSurface; @@ -440,3 +443,58 @@ impl DrmLeaseHandler for State { delegate_drm_lease!(State); delegate_viewporter!(State); + +impl GammaControlHandler for State { + fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState { + &mut self.niri.gamma_control_manager_state + } + + fn set_gamma( + &mut self, + wl_output: &WlOutput, + ramp: Vec<u16>, + gamma_size: u32, + ) -> anyhow::Result<()> { + if ramp.len() != gamma_size as usize * 3 { + error!( + "gamma length wrong (expected {}, got {})", + gamma_size, + ramp.len() + ); + return Err(anyhow!("Gamma length wrong")); + } + + let Some(output) = Output::from_resource(wl_output) else { + return Err(anyhow!("No Output matching WlOutput")); + }; + + self.backend.tty().set_gamma(&mut self.niri, &output, ramp) + } + + fn get_gamma(&mut self, wl_output: &WlOutput) -> Option<Vec<u16>> { + let output = Output::from_resource(wl_output)?; + + let gamma_ramp = self.backend.tty().get_gamma(&output); + if gamma_ramp.is_none() { + warn!("Failed to get gamma ramp"); + } + gamma_ramp + } + + fn destroy(&mut self, output_id: ObjectId) { + self.niri + .gamma_control_manager_state + .destroy_gamma_control(output_id) + } + + fn get_gamma_size(&mut self, wl_output: &WlOutput) -> Option<u32> { + let output = Output::from_resource(wl_output)?; + let gamma_size = self.backend.tty().get_gamma_length(&output); + if gamma_size.is_none() { + warn!("Failed to get gamma size"); + } + gamma_size + } +} + +delegate_gamma_control!(State); diff --git a/src/niri.rs b/src/niri.rs index 01dc9eb5..1e5b5c16 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -101,6 +101,7 @@ use crate::input::{apply_libinput_settings, TabletData}; use crate::ipc::server::IpcServer; use crate::layout::{Layout, MonitorRenderElement}; use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; +use crate::protocols::gamma_control::GammaControlManagerState; use crate::protocols::screencopy::{Screencopy, ScreencopyManagerState}; use crate::pw_utils::{Cast, PipeWire}; use crate::render_helpers::renderer::NiriRenderer; @@ -187,6 +188,7 @@ pub struct Niri { pub popup_grab: Option<PopupGrabState>, pub presentation_state: PresentationState, pub security_context_state: SecurityContextState, + pub gamma_control_manager_state: GammaControlManagerState, pub seat: Seat<State>, /// Scancodes of the keys to suppress. @@ -929,6 +931,15 @@ impl Niri { let viewporter_state = ViewporterState::new::<State>(&display_handle); let xdg_foreign_state = XdgForeignState::new::<State>(&display_handle); + let gamma_control_manager_state = GammaControlManagerState::new::<State, _>( + &display_handle, + match backend { + Backend::Tty(_) => true, + _ => false, + }, + |client| !client.get_data::<ClientState>().unwrap().restricted, + ); + let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, backend.seat_name()); seat.add_keyboard( config_.input.keyboard.xkb.to_xkb_config(), @@ -1082,6 +1093,7 @@ impl Niri { suppressed_keys: HashSet::new(), presentation_state, security_context_state, + gamma_control_manager_state, seat, keyboard_focus: None, diff --git a/src/protocols/gamma_control.rs b/src/protocols/gamma_control.rs new file mode 100644 index 00000000..947b5e28 --- /dev/null +++ b/src/protocols/gamma_control.rs @@ -0,0 +1,238 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; + +use smithay::reexports::wayland_protocols_wlr; +use smithay::reexports::wayland_server::backend::{ClientId, ObjectId}; +use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; +use wayland_protocols_wlr::gamma_control::v1::server::{ + zwlr_gamma_control_manager_v1, zwlr_gamma_control_v1, +}; +use zwlr_gamma_control_manager_v1::ZwlrGammaControlManagerV1; +use zwlr_gamma_control_v1::ZwlrGammaControlV1; + +const VERSION: u32 = 1; + +pub struct GammaControlManagerState { + gamma_controls: HashMap<WlOutput, ZwlrGammaControlV1>, +} + +pub struct GammaControlManagerGlobalData { + filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>, + can_view: bool, +} + +pub trait GammaControlHandler { + fn gamma_control_manager_state(&mut self) -> &mut GammaControlManagerState; + fn set_gamma( + &mut self, + output: &WlOutput, + ramp: Vec<u16>, + gamma_size: u32, + ) -> anyhow::Result<()>; + fn get_gamma(&mut self, output: &WlOutput) -> Option<Vec<u16>>; + fn destroy(&mut self, output_id: ObjectId); + fn get_gamma_size(&mut self, output: &WlOutput) -> Option<u32>; +} + +pub struct GammaControlState { + gamma_size: Option<u32>, + previous_gamma_ramp: Option<Vec<u16>>, + output: WlOutput, + failed: bool, +} + +impl GammaControlManagerState { + pub fn new<D, F>(display: &DisplayHandle, can_view: bool, filter: F) -> Self + where + D: GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData>, + D: Dispatch<ZwlrGammaControlManagerV1, ()>, + D: Dispatch<ZwlrGammaControlV1, GammaControlState>, + D: GammaControlHandler, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let global_data = GammaControlManagerGlobalData { + filter: Box::new(filter), + can_view, + }; + display.create_global::<D, ZwlrGammaControlManagerV1, _>(VERSION, global_data); + + Self { + gamma_controls: HashMap::new(), + } + } + pub fn destroy_gamma_control(&mut self, output_id: ObjectId) { + self.gamma_controls.remove(&output_id); + } +} + +impl<D> GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData, D> + for GammaControlManagerState +where + D: GlobalDispatch<ZwlrGammaControlManagerV1, GammaControlManagerGlobalData>, + D: Dispatch<ZwlrGammaControlManagerV1, ()>, + D: Dispatch<ZwlrGammaControlV1, GammaControlState>, + D: GammaControlHandler, + D: 'static, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + manager: New<ZwlrGammaControlManagerV1>, + _manager_state: &GammaControlManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(manager, ()); + } + + fn can_view(client: Client, global_data: &GammaControlManagerGlobalData) -> bool { + global_data.can_view && (global_data.filter)(&client) + } +} + +impl<D> Dispatch<ZwlrGammaControlManagerV1, (), D> for GammaControlManagerState +where + D: Dispatch<ZwlrGammaControlManagerV1, ()>, + D: Dispatch<ZwlrGammaControlV1, GammaControlState>, + D: GammaControlHandler, + D: 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _resource: &ZwlrGammaControlManagerV1, + request: <ZwlrGammaControlManagerV1 as Resource>::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_gamma_control_manager_v1::Request::GetGammaControl { id, output } => { + let gamma_size = state.get_gamma_size(&output); + let previous_gamma_ramp = state.get_gamma(&output); + + if state + .gamma_control_manager_state() + .gamma_controls + .contains_key(&output) + || gamma_size.is_none() + || previous_gamma_ramp.is_none() + { + data_init + .init( + id, + GammaControlState { + gamma_size: gamma_size.clone(), + previous_gamma_ramp: None, + output: output.clone(), + failed: true, + }, + ) + .failed(); + return; + } + + let zwlr_gamma_control = data_init.init( + id, + GammaControlState { + gamma_size: gamma_size.clone(), + previous_gamma_ramp, + output: output.clone(), + failed: false, + }, + ); + + zwlr_gamma_control.gamma_size(gamma_size.unwrap()); + state + .gamma_control_manager_state() + .gamma_controls + .insert(output, zwlr_gamma_control); + } + zwlr_gamma_control_manager_v1::Request::Destroy => (), + _ => unreachable!(), + } + } +} + +impl<D> Dispatch<ZwlrGammaControlV1, GammaControlState, D> for GammaControlManagerState +where + D: Dispatch<ZwlrGammaControlV1, GammaControlState>, + D: GammaControlHandler, + D: 'static, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ZwlrGammaControlV1, + request: <ZwlrGammaControlV1 as Resource>::Request, + data: &GammaControlState, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_gamma_control_v1::Request::SetGamma { fd } => { + if data.failed { + return; + } + debug!("setting gamma for output {:?}", data.output); + let buf = &mut Vec::new(); + if File::from(fd).read_to_end(buf).is_err() { + warn!("failed to read gamma data for output {:?}", data.output); + resource.failed(); + return; + } + + let gamma = bytemuck::cast_slice(buf).to_vec(); + let gamma_size = data.gamma_size.unwrap(); + + if let Err(err) = state.set_gamma(&data.output, gamma, gamma_size) { + warn!("error setting gamma: {err:?}"); + resource.failed(); + return; + } + } + zwlr_gamma_control_v1::Request::Destroy => (), + _ => unreachable!(), + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + _resource: &ZwlrGammaControlV1, + data: &GammaControlState, + ) { + if data.failed { + return; + } + let ramp = data.previous_gamma_ramp.as_ref().unwrap(); + + if let Err(err) = state.set_gamma(&data.output, ramp.to_vec(), data.gamma_size.unwrap()) { + warn!("error resetting gamma: {err:?}"); + } + + state.destroy(data.output.id()); + } +} + +#[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::protocols::gamma_control::GammaControlManagerGlobalData + ] => $crate::protocols::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::protocols::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::protocols::gamma_control::GammaControlState + ] => $crate::protocols::gamma_control::GammaControlManagerState); + }; +} diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 1e3ef42f..d1779b7e 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -1,2 +1,3 @@ pub mod foreign_toplevel; +pub mod gamma_control; pub mod screencopy; |
