diff --git a/src/font/sprite/cursor.zig b/src/font/sprite/cursor.zig index 62195316e..d63db624a 100644 --- a/src/font/sprite/cursor.zig +++ b/src/font/sprite/cursor.zig @@ -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), diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 03a568d87..a001ca08d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 23496c148..dcc295eaf 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -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. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index f178d7bef..c0091cbf6 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -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. diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 68994882e..dc5d1122c 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -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(); diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig index e509b723a..7e54fd37b 100644 --- a/src/renderer/opengl/shaders.zig +++ b/src/renderer/opengl/shaders.zig @@ -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( diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index 5bc25bc03..6d9cf0f68 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -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; diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 68171a23e..576237587 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -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 };