renderer: animation timer if we have custom shaders

This commit is contained in:
Mitchell Hashimoto
2023-11-16 19:22:48 -08:00
parent 17de73f802
commit 4742cd308d
3 changed files with 105 additions and 21 deletions

View File

@ -125,6 +125,7 @@ pub const CustomShaderState = struct {
screen_texture: objc.Object, // MTLTexture screen_texture: objc.Object, // MTLTexture
sampler: mtl_sampler.Sampler, sampler: mtl_sampler.Sampler,
uniforms: mtl_shaders.PostUniforms, uniforms: mtl_shaders.PostUniforms,
last_frame_time: std.time.Instant,
pub fn deinit(self: *CustomShaderState) void { pub fn deinit(self: *CustomShaderState) void {
deinitMTLResource(self.screen_texture); deinitMTLResource(self.screen_texture);
@ -325,6 +326,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.date = .{ 0, 0, 0, 0 }, .date = .{ 0, 0, 0, 0 },
.sample_rate = 1, .sample_rate = 1,
}, },
.last_frame_time = try std.time.Instant.now(),
}; };
}; };
errdefer if (custom_shader_state) |*state| state.deinit(); 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. // 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 /// Returns the grid size for a given screen size. This is safe to call
/// on any thread. /// on any thread.
fn gridSize(self: *Metal) ?renderer.GridSize { fn gridSize(self: *Metal) ?renderer.GridSize {
@ -653,6 +662,14 @@ pub fn updateFrame(
pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
_ = surface; _ = 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 {} // @autoreleasepool {}
const pool = objc.AutoreleasePool.init(); const pool = objc.AutoreleasePool.init();
defer pool.deinit(); defer pool.deinit();

View File

@ -15,6 +15,7 @@ const App = @import("../App.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = std.log.scoped(.renderer_thread); const log = std.log.scoped(.renderer_thread);
const DRAW_INTERVAL = 33; // 30 FPS
const CURSOR_BLINK_INTERVAL = 600; const CURSOR_BLINK_INTERVAL = 600;
/// The type used for sending messages to the IO thread. For now this is /// 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_h: xev.Timer,
render_c: xev.Completion = .{}, 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 /// The timer used for cursor blinking
cursor_h: xev.Timer, cursor_h: xev.Timer,
cursor_c: xev.Completion = .{}, cursor_c: xev.Completion = .{},
@ -100,6 +108,10 @@ pub fn init(
var render_h = try xev.Timer.init(); var render_h = try xev.Timer.init();
errdefer render_h.deinit(); 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 // Setup a timer for blinking the cursor
var cursor_timer = try xev.Timer.init(); var cursor_timer = try xev.Timer.init();
errdefer cursor_timer.deinit(); errdefer cursor_timer.deinit();
@ -114,6 +126,7 @@ pub fn init(
.wakeup = wakeup_h, .wakeup = wakeup_h,
.stop = stop_h, .stop = stop_h,
.render_h = render_h, .render_h = render_h,
.draw_h = draw_h,
.cursor_h = cursor_timer, .cursor_h = cursor_timer,
.surface = surface, .surface = surface,
.renderer = renderer_impl, .renderer = renderer_impl,
@ -129,6 +142,7 @@ pub fn deinit(self: *Thread) void {
self.stop.deinit(); self.stop.deinit();
self.wakeup.deinit(); self.wakeup.deinit();
self.render_h.deinit(); self.render_h.deinit();
self.draw_h.deinit();
self.cursor_h.deinit(); self.cursor_h.deinit();
self.loop.deinit(); self.loop.deinit();
@ -172,27 +186,8 @@ fn threadMain_(self: *Thread) !void {
cursorTimerCallback, cursorTimerCallback,
); );
// If we are using tracy, then we setup a prepare handle so that // Start the draw timer
// we can mark the frame. self.startDrawTimer();
// 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 {};
// };
// Run // Run
log.debug("starting renderer thread", .{}); log.debug("starting renderer thread", .{});
@ -200,6 +195,34 @@ fn threadMain_(self: *Thread) !void {
_ = try self.loop.run(.until_done); _ = 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. /// Drain the mailbox.
fn drainMailbox(self: *Thread) !void { fn drainMailbox(self: *Thread) !void {
const zone = trace(@src()); const zone = trace(@src());
@ -213,6 +236,9 @@ fn drainMailbox(self: *Thread) !void {
try self.renderer.setFocus(v); try self.renderer.setFocus(v);
if (!v) { if (!v) {
// Stop the draw timer
self.stopDrawTimer();
// If we're not focused, then we stop the cursor blink // If we're not focused, then we stop the cursor blink
if (self.cursor_c.state() == .active and if (self.cursor_c.state() == .active and
self.cursor_c_cancel.state() == .dead) self.cursor_c_cancel.state() == .dead)
@ -227,6 +253,9 @@ fn drainMailbox(self: *Thread) !void {
); );
} }
} else { } else {
// Start the draw timer
self.startDrawTimer();
// If we're focused, we immediately show the cursor again // If we're focused, we immediately show the cursor again
// and then restart the timer. // and then restart the timer.
if (self.cursor_c.state() != .active) { if (self.cursor_c.state() != .active) {
@ -325,6 +354,41 @@ fn wakeupCallback(
return .rearm; 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( fn renderCallback(
self_: ?*Thread, self_: ?*Thread,
_: *xev.Loop, _: *xev.Loop,

View File

@ -129,6 +129,9 @@ pub const Uniforms = extern struct {
/// The uniforms used for custom postprocess shaders. /// The uniforms used for custom postprocess shaders.
pub const PostUniforms = extern struct { 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), resolution: [3]f32 align(16),
time: f32 align(4), time: f32 align(4),
time_delta: f32 align(4), time_delta: f32 align(4),