custom shaders: add colorscheme information to shader uniforms (#9656)

#9417
Adds palette and color scheme uniforms to custom shaders, allowing
custom shaders to access terminal color information:

  - iPalette[256]: Full 256-color terminal palette (RGB)
  - iBackgroundColor, iForegroundColor: Terminal colors (RGB)
  - iCursorColor, iCursorText: Cursor colors (RGB)
- iSelectionBackgroundColor, iSelectionForegroundColor: Selection colors
(RGB)

Colors are normalized to [0.0, 1.0] range and update when the palette
changes via OSC sequences or configuration changes. The palette_dirty
flag tracks when colors need to be refreshed, initialized to true to
ensure correct colors on new surfaces.
This commit is contained in:
Mitchell Hashimoto
2026-01-20 11:41:08 -08:00
committed by GitHub
4 changed files with 151 additions and 24 deletions

View File

@@ -2883,6 +2883,24 @@ keybind: Keybinds = .{},
/// (e.g., modifier key presses, link hover events in unfocused split panes).
/// Check `iFocus > 0` to determine if the surface is currently focused.
///
/// * `vec3 iPalette[256]` - The 256-color terminal palette.
///
/// RGB values for all 256 colors in the terminal palette, normalized
/// to [0.0, 1.0]. Index 0-15 are the ANSI colors, 16-231 are the 6x6x6
/// color cube, and 232-255 are the grayscale colors.
///
/// * `vec3 iBackgroundColor` - Terminal background color (RGB).
///
/// * `vec3 iForegroundColor` - Terminal foreground color (RGB).
///
/// * `vec3 iCursorColor` - Terminal cursor color (RGB).
///
/// * `vec3 iCursorText` - Terminal cursor text color (RGB).
///
/// * `vec3 iSelectionBackgroundColor` - Selection background color (RGB).
///
/// * `vec3 iSelectionForegroundColor` - Selection foreground color (RGB).
///
/// If the shader fails to compile, the shader will be ignored. Any errors
/// related to shader compilation will not show up as configuration errors
/// and only show up in the log, since shader compilation happens after

View File

@@ -231,7 +231,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
search_match,
search_match_selected,
};
/// 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.
@@ -752,6 +751,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.cursor_change_time = 0,
.time_focus = 0,
.focus = 1, // assume focused initially
.palette = @splat(@splat(0)),
.background_color = @splat(0),
.foreground_color = @splat(0),
.cursor_color = @splat(0),
.cursor_text = @splat(0),
.selection_background_color = @splat(0),
.selection_foreground_color = @splat(0),
},
.bg_image_buffer = undefined,
@@ -1276,26 +1282,25 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
}
// Build our GPU cells
try self.rebuildCells(
critical.preedit,
renderer.cursorStyle(&self.terminal_state, .{
.preedit = critical.preedit != null,
.focused = self.focused,
.blink_visible = cursor_blink_visible,
}),
&critical.links,
);
// Reset our dirty state after updating.
defer self.terminal_state.dirty = .false;
// Notify our shaper we're done for the frame. For some shapers,
// such as CoreText, this triggers off-thread cleanup logic.
self.font_shaper.endFrame();
// Acquire the draw mutex because we're modifying state here.
// Acquire the draw mutex for all remaining state updates.
{
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
// Build our GPU cells
try self.rebuildCells(
critical.preedit,
renderer.cursorStyle(&self.terminal_state, .{
.preedit = critical.preedit != null,
.focused = self.focused,
.blink_visible = cursor_blink_visible,
}),
&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.
@@ -1322,7 +1327,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
else => {},
};
// Update custom shader uniforms that depend on terminal state.
self.updateCustomShaderUniformsFromState();
}
// Notify our shaper we're done for the frame. For some shapers,
// such as CoreText, this triggers off-thread cleanup logic.
self.font_shaper.endFrame();
}
/// Draw the frame to the screen.
@@ -1444,8 +1456,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Upload the background image to the GPU as necessary.
try self.uploadBackgroundImage();
// Update custom shader uniforms if necessary.
try self.updateCustomShaderUniforms();
// Update per-frame custom shader uniforms.
try self.updateCustomShaderUniformsForFrame();
// Setup our frame data
try frame.uniforms.sync(&.{self.uniforms});
@@ -2259,10 +2271,93 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.bg_image_buffer_modified +%= 1;
}
/// Update uniforms for the custom shaders, if necessary.
/// Update custom shader uniforms that depend on terminal state.
///
/// This should be called in `updateFrame` when terminal state changes.
fn updateCustomShaderUniformsFromState(self: *Self) void {
// 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 (self.terminal_state.dirty == .false) return;
const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors;
// 256-color palette
for (colors.palette, 0..) |color, i| {
self.custom_shader_uniforms.palette[i] = .{
@as(f32, @floatFromInt(color.r)) / 255.0,
@as(f32, @floatFromInt(color.g)) / 255.0,
@as(f32, @floatFromInt(color.b)) / 255.0,
1.0,
};
}
// Background color
self.custom_shader_uniforms.background_color = .{
@as(f32, @floatFromInt(colors.background.r)) / 255.0,
@as(f32, @floatFromInt(colors.background.g)) / 255.0,
@as(f32, @floatFromInt(colors.background.b)) / 255.0,
1.0,
};
// Foreground color
self.custom_shader_uniforms.foreground_color = .{
@as(f32, @floatFromInt(colors.foreground.r)) / 255.0,
@as(f32, @floatFromInt(colors.foreground.g)) / 255.0,
@as(f32, @floatFromInt(colors.foreground.b)) / 255.0,
1.0,
};
// Cursor color
if (colors.cursor) |cursor_color| {
self.custom_shader_uniforms.cursor_color = .{
@as(f32, @floatFromInt(cursor_color.r)) / 255.0,
@as(f32, @floatFromInt(cursor_color.g)) / 255.0,
@as(f32, @floatFromInt(cursor_color.b)) / 255.0,
1.0,
};
}
// 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
// Cursor text color
if (self.config.cursor_text) |cursor_text| {
self.custom_shader_uniforms.cursor_text = .{
@as(f32, @floatFromInt(cursor_text.color.r)) / 255.0,
@as(f32, @floatFromInt(cursor_text.color.g)) / 255.0,
@as(f32, @floatFromInt(cursor_text.color.b)) / 255.0,
1.0,
};
}
// Selection background color
if (self.config.selection_background) |selection_bg| {
self.custom_shader_uniforms.selection_background_color = .{
@as(f32, @floatFromInt(selection_bg.color.r)) / 255.0,
@as(f32, @floatFromInt(selection_bg.color.g)) / 255.0,
@as(f32, @floatFromInt(selection_bg.color.b)) / 255.0,
1.0,
};
}
// Selection foreground color
if (self.config.selection_foreground) |selection_fg| {
self.custom_shader_uniforms.selection_foreground_color = .{
@as(f32, @floatFromInt(selection_fg.color.r)) / 255.0,
@as(f32, @floatFromInt(selection_fg.color.g)) / 255.0,
@as(f32, @floatFromInt(selection_fg.color.b)) / 255.0,
1.0,
};
}
}
/// Update per-frame custom shader uniforms.
///
/// This should be called exactly once per frame, inside `drawFrame`.
fn updateCustomShaderUniforms(self: *Self) !void {
fn updateCustomShaderUniformsForFrame(self: *Self) !void {
// We only need to do this if we have custom shaders.
if (!self.has_custom_shaders) return;
@@ -2388,6 +2483,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// 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 rebuildCells(
self: *Self,
preedit: ?renderer.State.Preedit,
@@ -2395,10 +2494,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
links: *const terminal.RenderState.CellSet,
) !void {
const state: *terminal.RenderState = &self.terminal_state;
defer state.dirty = .false;
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
// const start = try std.time.Instant.now();
// const start_micro = std.time.microTimestamp();

View File

@@ -18,6 +18,13 @@ layout(binding = 1, std140) uniform Globals {
uniform float iTimeCursorChange;
uniform float iTimeFocus;
uniform int iFocus;
uniform vec3 iPalette[256];
uniform vec3 iBackgroundColor;
uniform vec3 iForegroundColor;
uniform vec3 iCursorColor;
uniform vec3 iCursorText;
uniform vec3 iSelectionForegroundColor;
uniform vec3 iSelectionBackgroundColor;
};
layout(binding = 0) uniform sampler2D iChannel0;

View File

@@ -27,6 +27,13 @@ pub const Uniforms = extern struct {
cursor_change_time: f32 align(4),
time_focus: f32 align(4),
focus: i32 align(4),
palette: [256][4]f32 align(16),
background_color: [4]f32 align(16),
foreground_color: [4]f32 align(16),
cursor_color: [4]f32 align(16),
cursor_text: [4]f32 align(16),
selection_background_color: [4]f32 align(16),
selection_foreground_color: [4]f32 align(16),
};
/// The target to load shaders for.