Files
ghostty-rs/crates/ghostty-renderer/src/generic.rs
T
ddidderr 79a7fd4f76 fix(config): match Zig's numeric grammar, NUL handling and diagnostics
Resolves the 6 differential gap groups in docs/rewrite/notes/review-
rv9-codex.md (details in final-Z11.md):
- ghostty_os::zig_fmt ports std.fmt.parseInt/parseUnsigned/parseFloat
  (underscores, 0x/0o/0b prefixes, hex floats, inf/nan) with the Zig std
  tests and replaces every int/float parse in config, keybind
  parameters, ssh-cache and desktop detection.
- Interior NUL no longer panics in font names and ends C strings like
  Zig's backing bytes (fontconfig, C config getters, title actions,
  clipboard callback, relative paths, config-file, shaders, background
  image, GTK CSS).
- GTK new-window arguments are trimmed of ASCII whitespace only.
- Command tokenization uses a port of std.process.ArgIteratorGeneral
  (incl. CR/LF separators) with its std tests.
- UTF-8 validation happens where Zig's field parsers do it, so error
  kinds match (metric modifiers, quick-terminal size, link, ...).

A scripted differential run of 202 fields x ~220 hostile/valid values
against /usr/bin/ghostty (+validate-config and +show-config: exit code,
stdout, stderr) now has zero mismatches besides build-specific defaults
(theme resources path, auto-update-channel, gtk-opengl-debug); it also
surfaced and fixed background-blur failure state, NaN formatting and
NaN/-0 finalize clamps. Regression tests: ghostty-config/tests/
zig_parity.rs.

Test Plan:
- cargo test --workspace --no-fail-fast (3456 passed, exit 0)
- just check-macos (both), just check-windows, just check-wasm
2026-10-12 12:35:37 +02:00

3695 lines
135 KiB
Rust

//! The generic renderer (`renderer/generic.zig`), instantiated for the
//! OpenGL graphics API (`renderer.zig`: `pub const Renderer = GenericRenderer(OpenGL)`).
//!
//! The generic renderer builds the GPU cell buffers from a
//! [`ghostty_terminal::render::RenderState`] and draws frames.
//!
//! # Threading model
//!
//! Like Ghostty on GTK (`must_draw_from_app_thread = true`), the work is split
//! between two threads:
//!
//! * The **renderer thread** ([`crate::thread`]) owns the "update" half:
//! [`Renderer::update_frame`] snapshots the terminal under the renderer
//! mutex ([`crate::state::SharedState`]), shapes text, renders glyphs into the
//! font atlases and rebuilds the CPU copies of the cell buffers. It never
//! issues a GL call.
//! * The **apprt main thread** (the one the `GtkGLArea` context is current on)
//! owns the "draw" half: [`Renderer::draw_frame`] uploads buffers, atlases,
//! images and the background image, then issues all GL calls. The renderer
//! thread merely *requests* a draw through [`Host::redraw_surface`]; the
//! apprt answers by calling `queue_render` on the GLArea whose `render`
//! signal handler calls `draw_frame`.
//!
//! The renderer state is split to make that safe without `unsafe`:
//!
//! * `draw` ([`DrawState`], Zig's `draw_mutex`): everything `draw_frame`
//! reads. Held by the renderer thread only for the short "publish" part of
//! `update_frame` (`rebuild_cells`, uniforms) and by `set_*`/`change_config`.
//! * `thread` ([`ThreadState`]): data only the renderer thread touches
//! (`RenderState`, font shaper + cache, overlay, search highlights). It is a
//! `Mutex` only so that `Renderer` is `Sync`; it is uncontended.
//!
//! Lock order is always: renderer mutex (`SharedState`) is *not* held while
//! taking `draw`; `thread` before `draw` before the font grid lock.
//!
//! # GL context requirements
//!
//! Everything that creates, uses or destroys GL objects ([`Renderer::new`],
//! [`Renderer::draw_frame`], [`Renderer::display_realized`],
//! [`Renderer::display_unrealized`], dropping the `Renderer`) must run on the
//! thread with the GL context current. The renderer thread never does.
use std::{
collections::HashSet,
sync::{
Arc,
Condvar,
Mutex,
MutexGuard,
PoisonError,
atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
},
time::Instant,
};
use ghostty_config::{
config::Config,
path::Path as ConfigPath,
types_a::{BoldColor, FontShapingBreak, TerminalColor, WindowPaddingColor},
types_b::{
AlphaBlending,
BackgroundBlur,
BackgroundImageFit,
BackgroundImagePosition,
WindowColorspace,
},
};
use ghostty_font::{
Atlas,
Presentation,
SPRITE_INDEX,
SharedGrid,
Sprite,
Style as FontStyle,
face::{Constraint, ConstraintSize, RenderOptions},
nerd_font_attributes::get_constraint,
shape::{self, Cache as ShaperCache, RunIterator, RunOptions, TextRun},
shaper::run::RunCell,
};
use ghostty_input::Mods;
use ghostty_terminal::{
highlight::Flattened,
page::{self, Cell as PageCell, Wide},
page_list::Scrollbar,
point::{Coordinate, Tag},
render::{Cursor as RenderCursor, Dirty, RenderState, Row as RenderRow},
style::{self, Style},
terminal::ScrollViewport,
};
use ghostty_vt::{color::Rgb, modes::Mode, sgr::Underline};
use crate::{
backend::Health,
cell::{self, Contents, Key as CellKey},
cursor::{self, Style as CursorStyle},
display_link::DisplayLink,
graphics_api::{
ApiError,
Attachment,
AttachmentTarget,
Draw,
FrameCompletion,
GpuBuffer,
GpuFrame,
GpuRenderPass,
GpuShaders,
GpuTarget,
GpuTexture,
GraphicsApi,
Primitive,
Step,
TextureKind,
},
image::{self, Gpu},
link,
math,
opengl::OpenGL,
options::Options,
overlay::{self, Overlay},
row::never_extend_bg,
shader_types::{
BgImage,
BgImageFit,
BgImageInfo,
BgImagePosition,
Bools,
CellBg,
CellText,
CellTextAtlas,
PaddingExtend,
Uniforms,
},
shadertoy::{self, PathItem},
size::{CellSize, GridSize, Padding, Size},
state::{Preedit, SharedState},
};
/// The graphics API used by the renderer on Linux.
/// Errors surfaced by the generic renderer.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Api(#[from] ApiError),
#[error(transparent)]
Shaper(#[from] ghostty_font::shape::ShapeError),
#[error(transparent)]
ShapeRun(#[from] ghostty_font::shape::ShapeRunError),
#[error(transparent)]
Grid(#[from] ghostty_font::shared_grid::Error),
#[error(transparent)]
Link(#[from] link::Error),
#[error("terminal state error: {0}")]
Terminal(page::Error),
#[error("the swap chain is defunct (display unrealized)")]
Defunct,
}
/// What the renderer needs from the surface/apprt (Zig: `surface_mailbox`
/// and the `redraw_surface` app message). Implementations must not block for
/// long: `scrollbar` and `redraw_surface` are "instant" pushes in Zig.
pub trait Host: Send + Sync {
/// `surface_mailbox.push(.{ .scrollbar = .. }, .instant)`. Returns true if
/// the message was accepted (otherwise the renderer retries next frame).
fn scrollbar(&self, scrollbar: Scrollbar) -> bool;
/// `surface_mailbox.push(.{ .renderer_health = .. }, .forever)`.
fn renderer_health(&self, health: Health);
/// Ask the apprt to call [`Renderer::draw_frame`] from its main thread
/// (`app_mailbox.push(.{ .redraw_surface = surface }, .instant)`). Called
/// from the renderer thread because GTK must draw on the main thread
/// (`must_draw_from_app_thread = true`).
fn redraw_surface(&self) -> bool;
}
/// A host that discards everything. Useful for tests and headless use.
#[derive(Debug, Default, Clone, Copy)]
pub struct NullHost;
impl Host for NullHost {
fn scrollbar(&self, _: Scrollbar) -> bool {
true
}
fn renderer_health(&self, _: Health) {}
fn redraw_surface(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum HighlightTag {
SearchMatch = 0,
SearchMatchSelected = 1,
}
/// The configuration for this renderer that is derived from the main
/// configuration (`DerivedConfig`). Owned and cheap to send in messages.
#[derive(Clone)]
pub struct DerivedConfig {
pub font_thicken: bool,
pub font_thicken_strength: u8,
pub font_features: Vec<ghostty_os::osbytes::EncodedOs>,
pub font_styles: [bool; 4],
pub font_shaping_break: FontShapingBreak,
pub cursor_color: Option<TerminalColor>,
pub cursor_opacity: f64,
pub cursor_text: Option<TerminalColor>,
pub background: Rgb,
pub background_opacity: f64,
pub background_opacity_cells: bool,
pub foreground: Rgb,
pub selection_background: Option<TerminalColor>,
pub selection_foreground: Option<TerminalColor>,
pub search_background: TerminalColor,
pub search_foreground: TerminalColor,
pub search_selected_background: TerminalColor,
pub search_selected_foreground: TerminalColor,
pub bold_color: Option<BoldColor>,
pub faint_opacity: u8,
pub min_contrast: f32,
pub padding_color: WindowPaddingColor,
pub custom_shaders: Vec<PathItem>,
pub bg_image: Option<ConfigPath>,
pub bg_image_opacity: f32,
pub bg_image_position: BackgroundImagePosition,
pub bg_image_fit: BackgroundImageFit,
pub bg_image_repeat: bool,
pub links: Arc<link::Set>,
pub vsync: bool,
pub colorspace: WindowColorspace,
pub blending: AlphaBlending,
pub background_blur: BackgroundBlur,
pub scroll_to_bottom_on_output: bool,
}
impl DerivedConfig {
pub fn new(config: &Config) -> Result<DerivedConfig, link::Error> {
use ghostty_config::types_a::FontStyle as ConfigFontStyle;
// Copy our shaders
let custom_shaders = config
.custom_shader
.value
.iter()
.filter_map(|p| {
let path = match p.path().to_syscall_path() {
Ok(path) => path,
Err(error) => {
log::warn!("invalid shader path: {error}");
return None;
}
};
Some(match p {
ConfigPath::Optional(_) => PathItem::Optional(path),
ConfigPath::Required(_) => PathItem::Required(path),
})
})
.collect();
// Get our font styles (index order: regular, bold, italic, bold_italic)
let font_styles = [
true,
config.font_style_bold != ConfigFontStyle::False,
config.font_style_italic != ConfigFontStyle::False,
config.font_style_bold_italic != ConfigFontStyle::False,
];
// Our link configs
let links = link::Set::from_config(&config.link.links)?;
Ok(DerivedConfig {
background_opacity: config.background_opacity.clamp(0.0, 1.0),
background_opacity_cells: config.background_opacity_cells,
font_thicken: config.font_thicken,
font_thicken_strength: config.font_thicken_strength,
font_features: config.font_feature.list.clone(),
font_styles,
font_shaping_break: config.font_shaping_break,
cursor_color: config.cursor_color,
cursor_text: config.cursor_text,
cursor_opacity: config.cursor_opacity.clamp(0.0, 1.0),
background: config.background.to_terminal_rgb(),
foreground: config.foreground.to_terminal_rgb(),
bold_color: config.bold_color,
faint_opacity: (config.faint_opacity * 255.0).ceil().clamp(0.0, 255.0) as u8,
min_contrast: config.minimum_contrast as f32,
padding_color: config.window_padding_color,
selection_background: config.selection_background,
selection_foreground: config.selection_foreground,
search_background: config.search_background,
search_foreground: config.search_foreground,
search_selected_background: config.search_selected_background,
search_selected_foreground: config.search_selected_foreground,
custom_shaders,
bg_image: config.background_image.clone(),
bg_image_opacity: config.background_image_opacity,
bg_image_position: config.background_image_position,
bg_image_fit: config.background_image_fit,
bg_image_repeat: config.background_image_repeat,
links: Arc::new(links),
vsync: config.window_vsync,
colorspace: config.window_colorspace,
blending: config.alpha_blending,
background_blur: config.background_blur,
scroll_to_bottom_on_output: config.scroll_to_bottom.output,
})
}
}
impl std::fmt::Debug for DerivedConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DerivedConfig").finish_non_exhaustive()
}
}
// ---------------------------------------------------------------------------
// GPU frame state
/// State we need duplicated for every frame. Any state that could be in a
/// data race between the GPU and CPU while a frame is being drawn should be
/// in this struct.
///
/// With OpenGL `swap_chain_count == 1` and frames complete synchronously
/// (`glFinish`), so the semaphore of the Zig swap chain is not needed: the
/// draw mutex already serializes `draw_frame`.
struct FrameState<G: GraphicsApi> {
uniforms: G::Buffer<G::GpuUniforms>,
cells: G::Buffer<CellText>,
cells_bg: G::Buffer<CellBg>,
grayscale: G::Texture,
grayscale_modified: usize,
color: G::Texture,
color_modified: usize,
target: Arc<G::Target>,
/// See property of same name on Renderer for explanation.
target_config_modified: usize,
/// Buffer with the vertex data for our background image.
bg_image_buffer: G::Buffer<BgImage>,
/// See property of same name on Renderer for explanation.
bg_image_buffer_modified: usize,
/// Custom shader state, this is None if we have no custom shaders.
custom_shader_state: Option<CustomShaderState<G>>,
/// The swap chain's atlas epoch this frame state last synced with.
atlas_epoch: usize,
}
impl<G: GraphicsApi> FrameState<G> {
fn new(api: &G, custom_shaders: bool) -> Result<FrameState<G>, Error> {
// Uniform buffer contains exactly 1 uniform struct. The
// uniform data will be zeroed so this must be set before
// a frame is drawn.
let uniforms = api.init_buffer::<G::GpuUniforms>(1)?;
// Create GPU buffers for our cells.
//
// We start them off with a size of 1, which will of course be
// too small, but they will be resized as needed.
let cells = api.init_buffer::<CellText>(1)?;
let cells_bg = api.init_buffer::<CellBg>(1)?;
// Create a GPU buffer for our background image info.
let bg_image_buffer = api.init_buffer::<BgImage>(1)?;
// Initialize our textures for our font atlas.
//
// As with the buffers above, we start these off as small
// as possible since they'll inevitably be resized anyway.
//
// (2x2 rather than Zig's 1x1: `Atlas::new` keeps a 1px border so the
// smallest valid atlas is 2px; the texture is replaced on first sync.)
let grayscale =
api.init_atlas_texture(&Atlas::new(2, ghostty_font::atlas::Format::Grayscale))?;
let color = api.init_atlas_texture(&Atlas::new(2, ghostty_font::atlas::Format::Bgra))?;
let custom_shader_state = if custom_shaders {
Some(CustomShaderState::new(api)?)
} else {
None
};
// Initialize the target. Just as with the other resources,
// start it off as small as we can since it'll be resized.
let target = Arc::new(api.init_target(1, 1)?);
Ok(FrameState {
uniforms,
cells,
cells_bg,
grayscale,
grayscale_modified: 0,
color,
color_modified: 0,
target,
target_config_modified: 0,
bg_image_buffer,
bg_image_buffer_modified: 0,
custom_shader_state,
atlas_epoch: 0,
})
}
fn resize(&mut self, api: &G, width: usize, height: usize) -> Result<(), Error> {
if let Some(state) = &mut self.custom_shader_state {
state.resize(api, width, height)?;
}
self.target = Arc::new(api.init_target(width, height)?);
Ok(())
}
}
/// State relevant to our custom shaders if we have any.
struct CustomShaderState<G: GraphicsApi> {
/// When we have a custom shader state, we maintain a front
/// and back texture which we use as a swap chain to render
/// between when multiple custom shaders are defined.
front_texture: G::Texture,
back_texture: G::Texture,
/// Shadertoy uses a sampler for accessing the various channel textures.
/// Samplers are immutable and describe sampling properties so we can
/// share the sampler across front/back textures.
sampler: G::Sampler,
uniforms: G::Buffer<shadertoy::Uniforms>,
}
impl<G: GraphicsApi> CustomShaderState<G> {
fn new(api: &G) -> Result<CustomShaderState<G>, Error> {
// Create a GPU buffer to hold our uniforms.
let uniforms = api.init_buffer::<shadertoy::Uniforms>(1)?;
// Initialize the front and back textures at 1x1 px, this
// is slightly wasteful but it's only done once so whatever.
let front_texture = api.init_texture(TextureKind::CustomShader, 1, 1, None)?;
let back_texture = api.init_texture(TextureKind::CustomShader, 1, 1, None)?;
let sampler = api.init_sampler()?;
Ok(CustomShaderState {
front_texture,
back_texture,
sampler,
uniforms,
})
}
/// Swap the front and back textures.
fn swap(&mut self) {
std::mem::swap(&mut self.front_texture, &mut self.back_texture);
}
fn resize(&mut self, api: &G, width: usize, height: usize) -> Result<(), Error> {
self.front_texture = api.init_texture(TextureKind::CustomShader, width, height, None)?;
self.back_texture = api.init_texture(TextureKind::CustomShader, width, height, None)?;
Ok(())
}
}
/// The frame slots of a [`SwapChain`], shared with the frame completion
/// callbacks (which may run on another thread and hand the frame state back).
struct SwapChainInner<G: GraphicsApi> {
/// `None` while the slot's frame is in flight (or being drawn).
slots: Mutex<Vec<Option<FrameState<G>>>>,
/// Signalled when a frame is returned (Zig: `frame_sema.post`).
returned: Condvar,
/// Bumped to force every frame's atlas textures to re-sync, including
/// the ones that are in flight right now (see `FrameState::atlas_epoch`).
atlas_epoch: AtomicUsize,
}
/// Swap chain which maintains multiple copies of the state needed to render
/// a frame, so that we can start building the next frame while the previous
/// frame is still being processed on the GPU (always one for OpenGL, see
/// [`FrameState`]; three for Metal).
///
/// Zig guards the frames with a semaphore; here taking a frame waits (on a
/// condition variable) until its slot has been handed back by the frame's
/// completion callback.
struct SwapChain<G: GraphicsApi> {
inner: Arc<SwapChainInner<G>>,
/// Index of the most recently used frame state struct.
frame_index: usize,
/// Set to true when deinited, so that a defunct swap chain is ignored
/// instead of being used or freed twice (`displayUnrealized`).
defunct: bool,
}
impl<G: GraphicsApi> SwapChain<G> {
fn new(api: &G, custom_shaders: bool) -> Result<SwapChain<G>, Error> {
let mut frames = Vec::with_capacity(G::SWAP_CHAIN_COUNT);
for _ in 0..G::SWAP_CHAIN_COUNT {
frames.push(Some(FrameState::new(api, custom_shaders)?));
}
Ok(SwapChain {
inner: Arc::new(SwapChainInner {
slots: Mutex::new(frames),
returned: Condvar::new(),
atlas_epoch: AtomicUsize::new(0),
}),
frame_index: 0,
defunct: false,
})
}
fn deinit(&mut self) {
if self.defunct {
return;
}
self.defunct = true;
// Wait for all of our inflight draws to complete
// so that we can cleanly deinit our GPU state.
let mut slots = lock(&self.inner.slots);
while slots.iter().any(Option::is_none) {
slots = self
.inner
.returned
.wait(slots)
.unwrap_or_else(PoisonError::into_inner);
}
slots.clear();
}
/// Get the next frame state to draw to. This waits until the frame is
/// available. It must always be paired with a [`SwapChain::release_frame`]
/// (directly, or through the frame completion callback).
fn next_frame(&mut self) -> Result<(usize, FrameState<G>), Error> {
if self.defunct {
return Err(Error::Defunct);
}
let mut slots = lock(&self.inner.slots);
if slots.is_empty() {
return Err(Error::Defunct);
}
self.frame_index = (self.frame_index + 1) % slots.len();
let idx = self.frame_index;
let mut frame = loop {
if let Some(frame) = slots[idx].take() {
break frame;
}
slots = self
.inner
.returned
.wait(slots)
.unwrap_or_else(PoisonError::into_inner);
};
drop(slots);
// Update all our textures so that they sync on the next frame, if
// the font grid changed since this frame state was last used.
let epoch = self.inner.atlas_epoch.load(Ordering::SeqCst);
if frame.atlas_epoch != epoch {
frame.atlas_epoch = epoch;
frame.grayscale_modified = 0;
frame.color_modified = 0;
}
Ok((idx, frame))
}
/// A handle that hands a frame state back to its slot.
fn releaser(&self) -> Arc<SwapChainInner<G>> {
Arc::clone(&self.inner)
}
fn release_frame(&self, idx: usize, frame: FrameState<G>) {
SwapChainInner::release(&self.inner, idx, frame);
}
/// Make every frame re-upload its atlas textures on its next use.
fn invalidate_atlases(&self) {
self.inner.atlas_epoch.fetch_add(1, Ordering::SeqCst);
}
}
impl<G: GraphicsApi> SwapChainInner<G> {
/// Hand a frame state back to its slot (Zig: `releaseFrame`).
fn release(this: &Arc<Self>, idx: usize, frame: FrameState<G>) {
let mut slots = lock(&this.slots);
if let Some(slot) = slots.get_mut(idx) {
*slot = Some(frame);
}
drop(slots);
this.returned.notify_all();
}
}
/// The background image state (`bg_image: ?imagepkg.Image`).
type BgImageState<G> = Option<image::Image<G>>;
// ---------------------------------------------------------------------------
// Renderer state
/// State protected by the draw mutex: everything `draw_frame` reads.
struct DrawState<G: GraphicsApi> {
/// The configuration we need derived from the main config.
config: DerivedConfig,
/// Current font metrics defining our grid.
grid_metrics: ghostty_font::Metrics,
/// The size of everything.
size: Size,
/// True if the window is focused
focused: bool,
/// Flag to indicate that our focus state changed for custom
/// shaders to update their state.
custom_shader_focused_changed: bool,
/// The most recent scrollbar state. We use this as a cache to
/// determine if we need to notify the apprt that there was a
/// scrollbar change.
scrollbar: Scrollbar,
scrollbar_dirty: bool,
/// The current set of cells to render. This is rebuilt on every frame
/// but we keep this around so that we don't reallocate. Each set of
/// cells goes into a separate shader.
cells: Contents,
/// Set to true after rebuild_cells is called. This can be used
/// to determine if any possible changes have been made to the
/// cells for the draw call.
cells_rebuilt: bool,
/// The current GPU uniform values.
uniforms: Uniforms,
/// Custom shader uniform values.
custom_shader_uniforms: shadertoy::Uniforms,
/// Timestamp we rendered out first frame.
first_frame_time: Option<Instant>,
/// Timestamp when we rendered out more recent frame.
last_frame_time: Option<Instant>,
/// The font structures.
font_grid: Arc<SharedGrid>,
/// The images that we may render.
images: image::State<G>,
/// True when a debug overlay image is currently present (Zig: `overlay != null`).
has_overlay: bool,
/// Background image, if we have one.
bg_image: BgImageState<G>,
/// Background image vertex buffer.
bg_image_buffer: BgImage,
/// This value is used to force-update the swap chain copy
/// of the background image buffer whenever we change it.
bg_image_buffer_modified: usize,
/// Graphics API state.
api: G,
/// Our swap chain (multiple buffering)
swap_chain: SwapChain<G>,
/// This value is used to force-update swap chain targets in the
/// event of a config change that requires it (such as blending mode).
target_config_modified: usize,
/// If something happened that requires us to reinitialize our shaders,
/// this is set to true so that we can do that whenever possible.
reinitialize_shaders: bool,
/// Whether or not we have custom shaders.
has_custom_shaders: bool,
/// Our shader pipelines. None while defunct (display unrealized).
shaders: Option<G::Shaders>,
}
/// State only touched by the renderer thread.
struct ThreadState {
/// The thread's copy of the derived config (kept in sync with
/// `DrawState::config`).
config: DerivedConfig,
/// The render state we update per loop.
terminal_state: RenderState,
/// The number of frames since the last terminal state reset.
/// We reset the terminal state after ~100,000 frames (about 10 to
/// 15 minutes at 120Hz) to prevent wasted memory buildup from
/// a large screen.
terminal_state_frame_count: usize,
/// Tracks the last bottom-right pin of the screen to detect new output.
/// Used for scroll-to-bottom on output feature.
last_bottom: Option<(ghostty_terminal::page_list::NodeId, u16)>,
/// The most recent viewport matches so that we can render search
/// matches in the visible frame. This is provided asynchronously
/// from the search thread so we have the dirty flag to also note
/// if we need to rebuild our cells to include search highlights.
///
/// Note that the selections MAY BE INVALID (point to PageList nodes
/// that do not exist anymore). These are validated by the render state.
search_matches: Option<Vec<Flattened>>,
search_selected_match: Option<Flattened>,
search_matches_dirty: bool,
/// Our overlay state, if any.
overlay: Option<Overlay>,
font_shaper: shape::Shaper,
font_shaper_cache: ShaperCache,
/// Scratch buffers reused while rebuilding rows.
run_cells: Vec<RunCell>,
shaped_cells: Vec<shape::Cell>,
}
/// The generic renderer. See the module docs for the threading model.
pub struct Renderer<G: GraphicsApi = OpenGL> {
draw: Mutex<DrawState<G>>,
thread: Mutex<ThreadState>,
/// Health of the most recently completed frame.
health: Arc<AtomicU8>,
/// Mirrors `DrawState::has_custom_shaders` so the renderer thread can
/// read it without the draw lock (`hasAnimations`).
has_animations: AtomicBool,
/// The CVDisplayLink used to drive the rendering loop in sync with the
/// display. Always `None` on platforms that don't support a display link
/// (everything but macOS).
display_link: Mutex<Option<DisplayLink>>,
/// The surface/apprt callbacks.
host: Arc<dyn Host>,
}
fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(PoisonError::into_inner)
}
/// How far a [`PreeditRange`] reaches within a row.
struct PreeditRange {
y: u16,
x: [u16; 2],
cp_offset: usize,
}
/// Load the custom shaders (`initShaders`). Errors are logged and result in
/// no custom shaders.
fn load_custom_shaders<G: GraphicsApi>(config: &DerivedConfig) -> Vec<String> {
match shadertoy::load_from_files(&config.custom_shaders, G::CUSTOM_SHADER_TARGET) {
Ok(v) => v,
Err(err) => {
log::warn!("error loading custom shaders err={err}");
Vec::new()
}
}
}
impl<G: GraphicsApi> DrawState<G> {
/// Rebuild the shader pipelines (`initShaders`).
fn init_shaders(&mut self) -> Result<(), Error> {
// Load our custom shaders
let custom_shaders = load_custom_shaders::<G>(&self.config);
let has_custom_shaders = !custom_shaders.is_empty();
let shaders = self.api.init_shaders(&custom_shaders)?;
self.shaders = Some(shaders);
self.has_custom_shaders = has_custom_shaders;
Ok(())
}
/// Update uniforms that are based on the font grid.
fn update_font_grid_uniforms(&mut self) {
self.uniforms.cell_size = [
self.grid_metrics.cell_width as f32,
self.grid_metrics.cell_height as f32,
];
}
/// Update uniforms that are based on the screen size.
fn update_screen_size_uniforms(&mut self) {
let terminal_size = self.size.terminal();
// Blank space around the grid.
let blank: Padding = self.size.screen.blank_padding(
self.size.padding,
GridSize {
columns: self.cells.size.columns,
rows: self.cells.size.rows,
},
CellSize {
width: self.grid_metrics.cell_width,
height: self.grid_metrics.cell_height,
},
) + self.size.padding;
// Setup our uniforms
self.uniforms.projection_matrix = math::ortho2d(
-(self.size.padding.left as f32),
(terminal_size.width + self.size.padding.right) as f32,
(terminal_size.height + self.size.padding.bottom) as f32,
-(self.size.padding.top as f32),
);
self.uniforms.grid_padding = [
blank.top as f32,
blank.right as f32,
blank.bottom as f32,
blank.left as f32,
];
self.uniforms.screen_size = [
self.size.screen.width as f32,
self.size.screen.height as f32,
];
}
/// Update the background image vertex buffer (CPU-side).
///
/// This should be called if and when configs change that
/// could affect the background image.
fn update_bg_image_buffer(&mut self) {
self.bg_image_buffer = BgImage {
opacity: self.config.bg_image_opacity,
info: BgImageInfo {
position: match self.config.bg_image_position {
BackgroundImagePosition::TopLeft => BgImagePosition::Tl,
BackgroundImagePosition::TopCenter => BgImagePosition::Tc,
BackgroundImagePosition::TopRight => BgImagePosition::Tr,
BackgroundImagePosition::CenterLeft => BgImagePosition::Ml,
BackgroundImagePosition::CenterCenter | BackgroundImagePosition::Center => {
BgImagePosition::Mc
}
BackgroundImagePosition::CenterRight => BgImagePosition::Mr,
BackgroundImagePosition::BottomLeft => BgImagePosition::Bl,
BackgroundImagePosition::BottomCenter => BgImagePosition::Bc,
BackgroundImagePosition::BottomRight => BgImagePosition::Br,
},
fit: match self.config.bg_image_fit {
BackgroundImageFit::Contain => BgImageFit::Contain,
BackgroundImageFit::Cover => BgImageFit::Cover,
BackgroundImageFit::Stretch => BgImageFit::Stretch,
BackgroundImageFit::None => BgImageFit::None,
},
repeat: self.config.bg_image_repeat,
}
.to_bits(),
pad: [0; 3],
};
// Signal that the buffer was modified.
self.bg_image_buffer_modified = self.bg_image_buffer_modified.wrapping_add(1);
}
/// Call this any time the background image path changes.
fn prep_background_image(&mut self) {
// Then we try to load the background image if we have a path.
if let Some(p) = &self.config.bg_image
&& let Some(image) = p
.path()
.to_syscall_path()
.map_err(|error| log::warn!("invalid background image path: {error}"))
.ok()
.and_then(|path| load_background_image(&path))
.map(image::Image::Pending)
{
// If we have an existing background image, replace it.
// Otherwise, set this as our background image directly.
match &mut self.bg_image {
Some(img) => img.mark_for_replace(image),
None => self.bg_image = Some(image),
}
} else if self.config.bg_image.is_none()
&& let Some(img) = &mut self.bg_image
{
// If we don't have a background image path, mark our
// background image for unload if we currently have one.
img.mark_for_unload();
}
}
/// Update custom shader uniforms that depend on terminal state.
///
/// This should be called in `update_frame` when terminal state changes.
fn update_custom_shader_uniforms_from_state(
&mut self,
state: &RenderState,
cursor_text: Option<TerminalColor>,
) {
fn f4(c: Rgb) -> [f32; 4] {
[
f32::from(c.r) / 255.0,
f32::from(c.g) / 255.0,
f32::from(c.b) / 255.0,
1.0,
]
}
// We only need to do this if we have custom shaders.
if !self.has_custom_shaders {
return;
}
// Only update when terminal state is dirty.
if state.dirty == Dirty::False {
return;
}
let selection_background = self.config.selection_background;
let selection_foreground = self.config.selection_foreground;
let uniforms = &mut self.custom_shader_uniforms;
let colors = &state.colors;
// 256-color palette
for (i, color) in colors.palette.iter().enumerate() {
uniforms.palette[i] = f4(*color);
}
// Background color
uniforms.background_color = f4(colors.background);
// Foreground color
uniforms.foreground_color = f4(colors.foreground);
// Cursor color
if let Some(cursor_color) = colors.cursor {
uniforms.cursor_color = f4(cursor_color);
}
// NOTE: the following could be optimized to follow a change in
// config for a slight optimization however this is only 12 bytes
// each being updated and likely isn't a cause for concern
//
// Deviation: Zig reads `.color` unconditionally, which is invalid for
// the `cell-foreground`/`cell-background` variants; those are skipped.
// Cursor text color
if let Some(TerminalColor::Color(c)) = cursor_text {
uniforms.cursor_text = f4(c.to_terminal_rgb());
}
// Selection background color
if let Some(TerminalColor::Color(c)) = selection_background {
uniforms.selection_background_color = f4(c.to_terminal_rgb());
}
// Selection foreground color
if let Some(TerminalColor::Color(c)) = selection_foreground {
uniforms.selection_foreground_color = f4(c.to_terminal_rgb());
}
// Cursor visibility
uniforms.cursor_visible = i32::from(state.cursor.visible);
// Cursor style
let cursor_style = CursorStyle::from_terminal(state.cursor.visual_style);
uniforms.previous_cursor_style = uniforms.current_cursor_style;
uniforms.current_cursor_style = cursor_style as i32;
}
/// Update per-frame custom shader uniforms.
///
/// This should be called exactly once per frame, inside `draw_frame`.
#[allow(clippy::float_cmp)]
fn update_custom_shader_uniforms_for_frame(&mut self) {
// We only need to do this if we have custom shaders.
if !self.has_custom_shaders {
return;
}
let now = Instant::now();
let first_frame_time = *self.first_frame_time.get_or_insert(now);
let last_frame_time = self.last_frame_time.unwrap_or(now);
self.last_frame_time = Some(now);
let screen = self.size.screen;
let padding = self.size.padding;
let cell = self.size.cell;
let cursor_glyph = self.cells.get_cursor_glyph();
let focused = self.focused;
let y_is_down = G::CUSTOM_SHADER_Y_IS_DOWN;
let uniforms = &mut self.custom_shader_uniforms;
let since_ns = now.duration_since(first_frame_time).as_nanos() as f32;
uniforms.time = since_ns / 1_000_000_000.0;
let delta_ns = now.duration_since(last_frame_time).as_nanos() as f32;
uniforms.time_delta = delta_ns / 1_000_000_000.0;
uniforms.frame += 1;
uniforms.resolution = [screen.width as f32, screen.height as f32, 1.0];
uniforms.channel_resolution[0] = [screen.width as f32, screen.height as f32, 1.0, 0.0];
if let Some(cursor) = cursor_glyph {
let cursor_width = cursor.glyph_size[0] as f32;
let cursor_height = cursor.glyph_size[1] as f32;
// Left edge of the cell the cursor is in.
let mut pixel_x = (u32::from(cursor.grid_pos[0]) * cell.width + padding.left) as f32;
// Top edge, relative to the top of the
// screen, of the cell the cursor is in.
let mut pixel_y = (u32::from(cursor.grid_pos[1]) * cell.height + padding.top) as f32;
// If +Y is up in our shaders, we need to flip the coordinate
// so that it's instead the top edge of the cell relative to
// the *bottom* of the screen.
if !y_is_down {
pixel_y = screen.height as f32 - pixel_y;
}
// Add the X bearing to get the -X (left) edge of the cursor.
pixel_x += f32::from(cursor.bearings[0]);
// How we deal with the Y bearing depends on which direction
// is "up", since we want our final `pixel_y` value to be the
// +Y edge of the cursor.
if y_is_down {
// As a reminder, the Y bearing is the distance from the
// bottom of the cell to the top of the glyph, so to get
// the +Y edge we need to add the cell height, subtract
// the Y bearing, and add the glyph height to get the +Y
// (bottom) edge of the cursor.
pixel_y += cell.height as f32;
pixel_y -= f32::from(cursor.bearings[1]);
pixel_y += cursor.glyph_size[1] as f32;
} else {
// If the Y direction is reversed though, we instead want
// the *top* edge of the cursor, which means we just need
// to subtract the cell height and add the Y bearing.
pixel_y -= cell.height as f32;
pixel_y += f32::from(cursor.bearings[1]);
}
let new_cursor = [pixel_x, pixel_y, cursor_width, cursor_height];
let cursor_color = [
f32::from(cursor.color[0]) / 255.0,
f32::from(cursor.color[1]) / 255.0,
f32::from(cursor.color[2]) / 255.0,
f32::from(cursor.color[3]) / 255.0,
];
let cursor_changed = new_cursor != uniforms.current_cursor
|| cursor_color != uniforms.current_cursor_color;
if cursor_changed {
uniforms.previous_cursor = uniforms.current_cursor;
uniforms.previous_cursor_color = uniforms.current_cursor_color;
uniforms.current_cursor = new_cursor;
uniforms.current_cursor_color = cursor_color;
uniforms.cursor_change_time = uniforms.time;
}
}
// Update focus uniforms
uniforms.focus = i32::from(focused);
// If we need to update the time our focus state changed
// then update it to our current frame time. This may not be
// exactly correct since it is frame time, not exact focus
// time, but focus time on its own isn't exactly correct anyways
// since it comes async from a message.
if self.custom_shader_focused_changed && self.focused {
self.custom_shader_uniforms.time_focus = self.custom_shader_uniforms.time;
self.custom_shader_focused_changed = false;
}
}
/// Sync the atlas data to the given texture. This copies the bytes
/// associated with the atlas to the given texture. If the atlas no
/// longer fits into the texture, the texture will be resized.
fn sync_atlas_texture(api: &G, atlas: &Atlas, texture: &mut G::Texture) -> Result<(), Error> {
if atlas.size as usize > texture.width() {
// Reallocate (the old texture is dropped/deleted)
*texture = api.init_atlas_texture(atlas)?;
}
texture.replace_region(0, 0, atlas.size as usize, atlas.size as usize, &atlas.data)?;
Ok(())
}
}
/// Make sure our bg image is uploaded if it needs to be.
fn upload_background_image<G: GraphicsApi>(bg_image: &mut BgImageState<G>, gpu: &Gpu<'_, G>) {
if let Some(bg) = bg_image {
if bg.is_unloading() {
*bg_image = None;
return;
}
if bg.is_pending()
&& let Err(err) = bg.upload(gpu)
{
log::warn!("error uploading background image err={err}");
}
}
}
/// Decode a background image file (`prepBackgroundImage`). Failures are
/// logged and result in no image, as in Zig.
fn load_background_image(path: &std::path::Path) -> Option<image::Pending> {
let shown = path.display();
// Read it (Max size of 4 GiB, for now.)
let contents = match std::fs::read(path) {
Ok(c) if u32::try_from(c.len()).is_ok() => c,
Ok(_) => {
log::warn!("background image file \"{shown}\" is too large");
return None;
}
Err(err) => {
log::warn!("error opening background image file \"{shown}\": {err}");
return None;
}
};
// Figure out what type it probably is.
let file_type = match detect_file_type(&contents) {
FileType::Unknown => guess_file_type_from_extension(path),
t => t,
};
// Decode it if we know how.
let (width, height, data) = match file_type {
FileType::Png => match decode_png(&contents) {
Ok(v) => v,
Err(err) => {
log::warn!("error decoding png background image \"{shown}\": {err}");
return None;
}
},
FileType::Jpeg => match decode_jpeg(&contents) {
Ok(v) => v,
Err(err) => {
log::warn!("error decoding jpeg background image \"{shown}\": {err}");
return None;
}
},
FileType::Unknown => {
log::warn!("Cannot determine file type for background image file \"{shown}\"!");
return None;
}
f => {
log::warn!("Unsupported file type {f:?} for background image file \"{shown}\"!");
return None;
}
};
Some(image::Pending {
width,
height,
pixel_format: image::PixelFormat::Rgba,
data,
})
}
/// `file_type.zig` (the subset needed here).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FileType {
Jpeg,
Png,
Gif,
Bmp,
Qoi,
Webp,
Unknown,
}
fn detect_file_type(contents: &[u8]) -> FileType {
const PNG: &[u8] = &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
if contents.starts_with(PNG) {
FileType::Png
} else if contents.starts_with(&[0xFF, 0xD8, 0xFF]) {
FileType::Jpeg
} else if contents.starts_with(b"GIF87a") || contents.starts_with(b"GIF89a") {
FileType::Gif
} else if contents.starts_with(b"BM") {
FileType::Bmp
} else if contents.starts_with(b"qoif") {
FileType::Qoi
} else if contents.len() >= 12 && &contents[..4] == b"RIFF" && &contents[8..12] == b"WEBP" {
FileType::Webp
} else {
FileType::Unknown
}
}
fn guess_file_type_from_extension(path: &std::path::Path) -> FileType {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase);
match ext.as_deref() {
Some("jpg" | "jpeg" | "jfif") => FileType::Jpeg,
Some("png") => FileType::Png,
Some("gif") => FileType::Gif,
Some("bmp") => FileType::Bmp,
Some("qoi") => FileType::Qoi,
Some("webp") => FileType::Webp,
_ => FileType::Unknown,
}
}
/// Decode a PNG to straight (non-premultiplied) RGBA8.
#[allow(clippy::chunks_exact_to_as_chunks)]
fn decode_png(contents: &[u8]) -> Result<(u32, u32, Vec<u8>), String> {
let mut decoder = png::Decoder::new(std::io::Cursor::new(contents));
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
let mut reader = decoder.read_info().map_err(|e| e.to_string())?;
let mut buf = vec![0; reader.output_buffer_size().ok_or("png too large")?];
let info = reader.next_frame(&mut buf).map_err(|e| e.to_string())?;
buf.truncate(info.buffer_size());
let n = info.width as usize * info.height as usize;
let rgba = match info.color_type {
png::ColorType::Rgba => buf,
png::ColorType::Rgb => buf
.chunks_exact(3)
.flat_map(|p| [p[0], p[1], p[2], 255])
.collect(),
png::ColorType::GrayscaleAlpha => buf
.chunks_exact(2)
.flat_map(|p| [p[0], p[0], p[0], p[1]])
.collect(),
png::ColorType::Grayscale => buf.iter().flat_map(|&g| [g, g, g, 255]).collect(),
png::ColorType::Indexed => return Err("unexpanded indexed png".into()),
};
if rgba.len() != n * 4 {
return Err("unexpected png buffer size".into());
}
Ok((info.width, info.height, rgba))
}
/// Decode a JPEG to RGBA8.
#[allow(clippy::chunks_exact_to_as_chunks)]
fn decode_jpeg(contents: &[u8]) -> Result<(u32, u32, Vec<u8>), String> {
let mut decoder = jpeg_decoder::Decoder::new(std::io::Cursor::new(contents));
let pixels = decoder.decode().map_err(|e| e.to_string())?;
let info = decoder.info().ok_or("missing jpeg info")?;
let rgba: Vec<u8> = match info.pixel_format {
jpeg_decoder::PixelFormat::RGB24 => pixels
.chunks_exact(3)
.flat_map(|p| [p[0], p[1], p[2], 255])
.collect(),
jpeg_decoder::PixelFormat::L8 => pixels.iter().flat_map(|&g| [g, g, g, 255]).collect(),
jpeg_decoder::PixelFormat::L16 => pixels
.chunks_exact(2)
.flat_map(|p| [p[0], p[0], p[0], 255])
.collect(),
jpeg_decoder::PixelFormat::CMYK32 => pixels
.chunks_exact(4)
.flat_map(|p| [p[0], p[1], p[2], 255])
.collect(),
};
Ok((u32::from(info.width), u32::from(info.height), rgba))
}
/// Convert the config bold color to the terminal style's bold policy.
fn bold_policy(c: Option<BoldColor>) -> Option<style::BoldColor> {
c.map(|c| match c {
BoldColor::Color(c) => style::BoldColor::Color(c.to_terminal_rgb()),
BoldColor::Bright => style::BoldColor::Bright,
})
}
/// Resolve a configured [`TerminalColor`] against the cell colors
/// (the repeated `.color / .cell-foreground / .cell-background` switch).
/// `cell-foreground` is the foreground unless the cell is inverse, in which
/// case it is the background (and vice versa for `cell-background`).
fn resolve_terminal_color(
c: TerminalColor,
inverse: bool,
fg_style: Option<Rgb>,
bg_style: Option<Rgb>,
) -> Option<Rgb> {
match c {
TerminalColor::Color(color) => Some(color.to_terminal_rgb()),
TerminalColor::CellForeground => {
if inverse {
bg_style
} else {
fg_style
}
}
TerminalColor::CellBackground => {
if inverse {
fg_style
} else {
bg_style
}
}
}
}
/// Mutable shaping context for a row rebuild.
struct ShapeCtx<'a> {
shaper: &'a mut shape::Shaper,
cache: &'a mut ShaperCache,
run_cells: &'a mut Vec<RunCell>,
shaped: &'a mut Vec<shape::Cell>,
}
/// Fill `shaped` with the shaped cells for `run`, from the cache if possible
/// (a copy is made because the shaper's buffer is only valid until the next
/// shaping call).
fn shaped_cells_for_run(
shaper: &mut shape::Shaper,
cache: &mut ShaperCache,
shaped: &mut Vec<shape::Cell>,
grid: &SharedGrid,
run: &TextRun,
) -> Result<(), Error> {
shaped.clear();
// Try to read the cells from the shaping cache if we can.
if let Some(cells) = cache.get(run) {
shaped.extend_from_slice(cells);
return Ok(());
}
// Otherwise we have to shape them.
let new_cells = shaper.shape_run(grid, run)?;
// Cache them: this is just a performance optimization.
cache.put(run, new_cells);
shaped.extend_from_slice(new_cells);
Ok(())
}
impl<G: GraphicsApi> Renderer<G> {
/// Create the renderer (`Renderer.init`).
///
/// `api` must already be prepared (OpenGL: [`OpenGL::prepare_context`],
/// i.e. Zig `Renderer.surfaceInit`, with the context current on the
/// calling thread; Metal: attached to its view): swap chain buffers,
/// textures and shaders are created here.
pub fn new(opts: Options<DerivedConfig>, api: G) -> Result<Renderer<G>, Error> {
let config = opts.config;
let has_custom_shaders = !config.custom_shaders.is_empty();
// Prepare our swap chain
let swap_chain = SwapChain::new(&api, has_custom_shaders)?;
// Create the font shaper.
let font_shaper = shape::Shaper::new(&shape::Options {
features: config.font_features.clone(),
})?;
// Initialize all the data that requires a critical font section.
let grid_metrics = opts.font_grid.metrics();
let mut draw = DrawState {
config: config.clone(),
grid_metrics,
size: opts.size,
focused: true,
custom_shader_focused_changed: false,
scrollbar: Scrollbar::default(),
scrollbar_dirty: false,
// Render state
cells: Contents::new(),
cells_rebuilt: false,
uniforms: Uniforms {
min_contrast: config.min_contrast,
cursor_pos: [u16::MAX, u16::MAX],
bg_color: [
config.background.r,
config.background.g,
config.background.b,
// Note that if we're on macOS with glass effects
// we'll disable background opacity but we handle
// that in update_frame.
(config.background_opacity * 255.0).round() as u8,
],
bools: Bools {
cursor_wide: false,
use_display_p3: config.colorspace == WindowColorspace::DisplayP3,
use_linear_blending: config.blending.is_linear(),
use_linear_correction: config.blending == AlphaBlending::LinearCorrected,
}
.to_bits(),
..Uniforms::default()
},
custom_shader_uniforms: shadertoy::Uniforms {
resolution: [0.0, 0.0, 1.0],
frame_rate: 60.0, // not currently updated
focus: 1, // assume focused initially
..shadertoy::Uniforms::default()
},
first_frame_time: None,
last_frame_time: None,
font_grid: opts.font_grid,
images: image::State::new(),
has_overlay: false,
bg_image: None,
bg_image_buffer: BgImage::default(),
bg_image_buffer_modified: 0,
api,
swap_chain,
target_config_modified: 0,
reinitialize_shaders: false,
has_custom_shaders: false,
shaders: None,
};
draw.init_shaders()?;
// Ensure our default values above are correctly initialized.
draw.update_font_grid_uniforms();
draw.update_screen_size_uniforms();
draw.update_bg_image_buffer();
draw.prep_background_image();
let thread = ThreadState {
config,
terminal_state: RenderState::new(),
terminal_state_frame_count: 0,
last_bottom: None,
search_matches: None,
search_selected_match: None,
search_matches_dirty: false,
overlay: None,
font_shaper,
font_shaper_cache: ShaperCache::new(),
run_cells: Vec::new(),
shaped_cells: Vec::new(),
};
let has_animations = AtomicBool::new(draw.has_custom_shaders);
// The display link is created here but only started in `loop_enter`.
let display_link = if cfg!(target_os = "macos") && draw.config.vsync {
DisplayLink::create_with_active_cg_displays()
} else {
None
};
Ok(Renderer {
display_link: Mutex::new(display_link),
draw: Mutex::new(draw),
thread: Mutex::new(thread),
health: Arc::new(AtomicU8::new(Health::Healthy as u8)),
has_animations,
host: opts.host,
})
}
/// This is called by the apprt after the surface is reinitialized
/// due to any of the events mentioned in the doc comment of
/// [`Renderer::display_unrealized`] (Zig `displayRealized`). `realize` is
/// the API's own hook ("if our API has to do things on realize, let it":
/// OpenGL reloads its context). The GPU context must be current.
pub fn display_realized_with(&self, realize: impl FnOnce(&mut G)) -> Result<(), Error> {
// Lock the draw mutex so that we can
// safely reinitialize our GPU resources.
let mut d = lock(&self.draw);
// If our API has to do things on realize, let it.
realize(&mut d.api);
// We assume that the swap chain was deinited in
// `display_unrealized`, in which case it should be
// marked defunct. If not, we have a problem.
debug_assert!(d.swap_chain.defunct);
// We reinitialize our shaders and our swap chain.
d.init_shaders()?;
self.has_animations
.store(d.has_custom_shaders, Ordering::Relaxed);
d.swap_chain = SwapChain::new(&d.api, d.has_custom_shaders)?;
d.reinitialize_shaders = false;
d.target_config_modified = 1;
d.api.clear_last_target();
// The new atlas textures and buffers need to be refilled.
d.update_bg_image_buffer();
Ok(())
}
/// This is called by the GTK apprt when the surface is being destroyed.
/// This can happen because the surface is being closed but also when
/// moving the window between displays or splitting. The GL context
/// must be current.
pub fn display_unrealized(&self) {
// Lock the draw mutex so that we can
// safely deinitialize our GPU resources. (The thread state is locked
// first, per the lock order.)
let mut t = lock(&self.thread);
let mut d = lock(&self.draw);
// We deinit our swap chain and shaders.
//
// This will mark them as defunct so that they
// can't be double-freed or used in draw calls.
d.api.display_unrealized();
d.swap_chain.deinit();
d.shaders = None;
d.api.clear_last_target();
// GPU textures of images and the background image die with the
// context; drop them so they are re-uploaded after realize.
d.images = image::State::new();
d.images.force_kitty_update = true;
d.bg_image = None;
d.prep_background_image();
t.terminal_state.dirty = Dirty::Full;
}
/// Mark the full screen as dirty so that we redraw everything.
pub fn mark_dirty(&self) {
lock(&self.thread).terminal_state.dirty = Dirty::Full;
}
/// True if our renderer has animations so that a higher frequency
/// timer is used.
pub fn has_animations(&self) -> bool {
self.has_animations.load(Ordering::Relaxed)
}
/// True if our renderer is using vsync. If true, the renderer or apprt
/// is responsible for triggering draw_now calls to the render thread.
/// That is the only way to trigger a `draw_frame`. (Only true on macOS
/// with a running display link.)
pub fn has_vsync(&self) -> bool {
lock(&self.display_link)
.as_ref()
.is_some_and(DisplayLink::is_running)
}
/// The font grid currently used for rendering (for apprts that need
/// to read the primary face, e.g. macOS quicklook).
pub fn font_grid(&self) -> Arc<SharedGrid> {
Arc::clone(&lock(&self.draw).font_grid)
}
/// Called by the renderer thread when it begins (`threadEnter`).
pub fn thread_enter(&self) {
lock(&self.draw).api.thread_enter();
}
/// Called by the renderer thread when it exits (`threadExit`).
pub fn thread_exit(&self) {
lock(&self.draw).api.thread_exit();
}
/// Called by the renderer thread when it starts the main loop
/// (`loopEnter`). `draw_now` forces an immediate draw on the renderer
/// thread; the display link calls it on every vsync.
pub fn loop_enter(self: &Arc<Self>, draw_now: Arc<dyn Fn() + Send + Sync>) {
// If our API has to do things on loop enter, let it. (Metal draws
// synchronously from the layer's display callback.)
let weak = Arc::downgrade(self);
lock(&self.draw).api.loop_enter(Arc::new(move || {
if let Some(renderer) = weak.upgrade()
&& let Err(err) = renderer.draw_frame(true)
{
log::warn!("Error drawing frame in display callback, err={err}");
}
}));
// If we don't support a display link we have no work to do.
let guard = lock(&self.display_link);
let Some(link) = guard.as_ref() else {
return;
};
// To setup the display link we set our callback and we can start it
// immediately.
if let Err(err) = link.set_output_callback(draw_now) {
log::warn!("error setting display link callback err={err}");
return;
}
let _ = link.start();
}
/// Called by the renderer thread when it exits the main loop
/// (`loopExit`).
pub fn loop_exit(&self) {
// If our API has to do things on loop exit, let it.
lock(&self.draw).api.loop_exit();
// Stop our display link. If this fails its okay it just means
// that we either never started it or the view its attached to
// is gone which is fine.
if let Some(link) = lock(&self.display_link).as_ref() {
let _ = link.stop();
}
}
/// Called when we get an updated display ID for our display link
/// (`setMacOSDisplayID`).
pub fn set_macos_display_id(&self, id: u32) {
let guard = lock(&self.display_link);
let Some(link) = guard.as_ref() else {
return;
};
log::info!("updating display link display id={id}");
if let Err(err) = link.set_current_cg_display(id) {
log::warn!("error setting display link display id err={err}");
}
}
/// The health of the most recently completed frame.
pub fn health(&self) -> Health {
if self.health.load(Ordering::SeqCst) == Health::Healthy as u8 {
Health::Healthy
} else {
Health::Unhealthy
}
}
/// The current size of everything as the renderer knows it.
pub fn size(&self) -> Size {
lock(&self.draw).size
}
/// Callback when the focus changes for the terminal this is rendering.
///
/// Must be called on the render thread.
pub fn set_focus(&self, focus: bool) {
let mut d = lock(&self.draw);
debug_assert_ne!(d.focused, focus);
d.focused = focus;
// Flag that we need to update our custom shaders
d.custom_shader_focused_changed = true;
// If we're not focused, then we want to stop the display link
// because it is a waste of resources and we can move to pure
// change-driven updates.
if let Some(link) = lock(&self.display_link).as_ref() {
let _ = if focus { link.start() } else { link.stop() };
}
}
/// Callback when the window is visible or occluded.
///
/// Must be called on the render thread.
pub fn set_visible(&self, visible: bool) {
// If we're not visible, then we want to stop the display link
// because it is a waste of resources and we can move to pure
// change-driven updates.
let focused = lock(&self.draw).focused;
if let Some(link) = lock(&self.display_link).as_ref() {
let _ = if visible && focused {
link.start()
} else {
link.stop()
};
}
}
/// Set the new font grid.
///
/// Must be called on the render thread.
pub fn set_font_grid(&self, grid: Arc<SharedGrid>) {
let mut t = lock(&self.thread);
let mut d = lock(&self.draw);
// Get our metrics from the grid. This doesn't require a lock because
// the metrics are never recalculated.
let metrics = grid.metrics();
// Update our grid
d.font_grid = grid;
// Update all our textures so that they sync on the next frame.
d.swap_chain.invalidate_atlases();
d.grid_metrics = metrics;
// Reset our shaper cache. If our font changed (not just the size) then
// the data in the shaper cache may be invalid and cannot be used, so we
// always clear the cache just in case.
t.font_shaper_cache = ShaperCache::new();
// Update cell size.
d.size.cell = CellSize {
width: metrics.cell_width,
height: metrics.cell_height,
};
// Update relevant uniforms
d.update_font_grid_uniforms();
// Force a full rebuild, because cached rows may still reference
// an outdated atlas from the old grid and this can cause garbage
// to be rendered.
t.terminal_state.dirty = Dirty::Full;
}
/// Update the configuration.
#[allow(clippy::float_cmp)]
pub fn change_config(&self, config: DerivedConfig) -> Result<(), Error> {
let mut t = lock(&self.thread);
let mut d = lock(&self.draw);
// We always redo the font shaper in case font features changed. We
// could check to see if there was an actual config change but this is
// easier and rare enough to not cause performance issues.
t.font_shaper = shape::Shaper::new(&shape::Options {
features: config.font_features.clone(),
})?;
// We also need to reset the shaper cache so shaper info
// from the previous font isn't reused for the new font.
t.font_shaper_cache = ShaperCache::new();
// Set our new minimum contrast
d.uniforms.min_contrast = config.min_contrast;
// Set our new color space and blending
let mut bools = Bools::from_bits(d.uniforms.bools);
bools.use_display_p3 = config.colorspace == WindowColorspace::DisplayP3;
bools.use_linear_blending = config.blending.is_linear();
bools.use_linear_correction = config.blending == AlphaBlending::LinearCorrected;
d.uniforms.bools = bools.to_bits();
let bg_image_config_changed = d.config.bg_image_fit != config.bg_image_fit
|| d.config.bg_image_position != config.bg_image_position
|| d.config.bg_image_repeat != config.bg_image_repeat
|| d.config.bg_image_opacity != config.bg_image_opacity;
let bg_image_changed = d.config.bg_image != config.bg_image;
let old_blending = d.config.blending;
let custom_shaders_changed = d.config.custom_shaders != config.custom_shaders;
d.config = config.clone();
t.config = config;
// If our background image path changed, prepare the new bg image.
if bg_image_changed {
d.prep_background_image();
}
// If our background image config changed, update the vertex buffer.
if bg_image_config_changed {
d.update_bg_image_buffer();
}
// Reset our viewport to force a rebuild, in case of a font change.
t.terminal_state.dirty = Dirty::Full;
let blending_changed = old_blending != d.config.blending;
if blending_changed {
// We update our API's blending mode.
let blending = d.config.blending;
d.api.set_blending(blending);
// And indicate that we need to reinitialize our shaders.
d.reinitialize_shaders = true;
// And indicate that our swap chain targets need to
// be re-created to account for the new blending mode.
d.target_config_modified = d.target_config_modified.wrapping_add(1);
}
if custom_shaders_changed {
d.reinitialize_shaders = true;
}
Ok(())
}
/// Resize the screen.
pub fn set_screen_size(&self, size: Size) {
let mut d = lock(&self.draw);
// We only actually need the padding from this,
// everything else is derived elsewhere.
d.size.padding = size.padding;
d.update_screen_size_uniforms();
log::debug!("screen size size={size:?}");
}
/// Set the search highlights (from the renderer thread mailbox).
pub fn set_search_viewport_matches(&self, matches: Vec<Flattened>) {
let mut t = lock(&self.thread);
t.search_matches = Some(matches);
t.search_matches_dirty = true;
}
/// Set the selected search match (from the renderer thread mailbox).
pub fn set_search_selected_match(&self, m: Option<Flattened>) {
let mut t = lock(&self.thread);
t.search_selected_match = m;
t.search_matches_dirty = true;
}
/// The callback the graphics API calls when a frame is completed
/// (`renderer.frameCompleted`). Metal calls it from the command buffer's
/// completion handler, so it owns handles instead of borrowing `self`.
fn frame_completion(&self) -> FrameCompletion {
let health = Arc::clone(&self.health);
let host = Arc::clone(&self.host);
Arc::new(move |h| frame_completed(&health, host.as_ref(), h))
}
}
/// Record the health of a completed frame and notify the surface on change.
fn frame_completed(current: &AtomicU8, host: &dyn Host, health: Health) {
// If our health value hasn't changed, then we do nothing. We don't
// do a cmpxchg here because strict atomicity isn't important.
if current.load(Ordering::SeqCst) != health as u8 {
current.store(health as u8, Ordering::SeqCst);
// Our health value changed, so we notify the surface so that it
// can do something about it.
host.renderer_health(health);
}
}
impl Renderer<OpenGL> {
/// This is called by the GTK apprt after the surface is reinitialized
/// due to any of the events mentioned in the doc comment of
/// [`Renderer::display_unrealized`]. The GL context must be current.
pub fn display_realized<F>(&self, get_proc_address: F) -> Result<(), Error>
where
F: FnMut(&str) -> *const std::ffi::c_void,
{
self.display_realized_with(|api| api.display_realized(get_proc_address))
}
/// Read back the pixels (RGBA8, bottom-up like `glReadPixels`) of the
/// most recently presented frame. Intended for tests and screenshots;
/// requires the GL context to be current.
pub fn last_frame_pixels(&self) -> Option<(usize, usize, Vec<u8>)> {
let d = lock(&self.draw);
let target = d.api.last_target()?;
let gl = d.api.gl()?.clone();
let bind = gl.bind_framebuffer(
target.framebuffer(),
crate::opengl::gl::FramebufferTarget::Read,
);
let px = gl.read_pixels_rgba8(0, 0, target.width as i32, target.height as i32);
gl.unbind_framebuffer(bind);
Some((target.width, target.height, px))
}
}
// ---------------------------------------------------------------------------
// update_frame (renderer thread)
/// Data extracted from the terminal inside the critical section.
struct Critical {
links: HashSet<Coordinate>,
mouse_point: Option<Coordinate>,
mouse_mods: Mods,
preedit: Option<Preedit>,
scrollbar: Scrollbar,
overlay_features: Vec<overlay::Feature>,
}
/// The max number of frames before we throw away the `RenderState`
/// (~12 minutes at 120Hz).
const MAX_TERMINAL_STATE_FRAME_COUNT: usize = 100_000;
impl<G: GraphicsApi> Renderer<G> {
/// Update the frame data.
///
/// This is the renderer thread half of the renderer: it locks
/// `state` (the renderer mutex) briefly to snapshot the terminal and then
/// rebuilds the CPU copy of the GPU cell buffers. It performs no GL calls.
pub fn update_frame(
&self,
state: &SharedState,
cursor_blink_visible: bool,
) -> Result<(), Error> {
let mut guard = lock(&self.thread);
let t = &mut *guard;
// We fully deinit and reset the terminal state every so often
// so that a particularly large terminal state doesn't cause
// the renderer to hold on to retained memory.
if t.terminal_state_frame_count >= MAX_TERMINAL_STATE_FRAME_COUNT {
t.terminal_state = RenderState::new();
}
t.terminal_state_frame_count += 1;
// Update all our data as tightly as possible within the mutex.
let mut critical = {
let mut st = lock(state);
let st = &mut *st;
// If we're in a synchronized output state, we pause all rendering.
if st.terminal.modes.get(Mode::SynchronizedOutput) {
log::debug!("synchronized output started, skipping render");
return Ok(());
}
// If scroll-to-bottom on output is enabled, check if the final line
// changed by comparing the bottom-right pin. If the node or
// y offset changed, new content was added to the screen.
// Update this BEFORE we update our render state so we can
// draw the new scrolled data immediately.
if t.config.scroll_to_bottom_on_output
&& let Some(br) = st.terminal.screen().pages.get_bottom_right(Tag::Screen)
&& t.last_bottom != Some((br.node, br.y))
{
// Update tracked pin state for next frame
t.last_bottom = Some((br.node, br.y));
// Scroll
st.terminal.scroll_viewport(ScrollViewport::Bottom);
}
// Update our terminal state
t.terminal_state
.update(&mut st.terminal)
.map_err(Error::Terminal)?;
// If our terminal state is dirty at all we need to redo
// the viewport search.
if t.terminal_state.dirty != Dirty::False {
st.terminal.flags.search_viewport_dirty = true;
}
// Get our scrollbar out of the terminal. We synchronize
// the scrollbar read with frame data updates because this
// naturally limits the number of calls to this method (it
// can be expensive) and also makes it so we don't need another
// cross-thread mailbox message within the IO path.
let scrollbar = st.terminal.screen().pages.scrollbar();
// Get our preedit state
let preedit = st.preedit.clone();
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
// We only do this if the Kitty image state is dirty meaning only if
// it changes.
//
// If we have any virtual references, we must also rebuild our
// kitty state on every frame because any cell change can move
// an image.
{
let mut d = lock(&self.draw);
if d.images.kitty_requires_update(&st.terminal) {
let cell_size = CellSize {
width: d.grid_metrics.cell_width,
height: d.grid_metrics.cell_height,
};
update_kitty(&mut d.images, &mut st.terminal, cell_size);
}
}
// Get our OSC8 links we're hovering if we have a mouse.
// This requires terminal state because of URLs.
let links = 'osc8: {
// If our mouse isn't hovering, we have no links.
let Some(vp) = st.mouse.point else {
break 'osc8 HashSet::new();
};
// If the right mods aren't pressed, then we can't match.
if !st
.mouse
.mods
.equal(ghostty_input::ctrl_or_super(Mods::default()))
{
break 'osc8 HashSet::new();
}
t.terminal_state.link_cells(&st.terminal.screen().pages, vp)
};
Critical {
links,
mouse_point: st.mouse.point,
mouse_mods: st.mouse.mods,
preedit,
scrollbar,
overlay_features: st
.inspector
.as_ref()
.map(|i| i.overlay_features())
.unwrap_or_default(),
}
};
// Outside the critical area we can update our links to contain
// our regex results.
if let Err(err) = t.config.links.render_cell_map(
&mut critical.links,
&t.terminal_state,
critical.mouse_point,
critical.mouse_mods,
) {
log::warn!("error searching for regex links err={err}");
}
// Clear our highlight state and update.
if t.search_matches_dirty || t.terminal_state.dirty != Dirty::False {
t.search_matches_dirty = false;
// Clear the prior highlights
let mut any_dirty = false;
for row in &mut t.terminal_state.row_data {
if !row.highlights.is_empty() {
row.highlights.clear();
row.dirty = true;
any_dirty = true;
}
}
if any_dirty && t.terminal_state.dirty == Dirty::False {
t.terminal_state.dirty = Dirty::Partial;
}
// NOTE: The order below matters. Highlights added earlier
// will take priority.
if let Some(m) = &t.search_selected_match {
t.terminal_state.update_highlights_flattened(
HighlightTag::SearchMatchSelected as u8,
std::slice::from_ref(m),
);
}
if let Some(m) = &t.search_matches {
t.terminal_state
.update_highlights_flattened(HighlightTag::SearchMatch as u8, m);
}
}
// Rebuild the overlay image if we have one. We can do this
// outside of any critical areas.
if let Err(err) = self.rebuild_overlay(t, &critical.overlay_features) {
log::warn!("error rebuilding overlay surface err={err}");
}
// Acquire the draw mutex for all remaining state updates.
{
let mut d = lock(&self.draw);
// Build our GPU cells
let cursor_style = cursor::style(
&t.terminal_state,
cursor::StyleOptions {
preedit: critical.preedit.is_some(),
focused: d.focused,
blink_visible: cursor_blink_visible,
},
);
d.rebuild_cells(t, critical.preedit.as_ref(), cursor_style, &critical.links);
// The scrollbar is only emitted during draws so we also
// check the scrollbar cache here and update if needed.
// This is pretty fast.
if d.scrollbar != critical.scrollbar {
d.scrollbar = critical.scrollbar;
d.scrollbar_dirty = true;
}
// Update our background color
d.uniforms.bg_color = [
t.terminal_state.colors.background.r,
t.terminal_state.colors.background.g,
t.terminal_state.colors.background.b,
(t.config.background_opacity * 255.0).round() as u8,
];
// If we're on macOS and have glass styles, we remove
// the background opacity because the glass effect handles
// it.
#[cfg(target_os = "macos")]
if matches!(
t.config.background_blur,
BackgroundBlur::MacosGlassRegular | BackgroundBlur::MacosGlassClear
) {
d.uniforms.bg_color[3] = 0;
}
// Prepare our overlay image for upload (or unload).
d.has_overlay = t.overlay.is_some();
if let Err(err) = d.images.overlay_update(t.overlay.as_ref()) {
log::warn!("error updating overlay images err={err}");
}
// Update custom shader uniforms that depend on terminal state.
let cursor_text = t.config.cursor_text;
d.update_custom_shader_uniforms_from_state(&t.terminal_state, cursor_text);
}
// Reset our dirty state after updating.
t.terminal_state.dirty = Dirty::False;
// Notify our shaper we're done for the frame. For some shapers,
// such as CoreText, this triggers off-thread cleanup logic.
t.font_shaper.end_frame();
Ok(())
}
/// Build the overlay as configured (`rebuildOverlay`).
fn rebuild_overlay(
&self,
t: &mut ThreadState,
features: &[overlay::Feature],
) -> Result<(), overlay::InitError> {
// If we have no features enabled, don't build an overlay.
// If we had a previous overlay, deallocate it.
if features.is_empty() {
t.overlay = None;
return Ok(());
}
let size = lock(&self.draw).size;
// If we had a previous overlay, clear it. Otherwise, init.
let reuse = match &t.overlay {
Some(v) => {
// Verify that our overlay size matches our screen
// size as we know it now. If not, deinit and reinit.
let term_size = size.terminal();
// We also depend on cell size.
v.width() == term_size.width
&& v.height() == term_size.height
&& v.cell_size == size.cell
}
None => false,
};
if reuse {
// Everything matches, so we can just reset the surface
// and redraw.
if let Some(v) = &mut t.overlay {
v.reset();
}
} else {
// If we reached this point we want to reset our overlay.
t.overlay = None;
t.overlay = Some(Overlay::new(&size)?);
}
if let Some(v) = &mut t.overlay {
v.apply_features(&t.terminal_state, features);
}
Ok(())
}
}
/// Update the kitty image state from the terminal (the `kittyUpdate` call in
/// `updateFrame`).
fn update_kitty<G: GraphicsApi>(
images: &mut image::State<G>,
terminal: &mut ghostty_terminal::terminal::Terminal,
cell_size: CellSize,
) {
let info = ghostty_terminal::kitty::graphics_storage::TerminalInfo::from_terminal(terminal);
let screen = terminal.screen_mut();
images.kitty_update(&screen.kitty_images, &screen.pages, &info, cell_size);
images.force_kitty_update = false;
screen.kitty_images.dirty = false;
}
// ---------------------------------------------------------------------------
// rebuild_cells (draw lock held, renderer thread)
/// Read-only data shared by every row rebuild.
struct RowEnv<'a> {
config: &'a DerivedConfig,
colors: &'a ghostty_terminal::render::Colors,
cursor: &'a RenderCursor,
cols: u16,
}
/// Which highlight (if any) covers a cell.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
enum Selected {
False,
Selection,
Search,
SearchSelected,
}
impl<G: GraphicsApi> DrawState<G> {
/// Convert the terminal state to GPU cells stored in CPU memory. These
/// are then synced to the GPU in the next frame. This only updates CPU
/// memory and doesn't touch the GPU.
///
/// This requires the draw mutex.
///
/// Dirty state on terminal state won't be reset by this.
fn rebuild_cells(
&mut self,
t: &mut ThreadState,
preedit: Option<&Preedit>,
cursor_style_: Option<CursorStyle>,
links: &HashSet<Coordinate>,
) {
let ThreadState {
config,
terminal_state: state,
font_shaper,
font_shaper_cache,
run_cells,
shaped_cells,
..
} = t;
let grid_size_diff =
self.cells.size.rows != state.rows || self.cells.size.columns != state.cols;
if grid_size_diff {
let new_size = GridSize {
rows: state.rows,
columns: state.cols,
};
self.cells.resize(new_size);
// Update our uniforms accordingly, otherwise
// our background cells will be out of place.
self.uniforms.grid_size = [new_size.columns, new_size.rows];
}
let rebuild = state.dirty == Dirty::Full || grid_size_diff;
if rebuild {
// If we are doing a full rebuild, then we clear the entire cell buffer.
self.cells.reset();
// We also reset our padding extension depending on the screen type
match config.padding_color {
WindowPaddingColor::Background => {}
// For extension, assume we are extending in all directions.
// For "extend" this may be disabled due to heuristics below.
WindowPaddingColor::Extend | WindowPaddingColor::ExtendAlways => {
self.uniforms.padding_extend = PaddingExtend {
up: true,
down: true,
left: true,
right: true,
}
.to_bits();
}
}
}
let RenderState {
rows,
cols,
colors,
cursor,
row_data,
..
} = &mut *state;
let (rows, cols) = (*rows, *cols);
// If our cell contents buffer is shorter than the screen viewport,
// we render the rows that fit, starting from the bottom. If instead
// the viewport is shorter than the cell contents buffer, we align
// the top of the viewport with the top of the contents buffer.
let row_len = usize::from(rows.min(self.cells.size.rows));
// Determine our x/y range for preedit. We don't want to render anything
// here because we will render the preedit separately.
let preedit_range: Option<PreeditRange> = preedit.and_then(|preedit_v| {
// We base the preedit on the position of the cursor in the
// viewport. If the cursor isn't visible in the viewport we
// don't show it.
let cursor_vp = cursor.viewport?;
// If our preedit row isn't dirty then we don't need the
// preedit range. This also avoids an issue later where we
// unconditionally add preedit cells when this is set.
if !rebuild && !row_data[usize::from(cursor_vp.y)].dirty {
return None;
}
let range = preedit_v.range(cursor_vp.x, cols - 1);
Some(PreeditRange {
y: cursor_vp.y,
x: [range.start, range.end],
cp_offset: range.cp_offset,
})
});
let env = RowEnv {
config,
colors,
cursor,
cols,
};
let mut sh = ShapeCtx {
shaper: font_shaper,
cache: font_shaper_cache,
run_cells,
shaped: shaped_cells,
};
for (y_usize, row) in row_data.iter_mut().enumerate().take(row_len) {
let y = y_usize as u16;
if !rebuild {
// Only rebuild if we are doing a full rebuild or this row is dirty.
if !row.dirty {
continue;
}
// Clear the cells if the row is dirty
self.cells.clear(y);
}
// Unmark the dirty state in our render state.
row.dirty = false;
if let Err(err) = self.rebuild_row(&env, y, row, preedit_range.as_ref(), links, &mut sh)
{
// This should never happen except under exceptional
// scenarios. In this case, we don't want to corrupt
// our render state so just clear this row and keep
// trying to finish it out.
log::warn!("error building row y={y} err={err}");
self.cells.clear(y);
}
}
// Setup our cursor rendering information.
self.setup_cursor(&env, row_data, cursor_style_, preedit.is_some());
// Setup our preedit text.
if let Some(preedit_v) = preedit
&& let Some(range) = &preedit_range
{
let mut x = range.x[0];
for cp in &preedit_v.codepoints[range.cp_offset..] {
if let Err(err) = self.add_preedit_cell(
config,
cp.codepoint,
cp.wide,
Coordinate {
x,
y: u32::from(range.y),
},
colors.foreground,
) {
log::warn!(
"error building preedit cell, will be invalid x={x} y={}, err={err}",
range.y
);
}
x += if cp.wide { 2 } else { 1 };
}
}
// Update that our cells rebuilt
self.cells_rebuilt = true;
}
fn setup_cursor(
&mut self,
env: &RowEnv<'_>,
row_data: &[RenderRow],
cursor_style_: Option<CursorStyle>,
has_preedit: bool,
) {
let config = env.config;
let colors = env.colors;
// Clear our cursor by default.
self.cells.set_cursor(None, None);
self.uniforms.cursor_pos = [u16::MAX, u16::MAX];
// If the cursor isn't visible on the viewport, don't show
// a cursor. Otherwise, get our cursor cell, because we may
// need it for styling.
let Some(cursor_vp) = env.cursor.viewport else {
return;
};
let cursor_style: Style = {
let cell = row_data[usize::from(cursor_vp.y)]
.cells
.get(usize::from(cursor_vp.x));
if cell.raw.has_styling() {
cell.style
} else {
Style::default()
}
};
// If we have preedit text, we don't setup a cursor
if has_preedit {
return;
}
// If there isn't a cursor visual style requested then
// we don't render a cursor.
let Some(style) = cursor_style_ else {
return;
};
let bold = bold_policy(config.bold_color);
let fg_style = cursor_style.fg(style::Fg {
default: colors.foreground,
palette: &colors.palette,
bold,
});
let bg_style = cursor_style
.bg(&env.cursor.cell, &colors.palette)
.unwrap_or(colors.background);
// Determine the cursor color.
let cursor_color = 'color: {
// If an explicit cursor color was set by OSC 12, use that.
if let Some(v) = colors.cursor {
break 'color v;
}
// Use our configured color if specified
if let Some(v) = config.cursor_color {
break 'color resolve_terminal_color(
v,
cursor_style.flags.inverse,
Some(fg_style),
Some(bg_style),
)
.unwrap_or(colors.foreground);
}
colors.foreground
};
self.add_cursor(env, style, cursor_color);
// If the cursor is visible then we set our uniforms.
if style == CursorStyle::Block {
let wide = env.cursor.cell.wide();
self.uniforms.cursor_pos = [
// If we are a spacer tail of a wide cell, our cursor needs
// to move back one cell. The saturate is to ensure we don't
// overflow but this shouldn't happen with well-formed input.
match wide {
Wide::Narrow | Wide::SpacerHead | Wide::Wide => cursor_vp.x,
Wide::SpacerTail => cursor_vp.x.saturating_sub(1),
},
cursor_vp.y,
];
let mut bools = Bools::from_bits(self.uniforms.bools);
bools.cursor_wide = match wide {
Wide::Narrow | Wide::SpacerHead => false,
Wide::Wide | Wide::SpacerTail => true,
};
self.uniforms.bools = bools.to_bits();
let uniform_color = if let Some(txt) = config.cursor_text {
// If cursor-text is set, then compute the correct color.
// Otherwise, use the background color.
resolve_terminal_color(
txt,
cursor_style.flags.inverse,
Some(fg_style),
Some(bg_style),
)
.unwrap_or(colors.background)
} else {
colors.background
};
self.uniforms.cursor_color = [uniform_color.r, uniform_color.g, uniform_color.b, 255];
}
}
}
impl<G: GraphicsApi> DrawState<G> {
#[allow(clippy::too_many_arguments)]
fn rebuild_row(
&mut self,
env: &RowEnv<'_>,
y: u16,
row: &RenderRow,
preedit_range: Option<&PreeditRange>,
links: &HashSet<Coordinate>,
sh: &mut ShapeCtx<'_>,
) -> Result<(), Error> {
let config = env.config;
let colors = env.colors;
let cursor = env.cursor;
// If our viewport is wider than our cell contents buffer,
// we still only process cells up to the width of the buffer.
let cells = &row.cells;
let cells_len = cells.len().min(usize::from(self.cells.size.columns));
let cells_raw = &cells.raw;
// On primary screen, we still apply vertical padding
// extension under certain conditions we feel are safe.
//
// This helps make some scenarios look better while
// avoiding scenarios we know do NOT look good.
match config.padding_color {
// These already have the correct values set above.
WindowPaddingColor::Background | WindowPaddingColor::ExtendAlways => {}
// Apply heuristics for padding extension.
WindowPaddingColor::Extend => {
if y == 0 {
let mut pe = PaddingExtend::from_bits(self.uniforms.padding_extend);
pe.up = !never_extend_bg(
row.raw,
cells_raw,
|x| cells.style(x),
&colors.palette,
colors.background,
);
self.uniforms.padding_extend = pe.to_bits();
} else if y == self.cells.size.rows - 1 {
let mut pe = PaddingExtend::from_bits(self.uniforms.padding_extend);
pe.down = !never_extend_bg(
row.raw,
cells_raw,
|x| cells.style(x),
&colors.palette,
colors.background,
);
self.uniforms.padding_extend = pe.to_bits();
}
}
}
// The run iterator wants an owned-cell slice (`RunCell`).
sh.run_cells.clear();
sh.run_cells.extend(cells.iter().map(|c| RunCell {
raw: c.raw,
grapheme: c.grapheme.to_vec(),
style: c.style,
}));
// Iterator of runs for shaping.
let font_grid = Arc::clone(&self.font_grid);
let mut run_iter_opts = RunOptions::new(&font_grid, sh.run_cells.as_slice());
run_iter_opts.selection = row.selection;
// We want to do font shaping as long as the cursor is
// visible on this viewport.
run_iter_opts.cursor_x = cursor
.viewport
.filter(|vp| vp.y == y)
.map(|vp| usize::from(vp.x));
run_iter_opts.apply_break_config(config.font_shaping_break);
let mut run_iter = RunIterator::new(run_iter_opts);
let mut shaper_run: Option<TextRun> = run_iter.next(&mut *sh.shaper)?;
// `sh.shaped` holds the shaped cells of `shaper_run` when `have_cells`.
let mut have_cells = false;
let mut shaper_cells_i: usize = 0;
for x in 0..cells_len {
// If this cell falls within our preedit range then we
// skip this because preedits are setup separately.
if let Some(range) = preedit_range
&& range.y == y
&& x >= usize::from(range.x[0])
{
// We're in the preedit range, skip this cell.
if x <= usize::from(range.x[1]) {
continue;
}
// After exiting the preedit range we need to catch
// the run position up because of the missed cells.
// In all other cases, no action is necessary.
if x == usize::from(range.x[1]) + 1 {
// Step the run iterator until we find a run that ends
// after the current cell, which will be the soonest run
// that might contain glyphs for our cell.
while let Some(run) = shaper_run {
if usize::from(run.offset) + usize::from(run.cells) > x {
break;
}
shaper_run = run_iter.next(&mut *sh.shaper)?;
have_cells = false;
shaper_cells_i = 0;
}
if let Some(run) = shaper_run {
// If we haven't shaped this run, do so now.
if !have_cells {
shaped_cells_for_run(sh.shaper, sh.cache, sh.shaped, &font_grid, &run)?;
have_cells = true;
}
// Advance our index until we reach or pass
// our current x position in the shaper cells.
while shaper_cells_i < sh.shaped.len()
&& usize::from(run.offset) + usize::from(sh.shaped[shaper_cells_i].x)
< x
{
shaper_cells_i += 1;
}
}
}
}
let cell = cells_raw[x];
let wide = cell.wide();
let cell_style: Style = if cell.has_styling() {
cells.style(x)
} else {
Style::default()
};
// True if this cell is selected
let selected = 'selected: {
// Order below matters for precedence.
// Selection should take the highest precedence.
let x_compare = if wide == Wide::SpacerTail {
x.saturating_sub(1)
} else {
x
};
if let Some(sel) = row.selection
&& x_compare >= usize::from(sel[0])
&& x_compare <= usize::from(sel[1])
{
break 'selected Selected::Selection;
}
// If we're highlighted, then we're selected. In the
// future we want to use a different style for this
// but this to get started.
for hl in &row.highlights {
if x_compare >= usize::from(hl.range[0])
&& x_compare <= usize::from(hl.range[1])
{
break 'selected if hl.tag == HighlightTag::SearchMatchSelected as u8 {
Selected::SearchSelected
} else {
Selected::Search
};
}
}
Selected::False
};
// The `_style` suffixed values are the colors based on
// the cell style (SGR), before applying any additional
// configuration, inversions, selections, etc.
let bg_style = cell_style.bg(&cell, &colors.palette);
let fg_style = cell_style.fg(style::Fg {
default: colors.foreground,
palette: &colors.palette,
bold: bold_policy(config.bold_color),
});
let inverse = cell_style.flags.inverse;
// The final background color for the cell.
let bg: Option<Rgb> = match selected {
// If we have an explicit selection background color
// specified in the config, use that.
//
// If no configuration, then our selection background
// is our foreground color.
Selected::Selection => match config.selection_background {
Some(v) => resolve_terminal_color(v, inverse, Some(fg_style), bg_style),
None => Some(colors.foreground),
},
Selected::Search => resolve_terminal_color(
config.search_background,
inverse,
Some(fg_style),
bg_style,
),
Selected::SearchSelected => resolve_terminal_color(
config.search_selected_background,
inverse,
Some(fg_style),
bg_style,
),
// Not selected
Selected::False => {
if inverse == cell::is_covering(cell.codepoint()) {
// Otherwise they cancel out.
bg_style
} else {
// Two cases cause us to invert (use the fg color as the bg)
// - The "inverse" style flag.
// - A "covering" glyph; we use fg for bg in that
// case to help make sure that padding extension
// works correctly.
//
// If one of these is true (but not the other)
// then we use the fg style color for the bg.
Some(fg_style)
}
}
};
let fg: Rgb = {
// Our happy-path non-selection background color
// is our style or our configured defaults.
let final_bg = bg_style.unwrap_or(colors.background);
// Whether we need to use the bg color as our fg color:
// - Cell is selected, inverted, and set to cell-foreground
// - Cell is selected, not inverted, and set to cell-background
// - Cell is inverted and not selected
match selected {
Selected::Selection => match config.selection_foreground {
Some(v) => {
resolve_terminal_color(v, inverse, Some(fg_style), Some(final_bg))
}
None => Some(colors.background),
},
Selected::Search => resolve_terminal_color(
config.search_foreground,
inverse,
Some(fg_style),
Some(final_bg),
),
Selected::SearchSelected => resolve_terminal_color(
config.search_selected_foreground,
inverse,
Some(fg_style),
Some(final_bg),
),
Selected::False => Some(if inverse { final_bg } else { fg_style }),
}
.unwrap_or(fg_style)
};
// Foreground alpha for this cell.
let alpha: u8 = if cell_style.flags.faint {
config.faint_opacity
} else {
255
};
// Set the cell's background color.
{
let rgb = bg.unwrap_or(colors.background);
// Determine our background alpha. If we have transparency configured
// then this is dynamic depending on some situations. This is all
// in an attempt to make transparency look the best for various
// situations. See inline comments.
let bg_alpha: u8 = 'bg_alpha: {
let default: u8 = 255;
// Cells that are selected should be fully opaque.
if selected != Selected::False {
break 'bg_alpha default;
}
// Cells that are reversed should be fully opaque.
if inverse {
break 'bg_alpha default;
}
// If the user requested to have opacity on all cells, apply it.
if config.background_opacity_cells && bg_style.is_some() {
let mut opacity = f64::from(default);
opacity *= config.background_opacity;
break 'bg_alpha opacity as u8;
}
// Cells that have an explicit bg color should be fully opaque.
if bg_style.is_some() {
break 'bg_alpha default;
}
// Otherwise, we won't draw the bg for this cell,
// we'll let the already-drawn background color
// show through.
0
};
*self.cells.bg_cell(usize::from(y), x) = [rgb.r, rgb.g, rgb.b, bg_alpha];
}
// If the invisible flag is set on this cell then we
// don't need to render any foreground elements, so
// we just skip all glyphs with this x coordinate.
//
// NOTE: This behavior matches xterm. Some other terminal
// emulators, e.g. Alacritty, still render text decorations
// and only make the text itself invisible. The decision
// has been made here to match xterm's behavior for this.
if cell_style.flags.invisible {
continue;
}
// Give links a single underline, unless they already have
// an underline, in which case use a double underline to
// distinguish them.
let underline = if links.contains(&Coordinate {
x: x as u16,
y: u32::from(y),
}) {
if cell_style.flags.underline == Underline::Single {
Underline::Double
} else {
Underline::Single
}
} else {
cell_style.flags.underline
};
// We draw underlines first so that they layer underneath text.
// This improves readability when a colored underline is used
// which intersects parts of the text (descenders).
if underline != Underline::None
&& let Err(err) = self.add_underline(
x as u16,
y,
underline,
cell_style.underline_color(&colors.palette).unwrap_or(fg),
alpha,
)
{
log::warn!(
"error adding underline to cell, will be invalid x={x} y={y}, err={err}"
);
}
if cell_style.flags.overline
&& let Err(err) = self.add_overline(x as u16, y, fg, alpha)
{
log::warn!("error adding overline to cell, will be invalid x={x} y={y}, err={err}");
}
// If we're at or past the end of our shaper run then
// we need to get the next run from the run iterator.
if have_cells && shaper_cells_i >= sh.shaped.len() {
shaper_run = run_iter.next(&mut *sh.shaper)?;
have_cells = false;
shaper_cells_i = 0;
}
if let Some(run) = shaper_run {
// If we haven't shaped this run yet, do so.
if !have_cells {
shaped_cells_for_run(sh.shaper, sh.cache, sh.shaped, &font_grid, &run)?;
have_cells = true;
}
// If there are no shaper cells for this run, ignore it.
// This can occur for runs of empty cells, and is fine.
if !sh.shaped.is_empty() && shaper_cells_i < sh.shaped.len() {
// If we encounter a shaper cell to the left of the current
// cell then we have some problems. This logic relies on x
// position monotonically increasing.
debug_assert!(
usize::from(run.offset) + usize::from(sh.shaped[shaper_cells_i].x) >= x
);
// NOTE: An assumption is made here that a single cell will never
// be present in more than one shaper run. If that assumption is
// violated, this logic breaks.
while shaper_cells_i < sh.shaped.len()
&& usize::from(run.offset) + usize::from(sh.shaped[shaper_cells_i].x) == x
{
let shaped_cell = sh.shaped[shaper_cells_i];
if let Err(err) = self.add_glyph(
config,
x as u16,
y,
usize::from(env.cols),
cells_raw,
shaped_cell,
&run,
fg,
alpha,
) {
log::warn!(
"error adding glyph to cell, will be invalid x={x} y={y}, err={err}"
);
}
shaper_cells_i += 1;
}
}
}
// Finally, draw a strikethrough if necessary.
if cell_style.flags.strikethrough
&& let Err(err) = self.add_strikethrough(x as u16, y, fg, alpha)
{
log::warn!(
"error adding strikethrough to cell, will be invalid x={x} y={y}, err={err}"
);
}
}
Ok(())
}
/// Render options for a sprite glyph that spans a single cell.
fn sprite_options(&self, cell_width: u8) -> RenderOptions {
RenderOptions {
cell_width: Some(cell_width),
..RenderOptions::new(self.grid_metrics)
}
}
/// Add a sprite decoration (underline, overline, strikethrough) to the
/// specified cell.
fn add_sprite_decoration(
&mut self,
key: CellKey,
sprite: Sprite,
x: u16,
y: u16,
color: Rgb,
alpha: u8,
) -> Result<(), Error> {
let render =
self.font_grid
.render_glyph(SPRITE_INDEX, sprite as u32, self.sprite_options(1))?;
self.cells.add(
key,
CellText {
atlas: CellTextAtlas::Grayscale as u8,
grid_pos: [x, y],
color: [color.r, color.g, color.b, alpha],
glyph_pos: [render.glyph.atlas_x, render.glyph.atlas_y],
glyph_size: [render.glyph.width, render.glyph.height],
bearings: [render.glyph.offset_x as i16, render.glyph.offset_y as i16],
..CellText::default()
},
);
Ok(())
}
/// Add an underline decoration to the specified cell
fn add_underline(
&mut self,
x: u16,
y: u16,
style: Underline,
color: Rgb,
alpha: u8,
) -> Result<(), Error> {
let sprite = match style {
Underline::None => unreachable!(),
Underline::Single => Sprite::Underline,
Underline::Double => Sprite::UnderlineDouble,
Underline::Dotted => Sprite::UnderlineDotted,
Underline::Dashed => Sprite::UnderlineDashed,
Underline::Curly => Sprite::UnderlineCurly,
};
self.add_sprite_decoration(CellKey::Underline, sprite, x, y, color, alpha)
}
/// Add a overline decoration to the specified cell
fn add_overline(&mut self, x: u16, y: u16, color: Rgb, alpha: u8) -> Result<(), Error> {
self.add_sprite_decoration(CellKey::Overline, Sprite::Overline, x, y, color, alpha)
}
/// Add a strikethrough decoration to the specified cell
fn add_strikethrough(&mut self, x: u16, y: u16, color: Rgb, alpha: u8) -> Result<(), Error> {
self.add_sprite_decoration(
CellKey::Strikethrough,
Sprite::Strikethrough,
x,
y,
color,
alpha,
)
}
/// Add a glyph to the specified cell.
#[allow(clippy::too_many_arguments)]
fn add_glyph(
&mut self,
config: &DerivedConfig,
x: u16,
y: u16,
cols: usize,
cell_raws: &[PageCell],
shaper_cell: shape::Cell,
shaper_run: &TextRun,
color: Rgb,
alpha: u8,
) -> Result<(), Error> {
let cell = cell_raws[usize::from(x)];
let cp = cell.codepoint();
// Render
let render = self.font_grid.render_glyph(
shaper_run.font_index,
shaper_cell.glyph_index,
RenderOptions {
thicken: config.font_thicken,
thicken_strength: config.font_thicken_strength,
cell_width: Some(cell.grid_width()),
// If there's no Nerd Font constraint for this codepoint
// then, if it's a symbol, we constrain it to fit inside
// its cell(s), we don't modify the alignment at all.
constraint: get_constraint(cp).unwrap_or(if cell::is_symbol(cp) {
Constraint {
size: ConstraintSize::Fit,
..Constraint::NONE
}
} else {
Constraint::NONE
}),
constraint_width: cell::constraint_width(cell_raws, usize::from(x), cols),
..RenderOptions::new(self.grid_metrics)
},
)?;
// If the glyph is 0 width or height, it will be invisible
// when drawn, so don't bother adding it to the buffer.
if render.glyph.width == 0 || render.glyph.height == 0 {
return Ok(());
}
self.cells.add(
CellKey::Text,
CellText {
atlas: match render.presentation {
Presentation::Emoji => CellTextAtlas::Color as u8,
Presentation::Text => CellTextAtlas::Grayscale as u8,
},
bools: if cell::no_min_contrast(cp) {
CellText::NO_MIN_CONTRAST
} else {
0
},
grid_pos: [x, y],
color: [color.r, color.g, color.b, alpha],
glyph_pos: [render.glyph.atlas_x, render.glyph.atlas_y],
glyph_size: [render.glyph.width, render.glyph.height],
bearings: [
(render.glyph.offset_x + i32::from(shaper_cell.x_offset)) as i16,
(render.glyph.offset_y + i32::from(shaper_cell.y_offset)) as i16,
],
..CellText::default()
},
);
Ok(())
}
fn add_cursor(&mut self, env: &RowEnv<'_>, cursor_style: CursorStyle, cursor_color: Rgb) {
let cursor_state = env.cursor;
let Some(cursor_vp) = cursor_state.viewport else {
return;
};
// Add the cursor. We render the cursor over the wide character if
// we're on the wide character tail.
let (wide, x) = if cursor_vp.wide_tail {
// If we're part of a wide character, we move the cursor back
// to the actual character.
(true, cursor_vp.x.saturating_sub(1))
} else {
// The cursor goes over the screen cursor position.
(cursor_state.cell.wide() == Wide::Wide, cursor_vp.x)
};
let alpha: u8 = if self.focused {
(255.0 * env.config.cursor_opacity).ceil() as u8
} else {
255
};
let render = match cursor_style {
CursorStyle::Block
| CursorStyle::BlockHollow
| CursorStyle::Bar
| CursorStyle::Underline => {
let sprite = match cursor_style {
CursorStyle::Block => Sprite::CursorRect,
CursorStyle::BlockHollow => Sprite::CursorHollowRect,
CursorStyle::Bar => Sprite::CursorBar,
CursorStyle::Underline => Sprite::CursorUnderline,
CursorStyle::Lock => unreachable!(),
};
match self.font_grid.render_glyph(
SPRITE_INDEX,
sprite as u32,
self.sprite_options(if wide { 2 } else { 1 }),
) {
Ok(r) => r,
Err(err) => {
log::warn!("error rendering cursor glyph err={err}");
return;
}
}
}
CursorStyle::Lock => {
match self.font_grid.render_codepoint(
0xF023, // lock symbol
FontStyle::Regular,
Some(Presentation::Text),
self.sprite_options(if wide { 2 } else { 1 }),
) {
Ok(Some(r)) => r,
Ok(None) => {
// This should never happen because we embed nerd
// fonts so we just log and return instead of fallback.
log::warn!("failed to find lock symbol for cursor codepoint=0xF023");
return;
}
Err(err) => {
log::warn!("error rendering cursor glyph err={err}");
return;
}
}
}
};
self.cells.set_cursor(
Some(CellText {
atlas: CellTextAtlas::Grayscale as u8,
bools: CellText::IS_CURSOR_GLYPH,
grid_pos: [x, cursor_vp.y],
color: [cursor_color.r, cursor_color.g, cursor_color.b, alpha],
glyph_pos: [render.glyph.atlas_x, render.glyph.atlas_y],
glyph_size: [render.glyph.width, render.glyph.height],
bearings: [render.glyph.offset_x as i16, render.glyph.offset_y as i16],
..CellText::default()
}),
Some(cursor_style),
);
}
fn add_preedit_cell(
&mut self,
config: &DerivedConfig,
codepoint: u32,
wide: bool,
coord: Coordinate,
screen_fg: Rgb,
) -> Result<(), Error> {
let _ = config;
// Render the glyph for our preedit text
let render = match self.font_grid.render_codepoint(
codepoint,
FontStyle::Regular,
Some(Presentation::Text),
RenderOptions::new(self.grid_metrics),
) {
Ok(Some(r)) => r,
Ok(None) => {
log::warn!("failed to find font for preedit codepoint={codepoint:X}");
return Ok(());
}
Err(err) => {
log::warn!("error rendering preedit glyph err={err}");
return Ok(());
}
};
// Add our text
self.cells.add(
CellKey::Text,
CellText {
atlas: CellTextAtlas::Grayscale as u8,
grid_pos: [coord.x, coord.y as u16],
color: [screen_fg.r, screen_fg.g, screen_fg.b, 255],
glyph_pos: [render.glyph.atlas_x, render.glyph.atlas_y],
glyph_size: [render.glyph.width, render.glyph.height],
bearings: [render.glyph.offset_x as i16, render.glyph.offset_y as i16],
..CellText::default()
},
);
// Add underline
self.add_underline(coord.x, coord.y as u16, Underline::Single, screen_fg, 255)?;
if wide && coord.x < self.cells.size.columns - 1 {
self.add_underline(
coord.x + 1,
coord.y as u16,
Underline::Single,
screen_fg,
255,
)?;
}
Ok(())
}
}
// ---------------------------------------------------------------------------
// draw_frame (GL thread)
impl<G: GraphicsApi> Renderer<G> {
/// Draw the frame to the screen.
///
/// If `sync` is true, this will synchronously block until
/// the frame is finished drawing and has been presented.
///
/// Must be called with the GL context current (for GTK: from the
/// `GtkGLArea` `render` signal, on the main thread).
pub fn draw_frame(&self, sync: bool) -> Result<(), Error> {
// We hold a the draw mutex to prevent changes to any
// data we access while we're in the middle of drawing.
let mut d = lock(&self.draw);
let result = self.draw_frame_locked(&mut d, sync);
// After the graphics API is complete (so we defer) we want to
// update our scrollbar state.
if d.scrollbar_dirty {
// Fail instantly if the surface mailbox if full, we'll just
// get it on the next frame.
if self.host.scrollbar(d.scrollbar) {
d.scrollbar_dirty = false;
}
}
// Let our graphics API do any bookkeeping, etc.
// that it needs to do before / after `draw_frame`.
d.api.draw_frame_end();
result
}
fn draw_frame_locked(&self, d: &mut DrawState<G>, sync: bool) -> Result<(), Error> {
d.api.draw_frame_start();
// Retrieve the most up-to-date surface size from the Graphics API
let (surface_width, surface_height) = d.api.surface_size()?;
// If either of our surface dimensions is zero
// then drawing is absurd, so we just return.
if surface_width == 0 || surface_height == 0 {
return Ok(());
}
let size_changed =
d.size.screen.width != surface_width || d.size.screen.height != surface_height;
// Conditions under which we need to draw the frame, otherwise we
// don't need to since the previous frame should be identical.
let needs_redraw = size_changed || d.cells_rebuilt || self.has_animations() || sync;
if !needs_redraw {
// We still need to present the last target again, because the
// apprt may be swapping buffers and display an outdated frame
// if we don't draw something new.
d.api.present_last_target()?;
return Ok(());
}
d.cells_rebuilt = false;
// Get a frame state to draw to.
let (idx, mut frame) = d.swap_chain.next_frame()?;
let result = d.draw_with_frame(&mut frame, (surface_width, surface_height), size_changed);
let frame_ctx = match result {
Ok(frame_ctx) => frame_ctx,
Err(err) => {
// Nothing was submitted: give the frame state back.
d.swap_chain.release_frame(idx, frame);
return Err(err);
}
};
self.has_animations
.store(d.has_custom_shaders, Ordering::Relaxed);
// Complete the frame. The frame state is handed back to the swap
// chain, and the health reported, when the frame completes
// (`frameCompleted`): inline for synchronous frames, from the
// graphics API's completion thread otherwise.
let report = self.frame_completion();
let swap_chain = d.swap_chain.releaser();
let pending = Mutex::new(Some(frame));
let completion: FrameCompletion = Arc::new(move |health| {
report(health);
if let Some(frame) = lock(&pending).take() {
SwapChainInner::release(&swap_chain, idx, frame);
}
});
frame_ctx.complete(sync, completion);
Ok(())
}
}
impl<G: GraphicsApi> DrawState<G> {
/// Encode a frame into `frame`'s buffers and the swap chain's targets.
/// The returned graphics API frame has not been completed yet.
fn draw_with_frame(
&mut self,
frame: &mut FrameState<G>,
surface_size: (u32, u32),
size_changed: bool,
) -> Result<G::Frame, Error> {
let (surface_width, surface_height) = surface_size;
// If we need to reinitialize our shaders, do so.
if self.reinitialize_shaders {
self.reinitialize_shaders = false;
self.shaders = None;
self.init_shaders()?;
}
// Our shaders should not be defunct at this point.
if self.shaders.is_none() {
return Err(Error::Defunct);
}
// If we have custom shaders, make sure we have the
// custom shader state in our frame state, otherwise
// if we have a state but don't need it we remove it.
if self.has_custom_shaders {
if frame.custom_shader_state.is_none() {
let mut state = CustomShaderState::<G>::new(&self.api)?;
state.resize(&self.api, surface_width as usize, surface_height as usize)?;
frame.custom_shader_state = Some(state);
}
} else {
frame.custom_shader_state = None;
}
// If our stored size doesn't match the
// surface size we need to update it.
if size_changed {
self.size.screen = crate::size::ScreenSize {
width: surface_width,
height: surface_height,
};
self.update_screen_size_uniforms();
}
// If this frame's target isn't the correct size, or the target
// config has changed (such as when the blending mode changes),
// remove it and replace it with a new one with the right values.
if frame.target.width() != self.size.screen.width as usize
|| frame.target.height() != self.size.screen.height as usize
|| frame.target_config_modified != self.target_config_modified
{
frame.resize(
&self.api,
self.size.screen.width as usize,
self.size.screen.height as usize,
)?;
frame.target_config_modified = self.target_config_modified;
// The remembered last target (if any) is the old one.
self.api.clear_last_target();
}
{
let gpu = Gpu { api: &self.api };
// Upload images to the GPU as necessary.
let _ = self.images.upload(&gpu);
// Upload the background image to the GPU as necessary.
upload_background_image(&mut self.bg_image, &gpu);
}
// Update per-frame custom shader uniforms.
self.update_custom_shader_uniforms_for_frame();
// Setup our frame data
frame.uniforms.sync(&[G::gpu_uniforms(&self.uniforms)])?;
frame.cells_bg.sync(&self.cells.bg_cells)?;
let fg_lists: Vec<&[CellText]> = self.cells.fg_rows.iter().map(Vec::as_slice).collect();
let fg_count = frame.cells.sync_from_slices(&fg_lists)?;
drop(fg_lists);
// If our background image buffer has changed, sync it.
if frame.bg_image_buffer_modified != self.bg_image_buffer_modified {
frame.bg_image_buffer.sync(&[self.bg_image_buffer])?;
frame.bg_image_buffer_modified = self.bg_image_buffer_modified;
}
// If our font atlas changed, sync the texture data
{
let grid = Arc::clone(&self.font_grid);
let modified = grid.read().atlas_grayscale.modified.load(Ordering::Relaxed);
if modified > frame.grayscale_modified {
let guard = grid.read();
frame.grayscale_modified = guard.atlas_grayscale.modified.load(Ordering::Relaxed);
DrawState::sync_atlas_texture(
&self.api,
&guard.atlas_grayscale,
&mut frame.grayscale,
)?;
}
let modified = grid.read().atlas_color.modified.load(Ordering::Relaxed);
if modified > frame.color_modified {
let guard = grid.read();
frame.color_modified = guard.atlas_color.modified.load(Ordering::Relaxed);
DrawState::sync_atlas_texture(&self.api, &guard.atlas_color, &mut frame.color)?;
}
}
// Sync the custom shader uniforms now while we can still use `self`.
if let Some(state) = &mut frame.custom_shader_state {
state.uniforms.sync(&[self.custom_shader_uniforms])?;
}
// Get a frame context from the graphics API.
let DrawState {
api,
shaders,
images,
bg_image,
has_overlay,
..
} = &*self;
let Some(shaders) = shaders.as_ref() else {
return Err(Error::Defunct);
};
let gpu = Gpu { api };
let frame_ctx = api.begin_frame(Arc::clone(&frame.target));
{
let attachments = [Attachment {
target: match &frame.custom_shader_state {
Some(state) => AttachmentTarget::Texture(&state.back_texture),
None => AttachmentTarget::Target(&*frame.target),
},
clear_color: Some([0.0, 0.0, 0.0, 0.0]),
}];
let mut pass = frame_ctx.render_pass(&attachments);
// First we draw our background image, if we have one.
// The bg image shader also draws the main bg color.
//
// Otherwise, if we don't have a background image, we
// draw the background color by itself in its own step.
//
// NOTE: We don't use the clear_color for this because that
// would require us to do color space conversion on the
// CPU-side. In the future when we have utilities for
// that we should remove this step and use clear_color.
match bg_image {
Some(img) => {
if let image::Image::Ready(texture) = img {
pass.step(&Step {
pipeline: shaders.bg_image(),
uniforms: Some(frame.uniforms.raw()),
buffers: &[Some(frame.bg_image_buffer.raw())],
textures: &[Some(texture)],
samplers: &[],
draw: Draw::new(Primitive::Triangle, 3),
});
}
}
None => {
pass.step(&Step {
pipeline: shaders.bg_color(),
uniforms: Some(frame.uniforms.raw()),
buffers: &[None, Some(frame.cells_bg.raw())],
textures: &[],
samplers: &[],
draw: Draw::new(Primitive::Triangle, 3),
});
}
}
// Then we draw any kitty images that need
// to be behind text AND cell backgrounds.
images.draw(
&gpu,
shaders.image(),
&mut pass,
image::DrawPlacements::KittyBelowBg,
);
// Then we draw any opaque cell backgrounds.
pass.step(&Step {
pipeline: shaders.cell_bg(),
uniforms: Some(frame.uniforms.raw()),
buffers: &[None, Some(frame.cells_bg.raw())],
textures: &[],
samplers: &[],
draw: Draw::new(Primitive::Triangle, 3),
});
// Kitty images between cell backgrounds and text.
images.draw(
&gpu,
shaders.image(),
&mut pass,
image::DrawPlacements::KittyBelowText,
);
// Text.
pass.step(&Step {
pipeline: shaders.cell_text(),
uniforms: Some(frame.uniforms.raw()),
buffers: &[Some(frame.cells.raw()), Some(frame.cells_bg.raw())],
textures: &[Some(&frame.grayscale), Some(&frame.color)],
samplers: &[],
draw: Draw {
ty: Primitive::TriangleStrip,
vertex_count: 4,
instance_count: fg_count,
},
});
// Kitty images in front of text.
images.draw(
&gpu,
shaders.image(),
&mut pass,
image::DrawPlacements::KittyAboveText,
);
// Debug overlay. We do this before any custom shader state
// because our debug overlay is aligned with the grid.
if *has_overlay {
images.draw(
&gpu,
shaders.image(),
&mut pass,
image::DrawPlacements::Overlay,
);
}
pass.complete();
}
// If we have custom shaders, then we render them.
if let Some(state) = &mut frame.custom_shader_state {
let post_count = shaders.post_pipelines().len();
for (i, pipeline) in shaders.post_pipelines().iter().enumerate() {
{
let attachments = [Attachment {
target: if i < post_count - 1 {
AttachmentTarget::Texture(&state.front_texture)
} else {
AttachmentTarget::Target(&*frame.target)
},
clear_color: Some([0.0, 0.0, 0.0, 0.0]),
}];
let mut pass = frame_ctx.render_pass(&attachments);
pass.step(&Step {
pipeline,
uniforms: Some(state.uniforms.raw()),
buffers: &[],
textures: &[Some(&state.back_texture)],
samplers: &[Some(&state.sampler)],
draw: Draw::new(Primitive::Triangle, 3),
});
pass.complete();
}
state.swap();
}
}
// The caller completes the frame (glFinish, error check, present).
Ok(frame_ctx)
}
}
#[cfg(test)]
mod tests {
use ghostty_config::types_a::Color as ConfigColor;
use super::*;
fn rgb(r: u8, g: u8, b: u8) -> Rgb {
Rgb { r, g, b }
}
#[test]
fn derived_config_from_default_config() {
let config = Config::default();
let d = DerivedConfig::new(&config).unwrap();
// The default config installs the URL link.
assert!(!d.links.links.is_empty());
assert_eq!(d.background, config.background.to_terminal_rgb());
assert_eq!(d.foreground, config.foreground.to_terminal_rgb());
assert!((d.background_opacity - 1.0).abs() < f64::EPSILON);
// faint-opacity 0.5 -> ceil(127.5) = 128
assert_eq!(d.faint_opacity, 128);
assert_eq!(d.font_styles, [true; 4]);
assert_eq!(d.custom_shaders.len(), 0);
assert!(d.bg_image.is_none());
assert!(!d.scroll_to_bottom_on_output);
}
#[test]
fn derived_config_clamps_opacity() {
let config = Config {
background_opacity: 3.0,
cursor_opacity: -1.0,
..Config::default()
};
let d = DerivedConfig::new(&config).unwrap();
assert!((d.background_opacity - 1.0).abs() < f64::EPSILON);
assert!(d.cursor_opacity.abs() < f64::EPSILON);
}
#[test]
fn terminal_color_resolution() {
let fg = Some(rgb(1, 1, 1));
let bg = Some(rgb(2, 2, 2));
let explicit = TerminalColor::Color(ConfigColor { r: 9, g: 9, b: 9 });
assert_eq!(
resolve_terminal_color(explicit, false, fg, bg),
Some(rgb(9, 9, 9))
);
assert_eq!(
resolve_terminal_color(TerminalColor::CellForeground, false, fg, bg),
fg
);
assert_eq!(
resolve_terminal_color(TerminalColor::CellForeground, true, fg, bg),
bg
);
assert_eq!(
resolve_terminal_color(TerminalColor::CellBackground, false, fg, bg),
bg
);
assert_eq!(
resolve_terminal_color(TerminalColor::CellBackground, true, fg, bg),
fg
);
}
#[test]
fn bold_color_policy() {
assert!(bold_policy(None).is_none());
assert!(matches!(
bold_policy(Some(BoldColor::Bright)),
Some(style::BoldColor::Bright)
));
let c = ConfigColor { r: 1, g: 2, b: 3 };
assert!(matches!(
bold_policy(Some(BoldColor::Color(c))),
Some(style::BoldColor::Color(Rgb { r: 1, g: 2, b: 3 }))
));
}
#[test]
fn file_type_detection() {
assert_eq!(
detect_file_type(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0]),
FileType::Png
);
assert_eq!(detect_file_type(&[0xFF, 0xD8, 0xFF, 0xE0]), FileType::Jpeg);
assert_eq!(detect_file_type(b"GIF89a.."), FileType::Gif);
assert_eq!(detect_file_type(b"hello"), FileType::Unknown);
assert_eq!(
guess_file_type_from_extension("/a/b.JPG".as_ref()),
FileType::Jpeg
);
assert_eq!(
guess_file_type_from_extension("/a/b.png".as_ref()),
FileType::Png
);
assert_eq!(
guess_file_type_from_extension("/a/b".as_ref()),
FileType::Unknown
);
}
#[test]
fn background_image_png_roundtrip() {
// Encode a 2x1 RGBA png, write it to disk and load it back.
let mut bytes = Vec::new();
{
let mut enc = png::Encoder::new(&mut bytes, 2, 1);
enc.set_color(png::ColorType::Rgba);
enc.set_depth(png::BitDepth::Eight);
let mut w = enc.write_header().unwrap();
w.write_image_data(&[1, 2, 3, 255, 4, 5, 6, 128]).unwrap();
}
let dir = std::env::temp_dir().join(format!("ghostty-bg-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("bg.png");
std::fs::write(&path, &bytes).unwrap();
let img = load_background_image(&path).expect("decoded");
assert_eq!((img.width, img.height), (2, 1));
assert_eq!(img.data, [1, 2, 3, 255, 4, 5, 6, 128]);
// Missing / undecodable files are not an error, just no image.
assert!(load_background_image(&dir.join("missing.png")).is_none());
let bad = dir.join("bad.txt");
std::fs::write(&bad, b"not an image").unwrap();
assert!(load_background_image(&bad).is_none());
std::fs::remove_dir_all(&dir).unwrap();
}
}