From 916e3644c33652ff5b7b25a02ce8d38fd3ee58d2 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Wed, 13 Apr 2022 21:01:32 +0200 Subject: [PATCH] autocommit 2022-04-13 21:01:32 CEST --- Cargo.lock | 6 ++ Cargo.toml | 7 +- qoi/src/lib.rs | 91 ++++++++++++++++++ src/backends/metal/video.rs | 4 +- src/backends/x.rs | 2 +- src/cli.rs | 15 +++ src/cli/screenshot.rs | 108 ++++++++++++++++++++++ src/client.rs | 2 +- src/client/error.rs | 8 ++ src/ifs.rs | 1 + src/ifs/jay_compositor.rs | 101 ++++++++++---------- src/ifs/jay_screenshot.rs | 57 ++++++++++++ src/macros.rs | 38 ++++++++ src/main.rs | 1 + src/render/egl/display.rs | 4 +- src/render/renderer/context.rs | 3 + src/render/renderer/framebuffer.rs | 3 +- src/render/renderer/renderer.rs | 25 ++++- src/screenshoter.rs | 59 ++++++++++++ src/tasks/connector.rs | 1 + src/theme.rs | 7 ++ src/tree/display.rs | 34 ++++++- src/tree/output.rs | 1 + src/utils/oserror.rs | 6 ++ src/utils/windows.rs | 6 ++ src/video.rs | 2 + src/video/dmabuf.rs | 2 + src/video/gbm.rs | 142 +++++++++++++++++++++++++++-- wire/jay_compositor.txt | 4 + wire/jay_screenshot.txt | 14 +++ 30 files changed, 681 insertions(+), 73 deletions(-) create mode 100644 qoi/src/lib.rs create mode 100644 src/cli/screenshot.rs create mode 100644 src/ifs/jay_screenshot.rs create mode 100644 src/screenshoter.rs create mode 100644 wire/jay_screenshot.txt diff --git a/Cargo.lock b/Cargo.lock index 1e2c0271..e9e43c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,6 +314,7 @@ dependencies = [ "bitflags", "bstr", "byteorder", + "chrono", "clap", "clap_complete", "default-config", @@ -328,6 +329,7 @@ dependencies = [ "num-traits", "once_cell", "pin-project", + "qoi", "rand", "repc", "smallvec", @@ -515,6 +517,10 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "qoi" +version = "0.1.0" + [[package]] name = "quote" version = "1.0.16" diff --git a/Cargo.toml b/Cargo.toml index 769749c7..dcff4f90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" build = "build/build.rs" [workspace] -members = ["jay-config", "default-config"] +members = ["jay-config", "default-config", "qoi"] [profile.release] panic = "abort" @@ -32,12 +32,14 @@ byteorder = "1.4.3" bincode = "2.0.0-rc.1" jay-config = { path = "jay-config" } default-config = { path = "default-config" } +qoi = { path = "qoi" } pin-project = "1.0.10" clap = { version = "3.1.6", features = ["derive", "wrap_help"] } clap_complete = "3.1.1" humantime = "2.1.0" dirs = "4.0.0" backtrace = "0.3.64" +chrono = "0.4.19" [build-dependencies] repc = "0.1.1" @@ -47,5 +49,8 @@ bstr = { version = "0.2.17", default-features = false, features = ["std"] } #[profile.dev.build-override] #opt-level = 3 +[profile.dev.package."qoi"] +opt-level = 3 + [features] rc_tracking = [] diff --git a/qoi/src/lib.rs b/qoi/src/lib.rs new file mode 100644 index 00000000..182c2f6a --- /dev/null +++ b/qoi/src/lib.rs @@ -0,0 +1,91 @@ +pub fn xrgb8888_encode_qoi(bytes: &[u8], width: u32, height: u32, stride: u32) -> Vec { + const OP_RGB: u8 = 0b1111_1110; + const OP_INDEX: u8 = 0b0000_0000; + const OP_DIFF: u8 = 0b0100_0000; + const OP_LUMA: u8 = 0b1000_0000; + const OP_RUN: u8 = 0b1100_0000; + + let mut res = vec![]; + let width_bytes_be = width.to_be_bytes(); + let height_bytes_be = height.to_be_bytes(); + let header = [ + b'q', + b'o', + b'i', + b'f', + width_bytes_be[0], + width_bytes_be[1], + width_bytes_be[2], + width_bytes_be[3], + height_bytes_be[0], + height_bytes_be[1], + height_bytes_be[2], + height_bytes_be[3], + 3, + 0, + ]; + res.extend_from_slice(&header); + let mut prev_pixel = [0, 0, 0, 0xff]; + let mut array = [[0; 4]; 64]; + let mut run_length = 0; + for line in bytes.chunks_exact(stride as _) { + for &pixel in array_chunks::<_, 4>(&line[..(width * 4) as _]) { + let pixel = [pixel[2], pixel[1], pixel[0], 0xff]; + if pixel == prev_pixel { + run_length += 1; + if run_length == 62 { + res.push(OP_RUN | (run_length - 1)); + run_length = 0; + } + continue; + } + if run_length > 0 { + res.push(OP_RUN | (run_length - 1)); + run_length = 0; + } + let prev = prev_pixel; + prev_pixel = pixel; + let index = { + let sum = 0u8 + .wrapping_add(pixel[0].wrapping_mul(3)) + .wrapping_add(pixel[1].wrapping_mul(5)) + .wrapping_add(pixel[2].wrapping_mul(7)) + .wrapping_add(255u8.wrapping_mul(11)); + sum & 63 + }; + if array[index as usize] == pixel { + res.push(OP_INDEX | index); + continue; + } + array[index as usize] = pixel; + let dr = pixel[0].wrapping_sub(prev[0]); + let dg = pixel[1].wrapping_sub(prev[1]); + let db = pixel[2].wrapping_sub(prev[2]); + let dr_2 = dr.wrapping_add(2); + let dg_2 = dg.wrapping_add(2); + let db_2 = db.wrapping_add(2); + if dr_2 | dg_2 | db_2 | 3 == 3 { + res.push(OP_DIFF | (dr_2 << 4) | (dg_2 << 2) | db_2); + continue; + } + let dr_dg_8 = dr.wrapping_sub(dg).wrapping_add(8); + let db_dg_8 = db.wrapping_sub(dg).wrapping_add(8); + let dg_32 = dg.wrapping_add(32); + if (dg_32 | 63 == 63) && (dr_dg_8 | db_dg_8 | 15 == 15) { + res.extend_from_slice(&[OP_LUMA | dg_32, (dr_dg_8 << 4) | db_dg_8]); + continue; + } + res.extend_from_slice(&[OP_RGB, pixel[0], pixel[1], pixel[2]]); + } + } + if run_length > 0 { + res.push(OP_RUN | (run_length - 1)); + } + res.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 1]); + res +} + +fn array_chunks(slice: &[T]) -> &[[T; N]] { + let len = slice.len() / N; + unsafe { std::slice::from_raw_parts(slice.as_ptr() as _, len) } +} diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index 8e36ba4b..2e96feb3 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -869,11 +869,11 @@ impl MetalBackend { Ok(b) => b, Err(e) => return Err(MetalError::ScanoutBuffer(e)), }; - let drm_fb = match dev.dev.master.add_fb(bo.dma()) { + let drm_fb = match dev.dev.master.add_fb(bo.dmabuf()) { Ok(fb) => Rc::new(fb), Err(e) => return Err(MetalError::Framebuffer(e)), }; - let egl_fb = match dev.dev.egl.dmabuf_fb(bo.dma()) { + let egl_fb = match dev.dev.egl.dmabuf_fb(bo.dmabuf()) { Ok(fb) => fb, Err(e) => return Err(MetalError::ImportFb(e)), }; diff --git a/src/backends/x.rs b/src/backends/x.rs index f391665b..3385cf86 100644 --- a/src/backends/x.rs +++ b/src/backends/x.rs @@ -346,7 +346,7 @@ impl XBackendData { let bo = self .gbm .create_bo(width, height, &format, GBM_BO_USE_RENDERING)?; - let dma = bo.dma(); + let dma = bo.dmabuf(); assert!(dma.planes.len() == 1); let plane = dma.planes.first().unwrap(); let size = plane.stride * dma.height as u32; diff --git a/src/cli.rs b/src/cli.rs index 0aec16fc..c8a5d7e5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,7 @@ mod generate; mod log; mod quit; +mod screenshot; mod set_log_level; use { @@ -38,6 +39,19 @@ pub enum Cmd { SetLogLevel(SetLogArgs), /// Stop the compositor. Quit, + /// Take a screenshot. + Screenshot(ScreenshotArgs), +} + +#[derive(Args, Debug)] +pub struct ScreenshotArgs { + /// The filename of the saved screenshot + /// + /// If no filename is given, the screenshot will be saved under jay-%Y-%m-%d-%H:%M:%S.qoi + /// in the current directory. + /// + /// The filename can contain the usual strftime parameters. + pub filename: Option, } #[derive(Args, Debug)] @@ -121,5 +135,6 @@ pub fn main() { Cmd::Log(a) => log::main(cli.global, a), Cmd::Quit => quit::main(cli.global), Cmd::SetLogLevel(a) => set_log_level::main(cli.global, a), + Cmd::Screenshot(a) => screenshot::main(cli.global, a), } } diff --git a/src/cli/screenshot.rs b/src/cli/screenshot.rs new file mode 100644 index 00000000..22359692 --- /dev/null +++ b/src/cli/screenshot.rs @@ -0,0 +1,108 @@ +use { + crate::{ + cli::{GlobalArgs, ScreenshotArgs}, + format::XRGB8888, + tools::tool_client::{Handle, ToolClient}, + utils::{errorfmt::ErrorFmt, queue::AsyncQueue}, + video::{ + dmabuf::{DmaBuf, DmaBufPlane}, + drm::Drm, + gbm::{GbmDevice, GBM_BO_USE_LINEAR, GBM_BO_USE_RENDERING}, + INVALID_MODIFIER, + }, + wire::{ + jay_compositor::TakeScreenshot, + jay_screenshot::{Dmabuf, Error}, + }, + }, + chrono::Local, + qoi::xrgb8888_encode_qoi, + std::rc::Rc, +}; + +pub fn main(global: GlobalArgs, args: ScreenshotArgs) { + let tc = ToolClient::new(global.log_level.into()); + let screenshot = Rc::new(Screenshot { + tc: tc.clone(), + args, + }); + tc.run(run(screenshot)); +} + +struct Screenshot { + tc: Rc, + args: ScreenshotArgs, +} + +async fn run(screenshot: Rc) { + let tc = &screenshot.tc; + let comp = tc.jay_compositor().await; + let sid = tc.id(); + tc.send(TakeScreenshot { + self_id: comp, + id: sid, + }); + let result = Rc::new(AsyncQueue::new()); + Error::handle(&tc, sid, result.clone(), |res, err| { + res.push(Err(err.msg.to_owned())); + }); + Dmabuf::handle(&tc, sid, result.clone(), |res, buf| { + res.push(Ok(buf)); + }); + let buf = match result.pop().await { + Ok(b) => b, + Err(e) => { + fatal!("Could not take a screenshot: {}", e); + } + }; + let data = buf_to_qoi(&buf); + let filename = screenshot + .args + .filename + .as_deref() + .unwrap_or("jay-%Y-%m-%d-%H:%M:%S.qoi"); + let filename = Local::now().format(filename).to_string(); + if let Err(e) = std::fs::write(&filename, &data) { + fatal!("Could not write `{}`: {}", filename, ErrorFmt(e)); + } +} + +fn buf_to_qoi(buf: &Dmabuf) -> Vec { + let drm = match Drm::reopen(buf.drm_dev.raw(), false) { + Ok(drm) => drm, + Err(e) => { + fatal!("Could not open the drm device: {}", ErrorFmt(e)); + } + }; + let gbm = match GbmDevice::new(&drm) { + Ok(g) => g, + Err(e) => { + fatal!("Could not create a gbm device: {}", ErrorFmt(e)); + } + }; + let dmabuf = DmaBuf { + width: buf.width as _, + height: buf.height as _, + format: XRGB8888, + modifier: INVALID_MODIFIER, + planes: vec![DmaBufPlane { + offset: buf.offset, + stride: buf.stride, + fd: buf.fd.clone(), + }], + }; + let bo = match gbm.import_dmabuf(&dmabuf, GBM_BO_USE_LINEAR | GBM_BO_USE_RENDERING) { + Ok(bo) => Rc::new(bo), + Err(e) => { + fatal!("Could not import screenshot dmabuf: {}", ErrorFmt(e)); + } + }; + let bo_map = match bo.map() { + Ok(map) => map, + Err(e) => { + fatal!("Could not map dmabuf: {}", ErrorFmt(e)); + } + }; + let data = unsafe { bo_map.data() }; + xrgb8888_encode_qoi(data, buf.width, buf.height, buf.stride) +} diff --git a/src/client.rs b/src/client.rs index a04ccda1..a6595d0e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,4 @@ -pub use error::{ClientError, ObjectError}; +pub use error::{ClientError, MethodError, ObjectError}; use { crate::{ async_engine::{AsyncFd, SpawnedFuture}, diff --git a/src/client/error.rs b/src/client/error.rs index 4ce59d4d..54b4b9de 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -57,6 +57,14 @@ impl ClientError { } } +#[derive(Debug, Error)] +#[error("Could not process a `{method}` request")] +pub struct MethodError { + pub method: &'static str, + #[source] + pub error: Box, +} + #[derive(Debug, Error)] #[error("An error occurred in a `{}`", .interface.name())] pub struct ObjectError { diff --git a/src/ifs.rs b/src/ifs.rs index 751b47b1..5260fc31 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -1,6 +1,7 @@ pub mod ipc; pub mod jay_compositor; pub mod jay_log_file; +pub mod jay_screenshot; pub mod org_kde_kwin_server_decoration; pub mod org_kde_kwin_server_decoration_manager; pub mod wl_buffer; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 115beb88..50efdfae 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -3,14 +3,18 @@ use { cli::CliLogLevel, client::{Client, ClientError}, globals::{Global, GlobalName}, - ifs::jay_log_file::JayLogFile, + ifs::{jay_log_file::JayLogFile, jay_screenshot::JayScreenshot}, leaks::Tracker, object::Object, - utils::buffd::{MsgParser, MsgParserError}, + screenshoter::take_screenshot, + utils::{ + buffd::{MsgParser, MsgParserError}, + errorfmt::ErrorFmt, + }, wire::{jay_compositor::*, JayCompositorId}, }, log::Level, - std::rc::Rc, + std::{ops::Deref, rc::Rc}, thiserror::Error, }; @@ -65,13 +69,13 @@ pub struct JayCompositor { } impl JayCompositor { - fn destroy(&self, parser: MsgParser<'_, '_>) -> Result<(), DestroyError> { + fn destroy(&self, parser: MsgParser<'_, '_>) -> Result<(), JayCompositorError> { let _req: Destroy = self.client.parse(self, parser)?; self.client.remove_obj(self)?; Ok(()) } - fn get_log_file(&self, parser: MsgParser<'_, '_>) -> Result<(), GetLogFileError> { + fn get_log_file(&self, parser: MsgParser<'_, '_>) -> Result<(), JayCompositorError> { let req: GetLogFile = self.client.parse(self, parser)?; let log_file = Rc::new(JayLogFile::new(req.id, &self.client)); track!(self.client, log_file); @@ -80,14 +84,14 @@ impl JayCompositor { Ok(()) } - fn quit(&self, parser: MsgParser<'_, '_>) -> Result<(), QuitError> { + fn quit(&self, parser: MsgParser<'_, '_>) -> Result<(), JayCompositorError> { let _req: Quit = self.client.parse(self, parser)?; log::info!("Quitting"); self.client.state.el.stop(); Ok(()) } - fn set_log_level(&self, parser: MsgParser<'_, '_>) -> Result<(), SetLogLevelError> { + fn set_log_level(&self, parser: MsgParser<'_, '_>) -> Result<(), JayCompositorError> { let req: SetLogLevel = self.client.parse(self, parser)?; const ERROR: u32 = CliLogLevel::Error as u32; const WARN: u32 = CliLogLevel::Warn as u32; @@ -100,25 +104,57 @@ impl JayCompositor { INFO => Level::Info, DEBUG => Level::Debug, TRACE => Level::Trace, - _ => return Err(SetLogLevelError::UnknownLogLevel(req.level)), + _ => return Err(JayCompositorError::UnknownLogLevel(req.level)), }; self.client.state.logger.set_level(level); Ok(()) } + + fn take_screenshot(&self, parser: MsgParser<'_, '_>) -> Result<(), JayCompositorError> { + let req: TakeScreenshot = self.client.parse(self, parser)?; + let ss = Rc::new(JayScreenshot { + id: req.id, + client: self.client.clone(), + tracker: Default::default(), + }); + track!(self.client, ss); + self.client.add_client_obj(&ss)?; + match take_screenshot(&self.client.state) { + Ok(s) => { + let dmabuf = s.bo.dmabuf(); + let plane = &dmabuf.planes[0]; + ss.send_dmabuf( + &s.drm, + &plane.fd, + dmabuf.width, + dmabuf.height, + plane.offset, + plane.stride, + ); + } + Err(e) => { + let msg = ErrorFmt(e).to_string(); + ss.send_error(&msg); + } + } + self.client.remove_obj(ss.deref())?; + Ok(()) + } } -object_base! { - JayCompositor, JayCompositorError; +object_base2! { + JayCompositor; DESTROY => destroy, GET_LOG_FILE => get_log_file, QUIT => quit, SET_LOG_LEVEL => set_log_level, + TAKE_SCREENSHOT => take_screenshot, } impl Object for JayCompositor { fn num_requests(&self) -> u32 { - SET_LOG_LEVEL + 1 + TAKE_SCREENSHOT + 1 } } @@ -126,51 +162,12 @@ simple_add_obj!(JayCompositor); #[derive(Debug, Error)] pub enum JayCompositorError { - #[error("Could not process a `destroy` request")] - DestroyError(#[from] DestroyError), - #[error("Could not process a `get_log_file` request")] - GetLogFileError(#[from] GetLogFileError), - #[error("Could not process a `quit` request")] - QuitError(#[from] QuitError), - #[error("Could not process a `set_log_level` request")] - SetLogLevelError(#[from] SetLogLevelError), - #[error(transparent)] - ClientError(Box), -} -efrom!(JayCompositorError, ClientError); - -#[derive(Debug, Error)] -pub enum DestroyError { #[error("Parsing failed")] MsgParserError(#[source] Box), #[error(transparent)] ClientError(Box), -} -efrom!(DestroyError, ClientError); -efrom!(DestroyError, MsgParserError); - -#[derive(Debug, Error)] -pub enum GetLogFileError { - #[error("Parsing failed")] - MsgParserError(#[source] Box), - #[error(transparent)] - ClientError(Box), -} -efrom!(GetLogFileError, ClientError); -efrom!(GetLogFileError, MsgParserError); - -#[derive(Debug, Error)] -pub enum QuitError { - #[error("Parsing failed")] - MsgParserError(#[source] Box), -} -efrom!(QuitError, MsgParserError); - -#[derive(Debug, Error)] -pub enum SetLogLevelError { - #[error("Parsing failed")] - MsgParserError(#[source] Box), #[error("Unknown log level {0}")] UnknownLogLevel(u32), } -efrom!(SetLogLevelError, MsgParserError); +efrom!(JayCompositorError, ClientError); +efrom!(JayCompositorError, MsgParserError); diff --git a/src/ifs/jay_screenshot.rs b/src/ifs/jay_screenshot.rs new file mode 100644 index 00000000..60b6475f --- /dev/null +++ b/src/ifs/jay_screenshot.rs @@ -0,0 +1,57 @@ +use { + crate::{ + client::Client, + leaks::Tracker, + object::Object, + wire::{jay_screenshot::*, JayScreenshotId}, + }, + std::rc::Rc, + uapi::OwnedFd, +}; + +pub struct JayScreenshot { + pub id: JayScreenshotId, + pub client: Rc, + pub tracker: Tracker, +} + +impl JayScreenshot { + pub fn send_dmabuf( + &self, + drm_dev: &Rc, + fd: &Rc, + width: i32, + height: i32, + offset: u32, + stride: u32, + ) { + self.client.event(Dmabuf { + self_id: self.id, + drm_dev: drm_dev.clone(), + fd: fd.clone(), + width: width as _, + height: height as _, + offset, + stride, + }); + } + + pub fn send_error(&self, msg: &str) { + self.client.event(Error { + self_id: self.id, + msg, + }); + } +} + +object_base2! { + JayScreenshot; +} + +impl Object for JayScreenshot { + fn num_requests(&self) -> u32 { + 0 + } +} + +simple_add_obj!(JayScreenshot); diff --git a/src/macros.rs b/src/macros.rs index 9b881d3f..c0e62d1a 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -11,6 +11,44 @@ macro_rules! efrom { }; } +macro_rules! object_base2 { + ($oname:ident; $($code:ident => $f:ident,)*) => { + impl crate::object::ObjectBase for $oname { + fn id(&self) -> crate::object::ObjectId { + self.id.into() + } + + #[allow(unused_variables, unreachable_code)] + fn handle_request( + self: std::rc::Rc, + request: u32, + parser: crate::utils::buffd::MsgParser<'_, '_>, + ) -> Result<(), crate::client::ClientError> { + let res: Result<(), crate::client::MethodError> = match request { + $( + $code => $oname::$f(&self, parser).map_err(|e| crate::client::MethodError { + method: stringify!($f), + error: Box::new(e), + }), + )* + _ => unreachable!(), + }; + if let Err(e) = res { + return Err(crate::client::ClientError::ObjectError(crate::client::ObjectError { + interface: crate::wire::$oname, + error: Box::new(e), + })); + } + Ok(()) + } + + fn interface(&self) -> crate::object::Interface { + crate::wire::$oname + } + } + }; +} + macro_rules! object_base { ($oname:ident, $ename:ty; $($code:ident => $f:ident,)*) => { impl crate::object::ObjectBase for $oname { diff --git a/src/main.rs b/src/main.rs index 8a5c9c47..3e1b5e37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,7 @@ mod object; mod pango; mod rect; mod render; +mod screenshoter; mod sighand; mod state; mod tasks; diff --git a/src/render/egl/display.rs b/src/render/egl/display.rs index 4b4e703b..a09e0bea 100644 --- a/src/render/egl/display.rs +++ b/src/render/egl/display.rs @@ -36,7 +36,7 @@ use { pub struct EglDisplay { pub exts: DisplayExt, pub formats: Rc>, - pub gbm: GbmDevice, + pub gbm: Rc, pub dpy: EGLDisplay, } @@ -58,7 +58,7 @@ impl EglDisplay { let mut dpy = EglDisplay { exts: DisplayExt::empty(), formats: Rc::new(AHashMap::new()), - gbm, + gbm: Rc::new(gbm), dpy, }; let mut major = 0; diff --git a/src/render/renderer/context.rs b/src/render/renderer/context.rs index a76a3be8..d9cdc360 100644 --- a/src/render/renderer/context.rs +++ b/src/render/renderer/context.rs @@ -12,6 +12,7 @@ use { video::{ dmabuf::DmaBuf, drm::{Drm, NodeType}, + gbm::GbmDevice, }, }, ahash::AHashMap, @@ -44,6 +45,7 @@ impl TexProg { pub struct RenderContext { pub(super) ctx: Rc, + pub gbm: Rc, pub(super) render_node: Rc, @@ -97,6 +99,7 @@ impl RenderContext { )?; Ok(Self { ctx: ctx.clone(), + gbm: ctx.dpy.gbm.clone(), render_node: node.clone(), diff --git a/src/render/renderer/framebuffer.rs b/src/render/renderer/framebuffer.rs index 4a5c00d6..16d10143 100644 --- a/src/render/renderer/framebuffer.rs +++ b/src/render/renderer/framebuffer.rs @@ -47,10 +47,11 @@ impl Framebuffer { pub fn render(&self, node: &dyn Node, state: &State, cursor_rect: Option) { let _ = self.ctx.ctx.with_current(|| { + let c = state.theme.background_color.get(); unsafe { glBindFramebuffer(GL_FRAMEBUFFER, self.gl.fbo); glViewport(0, 0, self.gl.width, self.gl.height); - glClearColor(0.0, 0.0, 0.0, 1.0); + glClearColor(c.r, c.g, c.b, 1.0); glClear(GL_COLOR_BUFFER_BIT); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); } diff --git a/src/render/renderer/renderer.rs b/src/render/renderer/renderer.rs index 65533516..bfa007d1 100644 --- a/src/render/renderer/renderer.rs +++ b/src/render/renderer/renderer.rs @@ -25,7 +25,7 @@ use { }, state::State, theme::Color, - tree::{ContainerNode, FloatNode, OutputNode, WorkspaceNode}, + tree::{ContainerNode, DisplayNode, FloatNode, OutputNode, WorkspaceNode}, utils::rc_eq::rc_eq, }, std::{ops::Deref, rc::Rc, slice}, @@ -38,6 +38,16 @@ pub struct Renderer<'a> { } impl Renderer<'_> { + pub fn render_display(&mut self, display: &DisplayNode, x: i32, y: i32) { + let ext = display.extents.get(); + let outputs = display.outputs.lock(); + for output in outputs.values() { + let opos = output.global.pos.get(); + let (ox, oy) = ext.translate(opos.x1(), opos.y1()); + self.render_output(output, x + ox, y + oy); + } + } + pub fn render_output(&mut self, output: &OutputNode, x: i32, y: i32) { let opos = output.global.pos.get(); macro_rules! render_layer { @@ -57,15 +67,22 @@ impl Renderer<'_> { let theme = &self.state.theme; let th = theme.title_height.get(); { + let c = Color::BLACK; + self.fill_boxes2( + slice::from_ref(&Rect::new_sized(0, 0, opos.width(), th).unwrap()), + &c, + x, + y, + ); let rd = output.render_data.borrow_mut(); if let Some(aw) = &rd.active_workspace { let c = theme.active_title_color.get(); - self.fill_boxes(slice::from_ref(aw), &c); + self.fill_boxes2(slice::from_ref(aw), &c, x, y); } let c = theme.underline_color.get(); - self.fill_boxes(slice::from_ref(&rd.underline), &c); + self.fill_boxes2(slice::from_ref(&rd.underline), &c, x, y); let c = theme.title_color.get(); - self.fill_boxes(&rd.inactive_workspaces, &c); + self.fill_boxes2(&rd.inactive_workspaces, &c, x, y); for title in &rd.titles { self.render_texture(&title.tex, x + title.x, y + title.y, ARGB8888); } diff --git a/src/screenshoter.rs b/src/screenshoter.rs new file mode 100644 index 00000000..6c7ca277 --- /dev/null +++ b/src/screenshoter.rs @@ -0,0 +1,59 @@ +use { + crate::{ + format::XRGB8888, + render::RenderError, + state::State, + video::{ + drm::DrmError, + gbm::{GbmBo, GbmError, GBM_BO_USE_LINEAR, GBM_BO_USE_RENDERING}, + ModifiedFormat, INVALID_MODIFIER, + }, + }, + std::{ops::Deref, rc::Rc}, + thiserror::Error, + uapi::OwnedFd, +}; + +#[derive(Debug, Error)] +pub enum ScreenshooterError { + #[error("There is no render context")] + NoRenderContext, + #[error("Display is empty")] + EmptyDisplay, + #[error(transparent)] + GbmError(#[from] GbmError), + #[error(transparent)] + RenderError(#[from] RenderError), + #[error(transparent)] + DrmError(#[from] DrmError), +} + +pub struct Screenshot { + pub drm: Rc, + pub bo: GbmBo, +} + +pub fn take_screenshot(state: &State) -> Result { + let ctx = match state.render_ctx.get() { + Some(ctx) => ctx, + _ => return Err(ScreenshooterError::NoRenderContext), + }; + let extents = state.root.extents.get(); + if extents.is_empty() { + return Err(ScreenshooterError::EmptyDisplay); + } + let format = ModifiedFormat { + format: XRGB8888, + modifier: INVALID_MODIFIER, + }; + let bo = ctx.gbm.create_bo( + extents.width(), + extents.height(), + &format, + GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR, + )?; + let fb = ctx.dmabuf_fb(bo.dmabuf())?; + fb.render(state.root.deref(), state, Some(state.root.extents.get())); + let drm = ctx.gbm.drm.dup_render()?.fd().clone(); + Ok(Screenshot { drm, bo }) +} diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 4fd047a5..bc82faf3 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -123,6 +123,7 @@ impl ConnectorHandler { } on.update_render_data(); self.state.root.outputs.set(self.id, on.clone()); + self.state.root.update_extents(); self.state.add_global(&global); 'outer: loop { while let Some(event) = self.data.connector.event() { diff --git a/src/theme.rs b/src/theme.rs index e912fbfd..78302fcc 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -15,6 +15,13 @@ impl Color { b: 0.8, a: 1.0, }; + + pub const BLACK: Self = Self { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, + }; } fn to_f32(c: u8) -> f32 { diff --git a/src/tree/display.rs b/src/tree/display.rs index 9813bf82..6e26fb0e 100644 --- a/src/tree/display.rs +++ b/src/tree/display.rs @@ -6,16 +6,19 @@ use { wl_seat::{NodeSeatState, WlSeatGlobal}, zwlr_layer_shell_v1::{OVERLAY, TOP}, }, + rect::Rect, + render::Renderer, tree::{ walker::NodeVisitor, FindTreeResult, FoundNode, Node, NodeId, OutputNode, SizedNode, }, utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList}, }, - std::{ops::Deref, rc::Rc}, + std::{cell::Cell, ops::Deref, rc::Rc}, }; pub struct DisplayNode { pub id: NodeId, + pub extents: Cell, pub outputs: CopyHashMap>, pub stacked: LinkedList>, pub seat_state: NodeSeatState, @@ -25,11 +28,36 @@ impl DisplayNode { pub fn new(id: NodeId) -> Self { Self { id, + extents: Default::default(), outputs: Default::default(), stacked: Default::default(), seat_state: Default::default(), } } + + pub fn update_extents(&self) { + let outputs = self.outputs.lock(); + let mut x1 = i32::MAX; + let mut y1 = i32::MAX; + let mut x2 = i32::MIN; + let mut y2 = i32::MIN; + for output in outputs.values() { + let pos = output.global.pos.get(); + x1 = x1.min(pos.x1()); + y1 = y1.min(pos.y1()); + x2 = x2.max(pos.x2()); + y2 = y2.max(pos.y2()); + } + if x1 >= x2 { + x1 = 0; + x2 = 0; + } + if y1 >= y2 { + y1 = 0; + y2 = 0; + } + self.extents.set(Rect::new(x1, y1, x2, y2).unwrap()); + } } impl SizedNode for DisplayNode { @@ -132,4 +160,8 @@ impl SizedNode for DisplayNode { fn pointer_focus(&self, seat: &Rc) { seat.set_known_cursor(KnownCursor::Default); } + + fn render(&self, renderer: &mut Renderer, x: i32, y: i32) { + renderer.render_display(self, x, y); + } } diff --git a/src/tree/output.rs b/src/tree/output.rs index 0d132837..f7e9a4a0 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -200,6 +200,7 @@ impl OutputNode { fn change_extents_(&self, rect: &Rect) { self.global.pos.set(*rect); + self.state.root.update_extents(); self.update_render_data(); if let Some(c) = self.workspace.get() { c.node_change_extents(&self.workspace_rect()); diff --git a/src/utils/oserror.rs b/src/utils/oserror.rs index aec5f32e..148c02bd 100644 --- a/src/utils/oserror.rs +++ b/src/utils/oserror.rs @@ -187,6 +187,12 @@ impl From for OsError { } } +impl Default for OsError { + fn default() -> Self { + Errno::default().into() + } +} + impl Error for OsError {} impl Display for OsError { diff --git a/src/utils/windows.rs b/src/utils/windows.rs index d3472051..f80a3708 100644 --- a/src/utils/windows.rs +++ b/src/utils/windows.rs @@ -7,6 +7,7 @@ pub trait WindowsExt { T: 'a; fn array_windows_ext<'a, const N: usize>(&'a self) -> Self::Windows<'a, N>; + fn array_chunks_ext<'a, const N: usize>(&'a self) -> &'a [[T; N]]; } impl WindowsExt for [T] { @@ -18,6 +19,11 @@ impl WindowsExt for [T] { fn array_windows_ext<'a, const N: usize>(&'a self) -> Self::Windows<'a, N> { WindowsIter { slice: self } } + + fn array_chunks_ext<'a, const N: usize>(&'a self) -> &'a [[T; N]] { + let len = self.len() / N; + unsafe { std::slice::from_raw_parts(self.as_ptr() as _, len) } + } } pub struct WindowsIter<'a, T, const N: usize> { diff --git a/src/video.rs b/src/video.rs index 647f4358..68f27e97 100644 --- a/src/video.rs +++ b/src/video.rs @@ -7,6 +7,8 @@ pub mod gbm; pub type Modifier = u64; pub const INVALID_MODIFIER: Modifier = 0x00ff_ffff_ffff_ffff; +#[allow(dead_code)] +pub const LINEAR_MODIFIER: Modifier = 0; #[derive(Copy, Clone)] pub struct ModifiedFormat { diff --git a/src/video/dmabuf.rs b/src/video/dmabuf.rs index 795074c1..e0684bef 100644 --- a/src/video/dmabuf.rs +++ b/src/video/dmabuf.rs @@ -1,11 +1,13 @@ use {crate::format::Format, std::rc::Rc, uapi::OwnedFd}; +#[derive(Clone)] pub struct DmaBufPlane { pub offset: u32, pub stride: u32, pub fd: Rc, } +#[derive(Clone)] pub struct DmaBuf { pub width: i32, pub height: i32, diff --git a/src/video/gbm.rs b/src/video/gbm.rs index 142e0844..44a1a246 100644 --- a/src/video/gbm.rs +++ b/src/video/gbm.rs @@ -1,6 +1,9 @@ +#![allow(non_camel_case_types)] + use { crate::{ format::formats, + utils::oserror::OsError, video::{ dmabuf::{DmaBuf, DmaBufPlane}, drm::{Drm, DrmError}, @@ -11,6 +14,7 @@ use { fmt::{Debug, Formatter}, ptr, rc::Rc, + slice, }, thiserror::Error, uapi::{c, OwnedFd}, @@ -28,6 +32,8 @@ pub enum GbmError { UnknownFormat, #[error("Could not retrieve a drm-buf fd")] DrmFd, + #[error("Could not map bo")] + MapBo(#[source] OsError), } pub type Device = u8; @@ -39,16 +45,42 @@ pub const GBM_BO_USE_CURSOR: u32 = 1 << 1; pub const GBM_BO_USE_RENDERING: u32 = 1 << 2; #[allow(dead_code)] pub const GBM_BO_USE_WRITE: u32 = 1 << 3; -#[allow(dead_code)] pub const GBM_BO_USE_LINEAR: u32 = 1 << 4; #[allow(dead_code)] pub const GBM_BO_USE_PROTECTED: u32 = 1 << 5; +#[allow(dead_code)] +const GBM_BO_IMPORT_WL_BUFFER: u32 = 0x5501; +#[allow(dead_code)] +const GBM_BO_IMPORT_EGL_IMAGE: u32 = 0x5502; +#[allow(dead_code)] +const GBM_BO_IMPORT_FD: u32 = 0x5503; +const GBM_BO_IMPORT_FD_MODIFIER: u32 = 0x5504; + +const GBM_BO_TRANSFER_READ: u32 = 1 << 0; +#[allow(dead_code)] +const GBM_BO_TRANSFER_WRITE: u32 = 1 << 1; +#[allow(dead_code)] +const GBM_BO_TRANSFER_READ_WRITE: u32 = GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE; + +#[repr(C)] +struct gbm_import_fd_modifier_data { + width: u32, + height: u32, + format: u32, + num_fds: u32, + fds: [c::c_int; 4], + strides: [c::c_int; 4], + offsets: [c::c_int; 4], + modifier: u64, +} + #[link(name = "gbm")] extern "C" { fn gbm_create_device(fd: c::c_int) -> *mut Device; fn gbm_device_destroy(dev: *mut Device); + fn gbm_bo_import(dev: *mut Device, ty: u32, buffer: *mut u8, flags: u32) -> *mut Bo; fn gbm_bo_create_with_modifiers2( dev: *mut Device, width: u32, @@ -71,10 +103,21 @@ extern "C" { fn gbm_bo_get_format(bo: *mut Bo) -> u32; #[allow(dead_code)] fn gbm_bo_get_bpp(bo: *mut Bo) -> u32; + fn gbm_bo_map( + bo: *mut Bo, + x: u32, + y: u32, + width: u32, + height: u32, + flags: u32, + strid: *mut u32, + map_data: *mut *mut u8, + ) -> *mut u8; + fn gbm_bo_unmap(bo: *mut Bo, map_data: *mut u8); } pub struct GbmDevice { - _drm: Drm, + pub drm: Drm, dev: *mut Device, } @@ -89,8 +132,20 @@ struct BoHolder { } pub struct GbmBo { - _bo: BoHolder, - dma: DmaBuf, + bo: BoHolder, + dmabuf: DmaBuf, +} + +pub struct GbmBoMap { + bo: Rc, + data: *mut [u8], + opaque: *mut u8, +} + +impl GbmBoMap { + pub unsafe fn data(&self) -> &[u8] { + &*self.data + } } unsafe fn export_bo(bo: *mut Bo) -> Result { @@ -132,7 +187,7 @@ impl GbmDevice { if dev.is_null() { Err(GbmError::CreateDevice) } else { - Ok(Self { _drm: drm, dev }) + Ok(Self { drm: drm, dev }) } } @@ -167,7 +222,44 @@ impl GbmDevice { } let bo = BoHolder { bo }; let dma = export_bo(bo.bo)?; - Ok(GbmBo { _bo: bo, dma }) + Ok(GbmBo { + bo: bo, + dmabuf: dma, + }) + } + } + + pub fn import_dmabuf(&self, dmabuf: &DmaBuf, usage: u32) -> Result { + let mut import = gbm_import_fd_modifier_data { + width: dmabuf.width as _, + height: dmabuf.height as _, + format: dmabuf.format.drm as _, + num_fds: dmabuf.planes.len() as _, + fds: [0; 4], + strides: [0; 4], + offsets: [0; 4], + modifier: dmabuf.modifier, + }; + for (i, plane) in dmabuf.planes.iter().enumerate() { + import.fds[i] = plane.fd.raw(); + import.strides[i] = plane.stride as _; + import.offsets[i] = plane.offset as _; + } + unsafe { + let bo = gbm_bo_import( + self.dev, + GBM_BO_IMPORT_FD_MODIFIER, + &mut import as *const _ as _, + usage, + ); + if bo.is_null() { + return Err(GbmError::CreateBo); + } + let bo = BoHolder { bo }; + Ok(GbmBo { + bo: bo, + dmabuf: dmabuf.clone(), + }) } } } @@ -181,8 +273,42 @@ impl Drop for GbmDevice { } impl GbmBo { - pub fn dma(&self) -> &DmaBuf { - &self.dma + pub fn dmabuf(&self) -> &DmaBuf { + &self.dmabuf + } + + pub fn map(self: &Rc) -> Result { + let mut stride = 0; + let mut map_data = ptr::null_mut(); + unsafe { + let map = gbm_bo_map( + self.bo.bo, + 0, + 0, + self.dmabuf.width as _, + self.dmabuf.height as _, + GBM_BO_TRANSFER_READ, + &mut stride, + &mut map_data, + ); + if map.is_null() { + return Err(GbmError::MapBo(OsError::default())); + } + let map = slice::from_raw_parts_mut(map, (stride * self.dmabuf.height as u32) as usize); + Ok(GbmBoMap { + bo: self.clone(), + data: map, + opaque: map_data, + }) + } + } +} + +impl Drop for GbmBoMap { + fn drop(&mut self) { + unsafe { + gbm_bo_unmap(self.bo.bo.bo, self.opaque); + } } } diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index 86f80afe..81e95fb9 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -14,3 +14,7 @@ msg quit = 2 { msg set_log_level = 3 { level: u32, } + +msg take_screenshot = 4 { + id: id(jay_screenshot), +} diff --git a/wire/jay_screenshot.txt b/wire/jay_screenshot.txt new file mode 100644 index 00000000..726e9d53 --- /dev/null +++ b/wire/jay_screenshot.txt @@ -0,0 +1,14 @@ +# events + +msg dmabuf = 0 { + drm_dev: fd, + fd: fd, + width: u32, + height: u32, + offset: u32, + stride: u32, +} + +msg error = 1 { + msg: str, +}