mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-14 22:06:10 +00:00
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:
@@ -50,7 +50,11 @@ pub fn renderGlyph(
|
||||
const region = try canvas.writeAtlas(alloc, atlas);
|
||||
|
||||
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,
|
||||
.offset_x = 0,
|
||||
.offset_y = @intCast(height),
|
||||
|
@@ -33,6 +33,8 @@ pub const cellpkg = @import("metal/cell.zig");
|
||||
pub const imagepkg = @import("metal/image.zig");
|
||||
|
||||
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.
|
||||
pub const swap_chain_count = 3;
|
||||
|
@@ -28,6 +28,8 @@ pub const cellpkg = @import("opengl/cell.zig");
|
||||
pub const imagepkg = @import("opengl/image.zig");
|
||||
|
||||
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
|
||||
/// sync, we have no need for multi-buffering.
|
||||
|
@@ -156,6 +156,19 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
/// The current GPU uniform values.
|
||||
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.
|
||||
font_grid: *font.SharedGrid,
|
||||
font_shaper: font.Shaper,
|
||||
@@ -382,16 +395,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
front_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.
|
||||
pub fn swap(self: *CustomShaderState) void {
|
||||
std.mem.swap(Texture, &self.front_texture, &self.back_texture);
|
||||
@@ -417,22 +420,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
return .{
|
||||
.front_texture = front_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.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",
|
||||
},
|
||||
},
|
||||
.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
|
||||
.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
|
||||
try frame.uniforms.sync(&.{self.uniforms});
|
||||
try frame.cells_bg.sync(self.cells.bg_cells);
|
||||
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
|
||||
texture: {
|
||||
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| {
|
||||
// We create a buffer on the GPU for our post uniforms.
|
||||
// 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(
|
||||
self.api.bufferOptions(),
|
||||
&.{state.uniforms},
|
||||
&.{self.custom_shader_uniforms},
|
||||
);
|
||||
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
|
||||
/// are then synced to the GPU in the next frame. This only updates CPU
|
||||
/// memory and doesn't touch the GPU.
|
||||
|
@@ -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.
|
||||
fn initLibrary(device: objc.Object) !objc.Object {
|
||||
const start = try std.time.Instant.now();
|
||||
|
@@ -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
|
||||
/// set of shader source code, not file paths.
|
||||
fn initPostPipelines(
|
||||
|
@@ -11,6 +11,11 @@ layout(binding = 1, std140) uniform Globals {
|
||||
uniform vec4 iMouse;
|
||||
uniform vec4 iDate;
|
||||
uniform float iSampleRate;
|
||||
uniform vec4 iCurrentCursor;
|
||||
uniform vec4 iPreviousCursor;
|
||||
uniform vec4 iCurrentCursorColor;
|
||||
uniform vec4 iPreviousCursorColor;
|
||||
uniform float iTimeCursorChange;
|
||||
};
|
||||
|
||||
layout(binding = 0) uniform sampler2D iChannel0;
|
||||
|
@@ -9,6 +9,25 @@ const configpkg = @import("../config.zig");
|
||||
|
||||
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.
|
||||
pub const Target = enum { glsl, msl };
|
||||
|
||||
|
Reference in New Issue
Block a user