diff --git a/src/app.zig b/src/app.zig index a2014ef..74b03ef 100644 --- a/src/app.zig +++ b/src/app.zig @@ -34,6 +34,11 @@ ui: UI, channels: std.BoundedArray(Channel, 64) = .{}, ni_daq: NIDaq, +shown_window: enum { + channels, + add_from_device +} = .channels, + pub fn init(allocator: std.mem.Allocator) !App { return App{ .allocator = allocator, @@ -143,10 +148,201 @@ pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void { }); } -pub fn tick(self: *App) !void { - rl.beginDrawing(); - defer rl.endDrawing(); +fn showChannelsWindow(self: *App) void { + const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels")); + scroll_area.layout_axis = .Y; + defer self.ui.popScrollbar(); + for (self.channels.slice()) |*_channel| { + const channel: *Channel = _channel; + + const channel_box = self.ui.newBoxFromPtr(channel); + channel_box.background = rl.Color.blue; + channel_box.layout_axis = .Y; + channel_box.size.x = UI.Size.percent(1, 0); + channel_box.size.y = UI.Size.childrenSum(1); + self.ui.pushParent(channel_box); + defer self.ui.popParent(); + + const graph_box = self.ui.newBoxFromString("Graph"); + graph_box.background = rl.Color.blue; + graph_box.layout_axis = .Y; + graph_box.size.x = UI.Size.percent(1, 0); + graph_box.size.y = UI.Size.pixels(256, 1); + + Graph.drawCached(&channel.view_cache, graph_box.persistent.size, channel.view_rect, channel.samples.owned); + if (channel.view_cache.texture) |texture| { + graph_box.texture = texture.texture; + } + + { + const sample_count: f32 = @floatFromInt(channel.samples.owned.len); + const min_visible_samples = 1; // sample_count*0.02; + + const minimap_box = self.ui.newBoxFromString("Minimap"); + minimap_box.background = rl.Color.dark_purple; + minimap_box.layout_axis = .X; + minimap_box.size.x = UI.Size.percent(1, 0); + minimap_box.size.y = UI.Size.pixels(32, 1); + self.ui.pushParent(minimap_box); + defer self.ui.popParent(); + + const minimap_rect = minimap_box.computedRect(); + + + const middle_box = self.ui.newBoxFromString("Middle knob"); + { + middle_box.flags.insert(.clickable); + middle_box.flags.insert(.draggable_x); + middle_box.background = rl.Color.black.alpha(0.5); + middle_box.size.y = UI.Size.pixels(32, 1); + } + + const left_knob_box = self.ui.newBoxFromString("Left knob"); + { + left_knob_box.flags.insert(.clickable); + left_knob_box.flags.insert(.draggable_x); + left_knob_box.background = rl.Color.black.alpha(0.5); + left_knob_box.size.x = UI.Size.pixels(8, 1); + left_knob_box.size.y = UI.Size.pixels(32, 1); + } + + const right_knob_box = self.ui.newBoxFromString("Right knob"); + { + right_knob_box.flags.insert(.clickable); + right_knob_box.flags.insert(.draggable_x); + right_knob_box.background = rl.Color.black.alpha(0.5); + right_knob_box.size.x = UI.Size.pixels(8, 1); + right_knob_box.size.y = UI.Size.pixels(32, 1); + } + + const left_knob_size = left_knob_box.persistent.size.x; + const right_knob_size = right_knob_box.persistent.size.x; + + const left_signal = self.ui.signalFromBox(left_knob_box); + if (left_signal.dragged()) { + channel.view_rect.from += remap( + f32, + 0, minimap_rect.width, + 0, sample_count, + left_signal.drag.x + ); + + channel.view_rect.from = clamp(channel.view_rect.from, 0, channel.view_rect.to-min_visible_samples); + } + + const right_signal = self.ui.signalFromBox(right_knob_box); + if (right_signal.dragged()) { + channel.view_rect.to += remap( + f32, + 0, minimap_rect.width, + 0, sample_count, + right_signal.drag.x + ); + + channel.view_rect.to = clamp(channel.view_rect.to, channel.view_rect.from+min_visible_samples, sample_count); + } + + const middle_signal = self.ui.signalFromBox(middle_box); + if (middle_signal.dragged()) { + var samples_moved = middle_signal.drag.x / minimap_rect.width * sample_count; + + samples_moved = clamp(samples_moved, -channel.view_rect.from, sample_count - channel.view_rect.to); + + channel.view_rect.from += samples_moved; + channel.view_rect.to += samples_moved; + } + + left_knob_box.setFixedX(remap(f32, + 0, sample_count, + 0, minimap_rect.width - left_knob_size - right_knob_size, + channel.view_rect.from + )); + + right_knob_box.setFixedX(remap(f32, + 0, sample_count, + left_knob_size, minimap_rect.width - right_knob_size, + channel.view_rect.to + )); + + middle_box.setFixedX(remap(f32, + 0, sample_count, + left_knob_size, minimap_rect.width - right_knob_size, + channel.view_rect.from + )); + middle_box.setFixedWidth(remap(f32, + 0, sample_count, + 0, minimap_rect.width - right_knob_size - left_knob_size, + channel.view_rect.to - channel.view_rect.from + )); + + } + } +} + +fn showAddFromDeviceWindow(self: *App) !void { + const names = try self.ni_daq.listDeviceNames(); + _ = names; +} + +fn showToolbar(self: *App) void { + const toolbar = self.ui.newBoxFromString("Toolbar"); + toolbar.flags.insert(.clickable); + toolbar.background = rl.Color.green; + toolbar.layout_axis = .X; + toolbar.size = .{ + .x = UI.Size.percent(1, 0), + .y = UI.Size.pixels(32, 1), + }; + self.ui.pushParent(toolbar); + defer self.ui.popParent(); + + { + const box = self.ui.newBoxFromString("Add from file"); + box.flags.insert(.clickable); + box.background = rl.Color.red; + box.size = .{ + .x = UI.Size.text(2, 1), + .y = UI.Size.percent(1, 1) + }; + box.setText("Add from file", .text); + + const signal = self.ui.signalFromBox(box); + if (signal.clicked()) { + if (Platform.openFilePicker()) |file| { + defer file.close(); + + // TODO: Handle error + self.appendChannelFromFile(file) catch @panic("Failed to append channel from file"); + } else |err| { + // TODO: Show error message to user; + log.err("Failed to pick file: {}", .{ err }); + } + } + } + + { + const box = self.ui.newBoxFromString("Add from device"); + box.flags.insert(.clickable); + box.background = rl.Color.lime; + box.size = .{ + .x = UI.Size.text(2, 1), + .y = UI.Size.percent(1, 1) + }; + box.setText("Add from device", .text); + + const signal = self.ui.signalFromBox(box); + if (signal.clicked()) { + if (self.shown_window == .add_from_device) { + self.shown_window = .channels; + } else { + self.shown_window = .add_from_device; + } + } + } +} + +pub fn tick(self: *App) !void { rl.clearBackground(srcery.black); if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) { @@ -156,191 +352,16 @@ pub fn tick(self: *App) !void { { self.ui.begin(); defer self.ui.end(); - self.ui.getParent().?.layout_axis = .Y; - { - const toolbar = self.ui.newBoxFromString("Toolbar"); - toolbar.flags.insert(.clickable); - toolbar.background = rl.Color.green; - toolbar.layout_axis = .X; - toolbar.size = .{ - .x = UI.Size.percent(1, 0), - .y = UI.Size.pixels(32, 1), - }; - self.ui.pushParent(toolbar); - defer self.ui.popParent(); + const root_box = self.ui.getParent().?; + root_box.layout_axis = .Y; - { - const box = self.ui.newBoxFromString("Add from file"); - box.flags.insert(.clickable); - box.background = rl.Color.red; - box.size = .{ - .x = UI.Size.text(2, 1), - .y = UI.Size.percent(1, 1) - }; - box.setText("Add from file", .text); + self.showToolbar(); - const signal = self.ui.signalFromBox(box); - if (signal.clicked()) { - if (Platform.openFilePicker()) |file| { - defer file.close(); - - // TODO: Handle error - self.appendChannelFromFile(file) catch @panic("Failed to append channel from file"); - } else |err| { - // TODO: Show error message to user; - log.err("Failed to pick file: {}", .{ err }); - } - } - } - - { - const box = self.ui.newBoxFromString("Add from device"); - box.flags.insert(.clickable); - box.background = rl.Color.lime; - box.size = .{ - .x = UI.Size.text(2, 1), - .y = UI.Size.percent(1, 1) - }; - box.setText("Add from device", .text); - - const signal = self.ui.signalFromBox(box); - if (signal.clicked()) { - std.debug.print("click two!\n", .{}); - } - } - } - - { - const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels")); - scroll_area.layout_axis = .Y; - defer self.ui.popScrollbar(); - - for (self.channels.slice()) |*_channel| { - const channel: *Channel = _channel; - - const channel_box = self.ui.newBoxFromPtr(channel); - channel_box.background = rl.Color.blue; - channel_box.layout_axis = .Y; - channel_box.size.x = UI.Size.percent(1, 0); - channel_box.size.y = UI.Size.childrenSum(1); - self.ui.pushParent(channel_box); - defer self.ui.popParent(); - - const graph_box = self.ui.newBoxFromString("Graph"); - graph_box.background = rl.Color.blue; - graph_box.layout_axis = .Y; - graph_box.size.x = UI.Size.percent(1, 0); - graph_box.size.y = UI.Size.pixels(256, 1); - - Graph.drawCached(&channel.view_cache, graph_box.persistent.size, channel.view_rect, channel.samples.owned); - if (channel.view_cache.texture) |texture| { - graph_box.texture = texture.texture; - } - - { - const sample_count: f32 = @floatFromInt(channel.samples.owned.len); - const min_visible_samples = 1; // sample_count*0.02; - - const minimap_box = self.ui.newBoxFromString("Minimap"); - minimap_box.background = rl.Color.dark_purple; - minimap_box.layout_axis = .X; - minimap_box.size.x = UI.Size.percent(1, 0); - minimap_box.size.y = UI.Size.pixels(32, 1); - self.ui.pushParent(minimap_box); - defer self.ui.popParent(); - - const minimap_rect = minimap_box.computedRect(); - - - const middle_box = self.ui.newBoxFromString("Middle knob"); - { - middle_box.flags.insert(.clickable); - middle_box.flags.insert(.draggable_x); - middle_box.background = rl.Color.black.alpha(0.5); - middle_box.size.y = UI.Size.pixels(32, 1); - } - - const left_knob_box = self.ui.newBoxFromString("Left knob"); - { - left_knob_box.flags.insert(.clickable); - left_knob_box.flags.insert(.draggable_x); - left_knob_box.background = rl.Color.black.alpha(0.5); - left_knob_box.size.x = UI.Size.pixels(8, 1); - left_knob_box.size.y = UI.Size.pixels(32, 1); - } - - const right_knob_box = self.ui.newBoxFromString("Right knob"); - { - right_knob_box.flags.insert(.clickable); - right_knob_box.flags.insert(.draggable_x); - right_knob_box.background = rl.Color.black.alpha(0.5); - right_knob_box.size.x = UI.Size.pixels(8, 1); - right_knob_box.size.y = UI.Size.pixels(32, 1); - } - - const left_knob_size = left_knob_box.persistent.size.x; - const right_knob_size = right_knob_box.persistent.size.x; - - const left_signal = self.ui.signalFromBox(left_knob_box); - if (left_signal.dragged()) { - channel.view_rect.from += remap( - f32, - 0, minimap_rect.width, - 0, sample_count, - left_signal.drag.x - ); - - channel.view_rect.from = clamp(channel.view_rect.from, 0, channel.view_rect.to-min_visible_samples); - } - - const right_signal = self.ui.signalFromBox(right_knob_box); - if (right_signal.dragged()) { - channel.view_rect.to += remap( - f32, - 0, minimap_rect.width, - 0, sample_count, - right_signal.drag.x - ); - - channel.view_rect.to = clamp(channel.view_rect.to, channel.view_rect.from+min_visible_samples, sample_count); - } - - const middle_signal = self.ui.signalFromBox(middle_box); - if (middle_signal.dragged()) { - var samples_moved = middle_signal.drag.x / minimap_rect.width * sample_count; - - samples_moved = clamp(samples_moved, -channel.view_rect.from, sample_count - channel.view_rect.to); - - channel.view_rect.from += samples_moved; - channel.view_rect.to += samples_moved; - } - - left_knob_box.setFixedX(remap(f32, - 0, sample_count, - 0, minimap_rect.width - left_knob_size - right_knob_size, - channel.view_rect.from - )); - - right_knob_box.setFixedX(remap(f32, - 0, sample_count, - left_knob_size, minimap_rect.width - right_knob_size, - channel.view_rect.to - )); - - middle_box.setFixedX(remap(f32, - 0, sample_count, - left_knob_size, minimap_rect.width - right_knob_size, - channel.view_rect.from - )); - middle_box.setFixedWidth(remap(f32, - 0, sample_count, - 0, minimap_rect.width - right_knob_size - left_knob_size, - channel.view_rect.to - channel.view_rect.from - )); - - } - } + if (self.shown_window == .channels) { + self.showChannelsWindow(); + } else if (self.shown_window == .add_from_device) { + try self.showAddFromDeviceWindow(); } } diff --git a/src/main.zig b/src/main.zig index a9412e6..666cd20 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,12 +3,14 @@ const rl = @import("raylib"); const builtin = @import("builtin"); const Application = @import("./app.zig"); const Assets = @import("./assets.zig"); +const Profiler = @import("./profiler.zig"); const raylib_h = @cImport({ @cInclude("stdio.h"); @cInclude("raylib.h"); }); const log = std.log; +const profiler_enabled = builtin.mode == .Debug; // TODO: Maybe move this to a config.zig or options.zig file. // Have all of the contstants in a single file. @@ -134,6 +136,9 @@ pub fn main() !void { rl.setWindowMinSize(256, 256); rl.setWindowIcon(icon_image); + const target_fps = 60; + rl.setTargetFPS(target_fps); + if (builtin.mode != .Debug) { rl.setExitKey(.key_null); } @@ -158,8 +163,35 @@ pub fn main() !void { } } + var profiler: ?Profiler = null; + defer if (profiler) |p| p.deinit(); + + var profiler_shown = false; + if (profiler_enabled) { + const font_face = Assets.font(.text); + profiler = try Profiler.init(allocator, 10 * target_fps, @divFloor(std.time.ns_per_s, target_fps), font_face); + } + while (!rl.windowShouldClose()) { + rl.beginDrawing(); + defer rl.endDrawing(); + + if (profiler) |*p| { + p.start(); + } + try app.tick(); + + if (profiler) |*p| { + p.stop(); + if (rl.isKeyPressed(.key_p) and rl.isKeyDown(.key_left_control)) { + profiler_shown = !profiler_shown; + } + + if (profiler_shown) { + try p.showResults(); + } + } } } diff --git a/src/profiler.zig b/src/profiler.zig new file mode 100644 index 0000000..9288b94 --- /dev/null +++ b/src/profiler.zig @@ -0,0 +1,110 @@ +const std = @import("std"); +const rl = @import("raylib"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const FontFace = @import("./font-face.zig"); +const srcery = @import("./srcery.zig"); +const rect_utils = @import("./rect-utils.zig"); + +allocator: Allocator, +history: []u128, +history_size: usize, +history_head: usize, + +started_at: i128, +ns_budget: u128, +font_face: FontFace, + +pub fn init(allocator: Allocator, data_points: usize, ns_budget: u128, font_face: FontFace) !@This() { + return @This(){ + .allocator = allocator, + .history = try allocator.alloc(u128, data_points), + .history_size = 0, + .history_head = 0, + .started_at = 0, + .ns_budget = ns_budget, + .font_face = font_face, + }; +} + +pub fn deinit(self: @This()) void { + self.allocator.free(self.history); +} + +pub fn start(self: *@This()) void { + self.started_at = std.time.nanoTimestamp(); +} + +pub fn stop(self: *@This()) void { + const stopped_at = std.time.nanoTimestamp(); + assert(stopped_at >= self.started_at); + + const duration: u128 = @intCast(stopped_at - self.started_at); + + if (self.history_size < self.history.len) { + self.history[self.history_size] = duration; + self.history_size += 1; + } else { + self.history_head = @mod(self.history_head + 1, self.history.len); + self.history[self.history_head] = duration; + } +} + +pub fn showResults(self: *const @This()) !void { + const screen_width: f32 = @floatFromInt(rl.getScreenWidth()); + const screen_height: f32 = @floatFromInt(rl.getScreenHeight()); + + const profile_height = 200; + const profile_box = rl.Rectangle.init(0, screen_height - profile_height, screen_width, profile_height); + const color = srcery.bright_white; + rl.drawRectangleLinesEx(profile_box, 1, color); + + const ns_budget: f32 = @floatFromInt(self.ns_budget); + + const measurement_width = profile_box.width / @as(f32, @floatFromInt(self.history.len)); + for (0..self.history_size) |i| { + const measurement = self.history[@mod(self.history_head + i, self.history.len)]; + const measurement_height = @as(f32, @floatFromInt(measurement)) / ns_budget * profile_box.height; + rl.drawRectangleV( + .{ + .x = profile_box.x + measurement_width * @as(f32, @floatFromInt(i + self.history.len - self.history_size)), + .y = profile_box.y + profile_box.height - measurement_height + }, + .{ .x = measurement_width, .y = measurement_height }, + color + ); + } + + var min_time_taken = self.history[0]; + var max_time_taken = self.history[0]; + var sum_time_taken: u128 = 0; + for (self.history[0..self.history_size]) |measurement| { + min_time_taken = @min(min_time_taken, measurement); + max_time_taken = @max(max_time_taken, measurement); + sum_time_taken += measurement; + } + const avg_time_taken = @as(f32, @floatFromInt(sum_time_taken)) / @as(f32, @floatFromInt(self.history_size)); + + const content_rect = rect_utils.shrink(profile_box, 10); + var layout_offset: f32 = 0; + + const allocator = self.allocator; + const font_size = self.font_face.getSize(); + + const labels = .{ + .{ "Min", @as(f32, @floatFromInt(min_time_taken)) }, + .{ "Max", @as(f32, @floatFromInt(max_time_taken)) }, + .{ "Avg", avg_time_taken } + }; + + inline for (labels) |label| { + const label_name = label[0]; + const time_taken = label[1]; + const min_time_str = try std.fmt.allocPrintZ(allocator, "{s}: {d:10.0}us ({d:.3}%)", .{ label_name, time_taken / std.time.ns_per_us, time_taken / ns_budget * 100 }); + defer allocator.free(min_time_str); + + self.font_face.drawText(min_time_str, .{ .x = content_rect.x, .y = content_rect.y + layout_offset }, color); + layout_offset += font_size; + } +} \ No newline at end of file