From 4742cd308d0c2895048f34f0dda13fd5dd90aeab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 19:22:48 -0800 Subject: [PATCH] renderer: animation timer if we have custom shaders --- src/renderer/Metal.zig | 17 ++++++ src/renderer/Thread.zig | 106 ++++++++++++++++++++++++++------- src/renderer/metal/shaders.zig | 3 + 3 files changed, 105 insertions(+), 21 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 49ab67fc1..e26019a9a 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -125,6 +125,7 @@ pub const CustomShaderState = struct { screen_texture: objc.Object, // MTLTexture sampler: mtl_sampler.Sampler, uniforms: mtl_shaders.PostUniforms, + last_frame_time: std.time.Instant, pub fn deinit(self: *CustomShaderState) void { deinitMTLResource(self.screen_texture); @@ -325,6 +326,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .date = .{ 0, 0, 0, 0 }, .sample_rate = 1, }, + + .last_frame_time = try std.time.Instant.now(), }; }; errdefer if (custom_shader_state) |*state| state.deinit(); @@ -465,6 +468,12 @@ pub fn threadExit(self: *const Metal) void { // Metal requires no per-thread state. } +/// True if our renderer has animations so that a higher frequency +/// timer is used. +pub fn hasAnimations(self: *const Metal) bool { + return self.custom_shader_state != null; +} + /// Returns the grid size for a given screen size. This is safe to call /// on any thread. fn gridSize(self: *Metal) ?renderer.GridSize { @@ -653,6 +662,14 @@ pub fn updateFrame( pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { _ = surface; + // If we have custom shaders, update the animation time. + if (self.custom_shader_state) |*state| { + const now = std.time.Instant.now() catch state.last_frame_time; + const since_ns: f32 = @floatFromInt(now.since(state.last_frame_time)); + state.uniforms.time = since_ns / std.time.ns_per_s; + state.uniforms.time_delta = since_ns / std.time.ns_per_s; + } + // @autoreleasepool {} const pool = objc.AutoreleasePool.init(); defer pool.deinit(); diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 926bd8e42..e6d0809a5 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -15,6 +15,7 @@ const App = @import("../App.zig"); const Allocator = std.mem.Allocator; const log = std.log.scoped(.renderer_thread); +const DRAW_INTERVAL = 33; // 30 FPS const CURSOR_BLINK_INTERVAL = 600; /// The type used for sending messages to the IO thread. For now this is @@ -43,6 +44,13 @@ stop_c: xev.Completion = .{}, render_h: xev.Timer, render_c: xev.Completion = .{}, +/// The timer used for draw calls. Draw calls don't update from the +/// terminal state so they're much cheaper. They're used for animation +/// and are paused when the terminal is not focused. +draw_h: xev.Timer, +draw_c: xev.Completion = .{}, +draw_active: bool = false, + /// The timer used for cursor blinking cursor_h: xev.Timer, cursor_c: xev.Completion = .{}, @@ -100,6 +108,10 @@ pub fn init( var render_h = try xev.Timer.init(); errdefer render_h.deinit(); + // Draw timer, see comments. + var draw_h = try xev.Timer.init(); + errdefer draw_h.deinit(); + // Setup a timer for blinking the cursor var cursor_timer = try xev.Timer.init(); errdefer cursor_timer.deinit(); @@ -114,6 +126,7 @@ pub fn init( .wakeup = wakeup_h, .stop = stop_h, .render_h = render_h, + .draw_h = draw_h, .cursor_h = cursor_timer, .surface = surface, .renderer = renderer_impl, @@ -129,6 +142,7 @@ pub fn deinit(self: *Thread) void { self.stop.deinit(); self.wakeup.deinit(); self.render_h.deinit(); + self.draw_h.deinit(); self.cursor_h.deinit(); self.loop.deinit(); @@ -172,27 +186,8 @@ fn threadMain_(self: *Thread) !void { cursorTimerCallback, ); - // If we are using tracy, then we setup a prepare handle so that - // we can mark the frame. - // TODO - // var frame_h: libuv.Prepare = if (!tracy.enabled) undefined else frame_h: { - // const alloc_ptr = self.loop.getData(Allocator).?; - // const alloc = alloc_ptr.*; - // const h = try libuv.Prepare.init(alloc, self.loop); - // h.setData(self); - // try h.start(prepFrameCallback); - // - // break :frame_h h; - // }; - // defer if (tracy.enabled) { - // frame_h.close((struct { - // fn callback(h: *libuv.Prepare) void { - // const alloc_h = h.loop().getData(Allocator).?.*; - // h.deinit(alloc_h); - // } - // }).callback); - // _ = self.loop.run(.nowait) catch {}; - // }; + // Start the draw timer + self.startDrawTimer(); // Run log.debug("starting renderer thread", .{}); @@ -200,6 +195,34 @@ fn threadMain_(self: *Thread) !void { _ = try self.loop.run(.until_done); } +fn startDrawTimer(self: *Thread) void { + // If our renderer doesn't suppoort animations then we never run this. + if (!@hasDecl(renderer.Renderer, "hasAnimations")) return; + if (!self.renderer.hasAnimations()) return; + + // Set our active state so it knows we're running. We set this before + // even checking the active state in case we have a pending shutdown. + self.draw_active = true; + + // If our draw timer is already active, then we don't have to do anything. + if (self.draw_c.state() == .active) return; + + // Start the timer which loops + self.draw_h.run( + &self.loop, + &self.draw_c, + DRAW_INTERVAL, + Thread, + self, + drawCallback, + ); +} + +fn stopDrawTimer(self: *Thread) void { + // This will stop the draw on the next iteration. + self.draw_active = false; +} + /// Drain the mailbox. fn drainMailbox(self: *Thread) !void { const zone = trace(@src()); @@ -213,6 +236,9 @@ fn drainMailbox(self: *Thread) !void { try self.renderer.setFocus(v); if (!v) { + // Stop the draw timer + self.stopDrawTimer(); + // If we're not focused, then we stop the cursor blink if (self.cursor_c.state() == .active and self.cursor_c_cancel.state() == .dead) @@ -227,6 +253,9 @@ fn drainMailbox(self: *Thread) !void { ); } } else { + // Start the draw timer + self.startDrawTimer(); + // If we're focused, we immediately show the cursor again // and then restart the timer. if (self.cursor_c.state() != .active) { @@ -325,6 +354,41 @@ fn wakeupCallback( return .rearm; } +fn drawCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch unreachable; + const t = self_ orelse { + // This shouldn't happen so we log it. + log.warn("render callback fired without data set", .{}); + return .disarm; + }; + + // If we're doing single-threaded GPU calls then we just wake up the + // app thread to redraw at this point. + if (renderer.Renderer == renderer.OpenGL and + renderer.OpenGL.single_threaded_draw) + { + _ = t.app_mailbox.push( + .{ .redraw_surface = t.surface }, + .{ .instant = {} }, + ); + } else { + t.renderer.drawFrame(t.surface) catch |err| + log.warn("error drawing err={}", .{err}); + } + + // Only continue if we're still active + if (t.draw_active) { + t.draw_h.run(&t.loop, &t.draw_c, DRAW_INTERVAL, Thread, t, drawCallback); + } + + return .disarm; +} + fn renderCallback( self_: ?*Thread, _: *xev.Loop, diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 4edb7eb90..32f775a50 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -129,6 +129,9 @@ pub const Uniforms = extern struct { /// The uniforms used for custom postprocess shaders. pub const PostUniforms = extern struct { + // Note: all of the explicit aligmnments 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),