From 8d2eb280dbad06790a207edf5ca8f5305aafe678 Mon Sep 17 00:00:00 2001 From: ClearAspect Date: Thu, 20 Nov 2025 15:47:56 -0500 Subject: [PATCH 1/3] custom shaders: add colorscheme information to shader uniforms 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. --- src/config/Config.zig | 18 ++++ src/renderer/generic.zig | 100 ++++++++++++++++++++- src/renderer/shaders/shadertoy_prefix.glsl | 7 ++ src/renderer/shadertoy.zig | 7 ++ 4 files changed, 131 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 6a846b17f..8ca64efe9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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 diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 4181badc3..b70dca364 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -227,11 +227,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// a large screen. terminal_state_frame_count: usize = 0, + /// Captured dirty for reference after updateFrame() clears the flag + /// To be used for shader uniforms. + /// + /// Initialized to true because we need to get the correct palette even on + /// a new Surface. Otherwise we end up with it initialized to 0's + palette_dirty: bool = true, + const HighlightTag = enum(u8) { 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 +758,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, @@ -1209,6 +1222,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; }; + self.palette_dirty |= state.terminal.flags.dirty.palette; + break :critical .{ .links = links, .mouse = state.mouse, @@ -2300,6 +2315,89 @@ pub fn Renderer(comptime GraphicsAPI: type) type { 0, }; + // Renderer state required for getting the colors + const state: *renderer.State = &self.surface_mailbox.surface.renderer_state; + + // (Updates on OSC sequence changes and configuration changes) + if (self.palette_dirty) { + + // 256-color palette + for (state.terminal.colors.palette.current, 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 + if (state.terminal.colors.background.get()) |bg| { + self.custom_shader_uniforms.background_color = .{ + @as(f32, @floatFromInt(bg.r)) / 255.0, + @as(f32, @floatFromInt(bg.g)) / 255.0, + @as(f32, @floatFromInt(bg.b)) / 255.0, + 1.0, + }; + } + + // Foreground color + if (state.terminal.colors.foreground.get()) |fg| { + self.custom_shader_uniforms.foreground_color = .{ + @as(f32, @floatFromInt(fg.r)) / 255.0, + @as(f32, @floatFromInt(fg.g)) / 255.0, + @as(f32, @floatFromInt(fg.b)) / 255.0, + 1.0, + }; + } + + // Cursor color + if (state.terminal.colors.cursor.get()) |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) |seletion_bg| { + self.custom_shader_uniforms.selection_background_color = .{ + @as(f32, @floatFromInt(seletion_bg.color.r)) / 255.0, + @as(f32, @floatFromInt(seletion_bg.color.g)) / 255.0, + @as(f32, @floatFromInt(seletion_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, + }; + } + + self.palette_dirty = false; + } + // Update custom cursor uniforms, if we have a cursor. if (self.cells.getCursorGlyph()) |cursor| { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index c984334f4..661bd233d 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -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; diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index f71200610..7d0ad4b0a 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -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. From 87867459690de41063ac4f065a600d1b800442f5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 11:24:25 -0800 Subject: [PATCH 2/3] renderer: don't access shared state for custom shader color palettes --- src/renderer/generic.zig | 44 +++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index b70dca364..6f5a3d2ca 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2315,14 +2315,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { 0, }; - // Renderer state required for getting the colors - const state: *renderer.State = &self.surface_mailbox.surface.renderer_state; - // (Updates on OSC sequence changes and configuration changes) if (self.palette_dirty) { + const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors; // 256-color palette - for (state.terminal.colors.palette.current, 0..) |color, i| { + 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, @@ -2332,27 +2330,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Background color - if (state.terminal.colors.background.get()) |bg| { - self.custom_shader_uniforms.background_color = .{ - @as(f32, @floatFromInt(bg.r)) / 255.0, - @as(f32, @floatFromInt(bg.g)) / 255.0, - @as(f32, @floatFromInt(bg.b)) / 255.0, - 1.0, - }; - } + 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 - if (state.terminal.colors.foreground.get()) |fg| { - self.custom_shader_uniforms.foreground_color = .{ - @as(f32, @floatFromInt(fg.r)) / 255.0, - @as(f32, @floatFromInt(fg.g)) / 255.0, - @as(f32, @floatFromInt(fg.b)) / 255.0, - 1.0, - }; - } + 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 (state.terminal.colors.cursor.get()) |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, @@ -2376,11 +2370,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Selection background color - if (self.config.selection_background) |seletion_bg| { + if (self.config.selection_background) |selection_bg| { self.custom_shader_uniforms.selection_background_color = .{ - @as(f32, @floatFromInt(seletion_bg.color.r)) / 255.0, - @as(f32, @floatFromInt(seletion_bg.color.g)) / 255.0, - @as(f32, @floatFromInt(seletion_bg.color.b)) / 255.0, + @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, }; } From 7204f7ef9e70d0a87aef95903399f60d8071579a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 11:31:43 -0800 Subject: [PATCH 3/3] renderer: remove palette_dirty, rely on terminal state --- src/renderer/generic.zig | 221 ++++++++++++++++++++------------------- 1 file changed, 112 insertions(+), 109 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 6f5a3d2ca..f3c0556f5 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -227,13 +227,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// a large screen. terminal_state_frame_count: usize = 0, - /// Captured dirty for reference after updateFrame() clears the flag - /// To be used for shader uniforms. - /// - /// Initialized to true because we need to get the correct palette even on - /// a new Surface. Otherwise we end up with it initialized to 0's - palette_dirty: bool = true, - const HighlightTag = enum(u8) { search_match, search_match_selected, @@ -1222,8 +1215,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; }; - self.palette_dirty |= state.terminal.flags.dirty.palette; - break :critical .{ .links = links, .mouse = state.mouse, @@ -1291,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. @@ -1337,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. @@ -1459,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}); @@ -2274,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; @@ -2315,83 +2395,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { 0, }; - // (Updates on OSC sequence changes and configuration changes) - if (self.palette_dirty) { - 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, - }; - } - - self.palette_dirty = false; - } - // Update custom cursor uniforms, if we have a cursor. if (self.cells.getCursorGlyph()) |cursor| { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); @@ -2480,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, @@ -2487,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();