use { crate::{ cmm::cmm_eotf::Eotf, cpu_worker::{AsyncCpuWork, CpuJob, CpuWork, CpuWorker, PendingJob}, format::{ARGB8888, Format}, gfx_api::{ AsyncShmGfxTexture, AsyncShmGfxTextureCallback, GfxBuffer, GfxContext, GfxError, GfxStagingBuffer, GfxTexture, PendingShmTransfer, STAGING_UPLOAD, }, pango::{ CairoContext, CairoImageSurface, PangoCairoContext, PangoError, PangoFontDescription, PangoLayout, cairo_size, consts::{ CAIRO_FORMAT_ARGB32, CAIRO_OPERATOR_SOURCE, CairoFormat, PANGO_ELLIPSIZE_END, PANGO_SCALE, }, }, rect::{Rect, Region}, state::State, theme::Color, udmabuf::UdmabufHolder, utils::{ clonecell::CloneCell, double_buffered::DoubleBuffered, errorfmt::ErrorFmt, on_drop_event::OnDropEvent, oserror::{OsError, OsErrorExt2}, page_size::page_size, }, }, std::{ borrow::Cow, cell::{Cell, RefCell}, mem, ops::Neg, ptr, rc::{Rc, Weak}, slice, sync::{ Arc, atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering::Relaxed}, }, }, thiserror::Error, uapi::{ OwnedFd, c::{self, off_t}, ftruncate, }, }; #[derive(Debug, Error)] pub enum TextError { #[error("Could not create a cairo image")] CreateImage(#[source] PangoError), #[error("Could not create a cairo context")] CairoContext(#[source] PangoError), #[error("Could not create a pango context")] PangoContext(#[source] PangoError), #[error("Could not create a pango layout")] CreateLayout(#[source] PangoError), #[error("Texture upload failed")] Upload(#[source] GfxError), #[error("Could not create a texture")] CreateTexture(#[source] GfxError), #[error("Rendering is not scheduled or not yet completed")] NotScheduled, #[error("The size calculation overflowed")] SizeOverflow, #[error("Could not resize the memfd")] ResizeMemfd(#[source] OsError), #[error("Could not map the memfd")] MapMemfd(#[source] OsError), } impl<'a> Config<'a> { fn to_static(self) -> Config<'static> { match self { Config::None => Config::None, Config::RenderFitting { height, font, text, color, markup, scale, } => Config::RenderFitting { height, font, text: text.into_owned().into(), color, markup, scale, }, Config::RenderFittingOrEllipsized { height, max_width, font, text, color, markup, scale, } => Config::RenderFittingOrEllipsized { height, max_width, font, text: text.into_owned().into(), color, markup, scale, }, Config::Render { x, y, width, height, padding, font, text, color, ellipsize, markup, scale, } => Config::Render { x, y, width, height, padding, font, text: text.into_owned().into(), color, ellipsize, markup, scale, }, } } } struct Data { image: Rc, cctx: Rc, _pctx: Rc, _fd: PangoFontDescription, layout: PangoLayout, } const CAIRO_FORMAT: CairoFormat = CAIRO_FORMAT_ARGB32; const FORMAT: &Format = ARGB8888; fn create_data( memfd: &Memfd, font: &str, width: i32, height: i32, scale: Option, ) -> Result { let Some((stride, size)) = cairo_size(CAIRO_FORMAT, width, height) else { return Err(TextError::SizeOverflow); }; let data = memfd.get_pointer_for_size(size)?; let image = match unsafe { CairoImageSurface::new_image_surface_with_data(CAIRO_FORMAT, data, width, height, stride) } { Ok(s) => s, Err(e) => return Err(TextError::CreateImage(e)), }; let cctx = match image.create_context() { Ok(c) => c, Err(e) => return Err(TextError::CairoContext(e)), }; let pctx = match cctx.create_pango_context() { Ok(c) => c, Err(e) => return Err(TextError::PangoContext(e)), }; let mut fd = PangoFontDescription::from_string(font); if let Some(scale) = scale { fd.set_size((fd.size() as f64 * scale).round() as _); } let layout = match pctx.create_layout() { Ok(l) => l, Err(e) => return Err(TextError::CreateLayout(e)), }; layout.set_font_description(&fd); Ok(Data { image, cctx, _pctx: pctx, _fd: fd, layout, }) } fn measure( memfd: &Memfd, font: &str, text: &str, markup: bool, scale: Option, ) -> Result { let data = create_data(memfd, font, 1, 1, scale)?; if markup { data.layout.set_markup(text); } else { data.layout.set_text(text); } let mut res = TextMeasurement::default(); res.ink_rect = data.layout.inc_pixel_rect(); Ok(res) } fn render( memfd: &Memfd, x: i32, y: Option, width: i32, height: i32, padding: i32, font: &str, text: &str, color: Color, ellipsize: bool, markup: bool, scale: Option, ) -> Result { if width == 0 || height == 0 { return Ok(RenderedText { width, height, stride: width * 4, }); } let data = create_data(memfd, font, width, height, scale)?; if ellipsize { data.layout .set_width((width - 2 * padding).max(0) * PANGO_SCALE); data.layout.set_ellipsize(PANGO_ELLIPSIZE_END); } if markup { data.layout.set_markup(text); } else { data.layout.set_text(text); } let font_height = data.layout.pixel_size().1; let [r, g, b, a] = color.to_array(Eotf::Gamma22); data.cctx.set_operator(CAIRO_OPERATOR_SOURCE); data.cctx.set_source_rgba(r as _, g as _, b as _, a as _); let y = y.unwrap_or((height - font_height) / 2); data.cctx.move_to(x as f64, y as f64); data.layout.show_layout(); data.image.flush(); Ok(RenderedText { width, height, stride: data.image.stride(), }) } fn render_fitting( memfd: &Memfd, height: Option, font: &str, text: &str, color: Color, markup: bool, scale: Option, ) -> Result { let measurement = measure(memfd, font, text, markup, scale)?; let x = measurement.ink_rect.x1().neg(); let y = match height { Some(_) => None, _ => Some(measurement.ink_rect.y1().neg()), }; let width = measurement.ink_rect.width(); let height = height.unwrap_or(measurement.ink_rect.height()); render( memfd, x, y, width, height, 0, font, text, color, false, markup, scale, ) } fn render_fitting_or_ellipsized( memfd: &Memfd, height: Option, max_width: i32, font: &str, text: &str, color: Color, markup: bool, scale: Option, ) -> Result { let measurement = measure(memfd, font, text, markup, scale)?; if measurement.ink_rect.width() <= max_width { return render_fitting(memfd, height, font, text, color, markup, scale); } let height = height.unwrap_or(measurement.ink_rect.height()); render( memfd, 0, None, max_width, height, 0, font, text, color, true, markup, scale, ) } #[derive(Debug, Copy, Clone, Default)] pub struct TextMeasurement { pub ink_rect: Rect, } struct RenderedText { width: i32, height: i32, stride: i32, } struct RenderWork { memfd: Arc, config: Config<'static>, result: Option>, } struct RenderJob { work: RenderWork, data: Weak, } impl CpuWork for RenderWork { fn run(&mut self) -> Option> { self.result = Some(self.render()); None } } impl RenderWork { fn render(&mut self) -> Result { match self.config { Config::None => unreachable!(), Config::RenderFitting { height, ref font, ref text, color, markup, scale, } => render_fitting(&self.memfd, height, font, text, color, markup, scale), Config::RenderFittingOrEllipsized { height, max_width, ref font, ref text, color, markup, scale, } => render_fitting_or_ellipsized( &self.memfd, height, max_width, font, text, color, markup, scale, ), Config::Render { x, y, width, height, padding, ref font, ref text, color, ellipsize, markup, scale, } => render( &self.memfd, x, y, width, height, padding, font, text, color, ellipsize, markup, scale, ), } } } pub struct TextTexture { data: Rc, } impl Drop for TextTexture { fn drop(&mut self) { if let Some(pending) = self.data.pending_render.take() { pending.detach(); } self.data.pending_upload.take(); self.data.render_job.take(); self.data.waiter.take(); } } struct Shared { cpu_worker: Rc, ctx: Rc, udmabuf: Rc, staging: CloneCell>>, textures: DoubleBuffered, pending_render: Cell>, pending_upload: Cell>, render_job: Cell>>, result: Cell>>, waiter: Cell>>, busy: Cell, flip_is_noop: Cell, memfd: Arc, gfx_buffer: CloneCell>>>, } struct Memfd { fd: OwnedFd, size: AtomicUsize, size_changed: AtomicBool, mapping: AtomicPtr, } impl Shared { fn complete(&self, res: Result<(), TextError>) { if res.is_err() { self.textures.back().config.take(); } self.busy.set(false); self.result.set(Some(res)); if let Some(waiter) = self.waiter.take() { waiter.completed(); } } fn get_gfx_buffer(&self) -> Option> { if self.memfd.size_changed.load(Relaxed) { self.gfx_buffer.take(); self.memfd.size_changed.store(false, Relaxed); } if let Some(res) = self.gfx_buffer.get() { return res; } let size = self.memfd.size.load(Relaxed); let udmabuf = self.udmabuf.get()?; let res = 'res: { let dmabuf = match udmabuf.create_dmabuf_from_memfd(&self.memfd.fd, 0, size) { Ok(b) => b, Err(e) => { log::error!("Could not create udmabuf: {}", ErrorFmt(e)); break 'res None; } }; match self.ctx.create_dmabuf_buffer(&dmabuf, 0, size, FORMAT) { Ok(b) => Some(b), Err(e) => { log::debug!("Could not create GfxBuffer: {}", ErrorFmt(e)); None } } }; self.gfx_buffer.set(Some(res.clone())); res } } #[allow(dead_code)] #[derive(PartialEq, Default)] enum Config<'a> { #[default] None, RenderFitting { height: Option, font: Arc, text: Cow<'a, str>, color: Color, markup: bool, scale: Option, }, RenderFittingOrEllipsized { height: Option, max_width: i32, font: Arc, text: Cow<'a, str>, color: Color, markup: bool, scale: Option, }, Render { x: i32, y: Option, width: i32, height: i32, padding: i32, font: Arc, text: Cow<'a, str>, color: Color, ellipsize: bool, markup: bool, scale: Option, }, } #[derive(Default)] struct TextBuffer { config: RefCell>, tex: CloneCell>>, } pub trait OnCompleted { fn completed(self: Rc); } impl TextTexture { pub fn new(state: &Rc, ctx: &Rc) -> Self { let memfd = uapi::memfd_create("text", c::MFD_CLOEXEC | c::MFD_ALLOW_SEALING) .expect("Could not create memfd"); let _ = uapi::fcntl_add_seals(memfd.raw(), c::F_SEAL_SHRINK); let data = Rc::new(Shared { cpu_worker: state.cpu_worker.clone(), ctx: ctx.clone(), udmabuf: state.udmabuf.clone(), staging: Default::default(), textures: Default::default(), pending_render: Default::default(), pending_upload: Default::default(), render_job: Default::default(), result: Default::default(), waiter: Default::default(), busy: Default::default(), flip_is_noop: Default::default(), memfd: Arc::new(Memfd { fd: memfd, size: Default::default(), size_changed: Default::default(), mapping: Default::default(), }), gfx_buffer: Default::default(), }); Self { data } } pub fn texture(&self) -> Option> { self.data.textures.front().tex.get().map(|t| t as _) } fn apply_config(&self, on_completed: Rc, config: Config<'_>) { if self.data.busy.replace(true) { unreachable!(); } self.data.waiter.set(Some(on_completed)); self.data.flip_is_noop.set(false); if *self.data.textures.front().config.borrow() == config { self.data.flip_is_noop.set(true); self.data.complete(Ok(())); return; } if *self.data.textures.back().config.borrow() == config { self.data.complete(Ok(())); return; } let mut job = self.data.render_job.take().unwrap_or_else(|| { Box::new(RenderJob { work: RenderWork { memfd: self.data.memfd.clone(), config: Default::default(), result: Default::default(), }, data: Rc::downgrade(&self.data), }) }); job.work = RenderWork { config: config.to_static(), result: None, ..job.work }; let pending = self.data.cpu_worker.submit(job); self.data.pending_render.set(Some(pending)); } #[allow(dead_code)] pub fn schedule_render( &self, on_completed: Rc, x: i32, y: Option, width: i32, height: i32, padding: i32, font: &Arc, text: &str, color: Color, ellipsize: bool, markup: bool, scale: Option, ) { let config = Config::Render { x, y, width, height, padding, font: font.clone(), text: Cow::Borrowed(text), color, ellipsize, markup, scale, }; self.apply_config(on_completed, config) } pub fn schedule_render_fitting( &self, on_completed: Rc, height: Option, font: &Arc, text: &str, color: Color, markup: bool, scale: Option, ) { let config = Config::RenderFitting { height, font: font.clone(), text: text.into(), color, markup, scale, }; self.apply_config(on_completed, config) } pub fn schedule_render_fitting_or_ellipsized( &self, on_completed: Rc, height: Option, max_width: i32, font: &Arc, text: &str, color: Color, markup: bool, scale: Option, ) { let config = Config::RenderFittingOrEllipsized { height, max_width, font: font.clone(), text: text.into(), color, markup, scale, }; self.apply_config(on_completed, config) } pub fn flip(&self) -> Result<(), TextError> { let res = self .data .result .take() .unwrap_or(Err(TextError::NotScheduled)); if res.is_ok() && !self.data.flip_is_noop.get() { self.data.textures.flip(); } res } } impl CpuJob for RenderJob { fn work(&mut self) -> &mut dyn CpuWork { &mut self.work } fn completed(mut self: Box) { let Some(data) = self.data.upgrade() else { return; }; let result = self.work.result.take().unwrap(); *data.textures.back().config.borrow_mut() = mem::take(&mut self.work.config); data.render_job.set(Some(self)); let rt = match result { Ok(d) => d, Err(e) => { data.complete(Err(e)); return; } }; let mut tex = data.textures.back().tex.take(); if rt.width == 0 || rt.height == 0 { data.complete(Ok(())); return; } if let Some(t) = &tex && !t.compatible_with(FORMAT, rt.width, rt.height, rt.stride) { tex = None; } let tex = match tex { Some(t) => t, _ => { let tex = data .ctx .clone() .async_shmem_texture(FORMAT, rt.width, rt.height, rt.stride, &data.cpu_worker) .map_err(TextError::CreateTexture); match tex { Ok(t) => t, Err(e) => { data.complete(Err(e)); return; } } } }; let mut staging_opt = data.staging.take(); let pending = if let Some(gfx_buffer) = data.get_gfx_buffer() { tex.clone() .async_upload_from_buffer( &gfx_buffer, data.clone(), Region::new(Rect::new_sized_saturating(0, 0, rt.width, rt.height)), ) .map_err(TextError::Upload) } else { if let Some(staging) = &staging_opt && staging.size() != tex.staging_size() { staging_opt = None; } let staging = staging_opt.get_or_insert_with(|| { data.ctx .create_staging_buffer(tex.staging_size(), STAGING_UPLOAD) }); tex.clone() .async_upload( &staging, data.clone(), Rc::new(data.memfd.data(rt.stride, rt.height)), Region::new(Rect::new_sized_saturating(0, 0, rt.width, rt.height)), ) .map_err(TextError::Upload) }; if pending.is_ok() { data.textures.back().tex.set(Some(tex)); data.staging.set(staging_opt); } match pending { Ok(Some(p)) => data.pending_upload.set(Some(p)), Ok(None) => data.complete(Ok(())), Err(e) => data.complete(Err(e)), } } } impl AsyncShmGfxTextureCallback for Shared { fn completed(self: Rc, res: Result<(), GfxError>) { self.pending_upload.take(); self.complete(res.map_err(TextError::Upload)); } } impl OnCompleted for OnDropEvent { fn completed(self: Rc) { // nothing } } impl Memfd { fn get_pointer_for_size(&self, size: usize) -> Result<*mut u8, TextError> { let old_size = self.size.load(Relaxed); if old_size >= size { return Ok(self.mapping.load(Relaxed)); } let Some(size) = size.checked_next_multiple_of(page_size()) else { return Err(TextError::SizeOverflow); }; let Ok(isize) = off_t::try_from(size) else { return Err(TextError::SizeOverflow); }; ftruncate(self.fd.raw(), isize).map_os_err(TextError::ResizeMemfd)?; let old_ptr = self.mapping.load(Relaxed); let new_ptr = if old_ptr.is_null() { unsafe { c::mmap( ptr::null_mut(), size, c::PROT_READ | c::PROT_WRITE, c::MAP_SHARED, self.fd.raw(), 0, ) } } else { unsafe { c::mremap(old_ptr.cast(), old_size, size, c::MREMAP_MAYMOVE) } }; if new_ptr == c::MAP_FAILED { return Err(TextError::MapMemfd(OsError::default())); } let new_ptr = new_ptr.cast(); self.mapping.store(new_ptr, Relaxed); self.size.store(size, Relaxed); self.size_changed.store(true, Relaxed); Ok(new_ptr) } fn data(&self, stride: i32, height: i32) -> Vec> { let size = (stride * height) as usize; assert!(size <= self.size.load(Relaxed)); if size == 0 { return vec![]; } let mapping = self.mapping.load(Relaxed); unsafe { slice::from_raw_parts(mapping.cast(), size).to_vec() } } } impl Drop for Memfd { fn drop(&mut self) { let ptr = self.mapping.load(Relaxed); if ptr.is_null() { return; } unsafe { c::munmap(ptr.cast(), self.size.load(Relaxed)); } } }