Custom Shader Cursor Uniforms (#7648)

Supersedes #6912, implements #6901

Also included in this PR is a fix/cleanup of the custom shader uniform
handling, moved the CPU-side custom shader uniforms struct to the main
renderer struct instead of having it be per-frame, moved the layout
struct to `shadertoy.zig` since it has the `std140` layout for both
backends.

Also, I added the current/previous cursor colors to the uniforms, since
I figured they'd be useful to have and it's a trivial addition.

### Future Work
- This extension to the shadertoy uniforms needs to be documented
somewhere so it's discoverable by users.
- The flipped Y axis on Metal still needs to be fully addressed instead
of just being patched over like it is right now.
This commit is contained in:
Qwerasd
2025-06-22 12:04:12 -06:00
committed by GitHub
8 changed files with 165 additions and 82 deletions

View File

@@ -50,7 +50,11 @@ pub fn renderGlyph(
const region = try canvas.writeAtlas(alloc, atlas); const region = try canvas.writeAtlas(alloc, atlas);
return font.Glyph{ return font.Glyph{
.width = width, // HACK: Set the width for the bar cursor to just the thickness,
// this is just for the benefit of the custom shader cursor
// uniform code. -- In the future code will be introduced to
// auto-crop the canvas so that this isn't needed.
.width = if (sprite == .cursor_bar) thickness else width,
.height = height, .height = height,
.offset_x = 0, .offset_x = 0,
.offset_y = @intCast(height), .offset_y = @intCast(height),

View File

@@ -33,6 +33,8 @@ pub const cellpkg = @import("metal/cell.zig");
pub const imagepkg = @import("metal/image.zig"); pub const imagepkg = @import("metal/image.zig");
pub const custom_shader_target: shadertoy.Target = .msl; pub const custom_shader_target: shadertoy.Target = .msl;
// The fragCoord for Metal shaders is +Y = down.
pub const custom_shader_y_is_down = true;
/// Triple buffering. /// Triple buffering.
pub const swap_chain_count = 3; pub const swap_chain_count = 3;

View File

@@ -28,6 +28,8 @@ pub const cellpkg = @import("opengl/cell.zig");
pub const imagepkg = @import("opengl/image.zig"); pub const imagepkg = @import("opengl/image.zig");
pub const custom_shader_target: shadertoy.Target = .glsl; pub const custom_shader_target: shadertoy.Target = .glsl;
// The fragCoord for OpenGL shaders is +Y = up.
pub const custom_shader_y_is_down = false;
/// Because OpenGL's frame completion is always /// Because OpenGL's frame completion is always
/// sync, we have no need for multi-buffering. /// sync, we have no need for multi-buffering.

View File

@@ -156,6 +156,19 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// The current GPU uniform values. /// The current GPU uniform values.
uniforms: shaderpkg.Uniforms, uniforms: shaderpkg.Uniforms,
/// Custom shader uniform values.
custom_shader_uniforms: shadertoy.Uniforms,
/// Timestamp we rendered out first frame.
///
/// This is used when updating custom shader uniforms.
first_frame_time: ?std.time.Instant = null,
/// Timestamp when we rendered out more recent frame.
///
/// This is used when updating custom shader uniforms.
last_frame_time: ?std.time.Instant = null,
/// The font structures. /// The font structures.
font_grid: *font.SharedGrid, font_grid: *font.SharedGrid,
font_shaper: font.Shaper, font_shaper: font.Shaper,
@@ -382,16 +395,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
front_texture: Texture, front_texture: Texture,
back_texture: Texture, back_texture: Texture,
uniforms: shaderpkg.PostUniforms,
/// The first time a frame was drawn.
/// This is used to update the time uniform.
first_frame_time: std.time.Instant,
/// The last time a frame was drawn.
/// This is used to update the time uniform.
last_frame_time: std.time.Instant,
/// Swap the front and back textures. /// Swap the front and back textures.
pub fn swap(self: *CustomShaderState) void { pub fn swap(self: *CustomShaderState) void {
std.mem.swap(Texture, &self.front_texture, &self.back_texture); std.mem.swap(Texture, &self.front_texture, &self.back_texture);
@@ -417,22 +420,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
return .{ return .{
.front_texture = front_texture, .front_texture = front_texture,
.back_texture = back_texture, .back_texture = back_texture,
.uniforms = .{
.resolution = .{ 0, 0, 1 },
.time = 1,
.time_delta = 1,
.frame_rate = 1,
.frame = 1,
.channel_time = @splat(@splat(0)),
.channel_resolution = @splat(@splat(0)),
.mouse = .{ 0, 0, 0, 0 },
.date = .{ 0, 0, 0, 0 },
.sample_rate = 1,
},
.first_frame_time = try std.time.Instant.now(),
.last_frame_time = try std.time.Instant.now(),
}; };
} }
@@ -467,18 +454,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.front_texture = front_texture; self.front_texture = front_texture;
self.back_texture = back_texture; self.back_texture = back_texture;
self.uniforms.resolution = .{
@floatFromInt(width),
@floatFromInt(height),
1,
};
self.uniforms.channel_resolution[0] = .{
@floatFromInt(width),
@floatFromInt(height),
1,
0,
};
} }
}; };
@@ -689,6 +664,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.use_linear_correction = options.config.blending == .@"linear-corrected", .use_linear_correction = options.config.blending == .@"linear-corrected",
}, },
}, },
.custom_shader_uniforms = .{
.resolution = .{ 0, 0, 1 },
.time = 0,
.time_delta = 0,
.frame_rate = 60, // not currently updated
.frame = 0,
.channel_time = @splat(@splat(0)),
.channel_resolution = @splat(@splat(0)),
.mouse = @splat(0), // not currently updated
.date = @splat(0), // not currently updated
.sample_rate = 0, // N/A, we don't have any audio
.current_cursor = @splat(0),
.previous_cursor = @splat(0),
.current_cursor_color = @splat(0),
.previous_cursor_color = @splat(0),
.cursor_change_time = 0,
},
// Fonts // Fonts
.font_grid = options.font_grid, .font_grid = options.font_grid,
@@ -1359,21 +1351,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
} }
} }
// Update custom shader uniforms if necessary.
try self.updateCustomShaderUniforms();
// Setup our frame data // Setup our frame data
try frame.uniforms.sync(&.{self.uniforms}); try frame.uniforms.sync(&.{self.uniforms});
try frame.cells_bg.sync(self.cells.bg_cells); try frame.cells_bg.sync(self.cells.bg_cells);
const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists); const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
// If we have custom shaders, update the animation time.
if (frame.custom_shader_state) |*state| {
const now = std.time.Instant.now() catch state.first_frame_time;
const since_ns: f32 = @floatFromInt(now.since(state.first_frame_time));
const delta_ns: f32 = @floatFromInt(now.since(state.last_frame_time));
state.uniforms.time = since_ns / std.time.ns_per_s;
state.uniforms.time_delta = delta_ns / std.time.ns_per_s;
state.last_frame_time = now;
}
// If our font atlas changed, sync the texture data // If our font atlas changed, sync the texture data
texture: { texture: {
const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
@@ -1446,10 +1431,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (frame.custom_shader_state) |*state| { if (frame.custom_shader_state) |*state| {
// We create a buffer on the GPU for our post uniforms. // We create a buffer on the GPU for our post uniforms.
// TODO: This should be a part of the frame state tbqh. // TODO: This should be a part of the frame state tbqh.
const PostBuffer = Buffer(shaderpkg.PostUniforms); const PostBuffer = Buffer(shadertoy.Uniforms);
const uniform_buffer = try PostBuffer.initFill( const uniform_buffer = try PostBuffer.initFill(
self.api.bufferOptions(), self.api.bufferOptions(),
&.{state.uniforms}, &.{self.custom_shader_uniforms},
); );
defer uniform_buffer.deinit(); defer uniform_buffer.deinit();
@@ -1961,6 +1946,103 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}; };
} }
/// Update uniforms for the custom shaders, if necessary.
///
/// This should be called exactly once per frame, inside `drawFrame`.
fn updateCustomShaderUniforms(
self: *Self,
) !void {
// We only need to do this if we have custom shaders.
if (!self.has_custom_shaders) return;
const now = try std.time.Instant.now();
defer self.last_frame_time = now;
const first_frame_time = self.first_frame_time orelse t: {
self.first_frame_time = now;
break :t now;
};
const last_frame_time = self.last_frame_time orelse now;
const since_ns: f32 = @floatFromInt(now.since(first_frame_time));
self.custom_shader_uniforms.time = since_ns / std.time.ns_per_s;
const delta_ns: f32 = @floatFromInt(now.since(last_frame_time));
self.custom_shader_uniforms.time_delta = delta_ns / std.time.ns_per_s;
self.custom_shader_uniforms.frame += 1;
const screen = self.size.screen;
const padding = self.size.padding;
const cell = self.size.cell;
self.custom_shader_uniforms.resolution = .{
@floatFromInt(screen.width),
@floatFromInt(screen.height),
1,
};
self.custom_shader_uniforms.channel_resolution[0] = .{
@floatFromInt(screen.width),
@floatFromInt(screen.height),
1,
0,
};
// Update custom cursor uniforms, if we have a cursor.
if (self.cells.fg_rows.lists[0].items.len > 0) {
const cursor: shaderpkg.CellText =
self.cells.fg_rows.lists[0].items[0];
const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
var pixel_x: f32 = @floatFromInt(
cursor.grid_pos[0] * cell.width + padding.left,
);
var pixel_y: f32 = @floatFromInt(
cursor.grid_pos[1] * cell.height + padding.top,
);
pixel_x += @floatFromInt(cursor.bearings[0]);
pixel_y += @floatFromInt(cursor.bearings[1]);
// If +Y is up in our shaders, we need to flip the coordinate.
if (!GraphicsAPI.custom_shader_y_is_down) {
pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y;
// We need to add the cursor height because we need the +Y
// edge for the Y coordinate, and flipping means that it's
// the -Y edge now.
pixel_y += cursor_height;
}
const new_cursor: [4]f32 = .{
pixel_x,
pixel_y,
cursor_width,
cursor_height,
};
const cursor_color: [4]f32 = .{
@as(f32, @floatFromInt(cursor.color[0])) / 255.0,
@as(f32, @floatFromInt(cursor.color[1])) / 255.0,
@as(f32, @floatFromInt(cursor.color[2])) / 255.0,
@as(f32, @floatFromInt(cursor.color[3])) / 255.0,
};
const uniforms = &self.custom_shader_uniforms;
const cursor_changed: bool =
!std.meta.eql(new_cursor, uniforms.current_cursor) or
!std.meta.eql(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;
}
}
}
/// Convert the terminal state to GPU cells stored in CPU memory. These /// 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 /// are then synced to the GPU in the next frame. This only updates CPU
/// memory and doesn't touch the GPU. /// memory and doesn't touch the GPU.

View File

@@ -182,23 +182,6 @@ pub const Uniforms = extern struct {
}; };
}; };
/// The uniforms used for custom postprocess shaders.
pub const PostUniforms = extern struct {
// Note: all of the explicit alignments are copied from the
// MSL developer reference just so that we can be sure that we got
// it all exactly right.
resolution: [3]f32 align(16),
time: f32 align(4),
time_delta: f32 align(4),
frame_rate: f32 align(4),
frame: i32 align(4),
channel_time: [4][4]f32 align(16),
channel_resolution: [4][4]f32 align(16),
mouse: [4]f32 align(16),
date: [4]f32 align(16),
sample_rate: f32 align(4),
};
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders. /// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
fn initLibrary(device: objc.Object) !objc.Object { fn initLibrary(device: objc.Object) !objc.Object {
const start = try std.time.Instant.now(); const start = try std.time.Instant.now();

View File

@@ -165,20 +165,6 @@ pub const Uniforms = extern struct {
}; };
}; };
/// The uniforms used for custom postprocess shaders.
pub const PostUniforms = extern struct {
resolution: [3]f32 align(16),
time: f32 align(4),
time_delta: f32 align(4),
frame_rate: f32 align(4),
frame: i32 align(4),
channel_time: [4][4]f32 align(16),
channel_resolution: [4][4]f32 align(16),
mouse: [4]f32 align(16),
date: [4]f32 align(16),
sample_rate: f32 align(4),
};
/// Initialize our custom shader pipelines. The shaders argument is a /// Initialize our custom shader pipelines. The shaders argument is a
/// set of shader source code, not file paths. /// set of shader source code, not file paths.
fn initPostPipelines( fn initPostPipelines(

View File

@@ -11,6 +11,11 @@ layout(binding = 1, std140) uniform Globals {
uniform vec4 iMouse; uniform vec4 iMouse;
uniform vec4 iDate; uniform vec4 iDate;
uniform float iSampleRate; uniform float iSampleRate;
uniform vec4 iCurrentCursor;
uniform vec4 iPreviousCursor;
uniform vec4 iCurrentCursorColor;
uniform vec4 iPreviousCursorColor;
uniform float iTimeCursorChange;
}; };
layout(binding = 0) uniform sampler2D iChannel0; layout(binding = 0) uniform sampler2D iChannel0;

View File

@@ -9,6 +9,25 @@ const configpkg = @import("../config.zig");
const log = std.log.scoped(.shadertoy); const log = std.log.scoped(.shadertoy);
/// The uniform struct used for shadertoy shaders.
pub const Uniforms = extern struct {
resolution: [3]f32 align(16),
time: f32 align(4),
time_delta: f32 align(4),
frame_rate: f32 align(4),
frame: i32 align(4),
channel_time: [4][4]f32 align(16),
channel_resolution: [4][4]f32 align(16),
mouse: [4]f32 align(16),
date: [4]f32 align(16),
sample_rate: f32 align(4),
current_cursor: [4]f32 align(16),
previous_cursor: [4]f32 align(16),
current_cursor_color: [4]f32 align(16),
previous_cursor_color: [4]f32 align(16),
cursor_change_time: f32 align(4),
};
/// The target to load shaders for. /// The target to load shaders for.
pub const Target = enum { glsl, msl }; pub const Target = enum { glsl, msl };