Compare commits
14 Commits
45f659cdd7
...
4db4b9fa81
Author | SHA1 | Date | |
---|---|---|---|
4db4b9fa81 | |||
eb97a0d832 | |||
401056b676 | |||
d744ab4b6b | |||
785355509e | |||
65ad8d7786 | |||
a87a9c104e | |||
dc3e66cfdc | |||
fa4fb2009f | |||
43b6ca0ff2 | |||
160b7778ce | |||
801f9f6ef4 | |||
0d9033c926 | |||
24895afce6 |
@ -3,3 +3,7 @@
|
||||
```shell
|
||||
zig build run
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
* Use downsampling for faster rendering of samples. When viewing many samples use dowsampled versions of data for rendering. Because you either way, you won't be able to see the detail.
|
@ -17,6 +17,10 @@ pub fn build(b: *std.Build) !void {
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
exe.addIncludePath(b.path("src"));
|
||||
exe.addCSourceFile(.{
|
||||
.file = b.path("src/cute_aseprite.c")
|
||||
});
|
||||
exe.linkLibrary(raylib_dep.artifact("raylib"));
|
||||
exe.root_module.addImport("raylib", raylib_dep.module("raylib"));
|
||||
|
||||
@ -36,10 +40,13 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
const resource_file = b.addWriteFiles();
|
||||
|
||||
// https://www.ryanliptak.com/blog/zig-is-a-windows-resource-compiler/
|
||||
// TODO: Generate icon file at build time
|
||||
exe.addWin32ResourceFile(.{
|
||||
.file = resource_file.add("daq-view.rc", "IDI_ICON ICON \"./src/assets/icon.ico\""),
|
||||
});
|
||||
|
||||
exe.linkSystemLibrary("Comdlg32");
|
||||
}
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
834
src/app.zig
Normal file
834
src/app.zig
Normal file
@ -0,0 +1,834 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const srcery = @import("./srcery.zig");
|
||||
const UI = @import("./ui.zig");
|
||||
const Platform = @import("./platform.zig");
|
||||
const Assets = @import("./assets.zig");
|
||||
const Graph = @import("./graph.zig");
|
||||
const NIDaq = @import("ni-daq.zig");
|
||||
const rect_utils = @import("./rect-utils.zig");
|
||||
const remap = @import("./utils.zig").remap;
|
||||
const TaskPool = @import("./task-pool.zig");
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
const assert = std.debug.assert;
|
||||
const clamp = std.math.clamp;
|
||||
|
||||
const App = @This();
|
||||
|
||||
const max_channels = 64;
|
||||
const max_files = 32;
|
||||
|
||||
const FileChannel = struct {
|
||||
path: []u8,
|
||||
min_value: f64,
|
||||
max_value: f64,
|
||||
samples: []f64,
|
||||
|
||||
fn deinit(self: FileChannel, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.path);
|
||||
allocator.free(self.samples);
|
||||
}
|
||||
};
|
||||
|
||||
const DeviceChannel = struct {
|
||||
const Name = std.BoundedArray(u8, NIDaq.max_channel_name_size + 1); // +1 for null byte
|
||||
name: Name = .{},
|
||||
|
||||
mutex: std.Thread.Mutex = .{},
|
||||
samples: std.ArrayList(f64),
|
||||
|
||||
units: i32 = NIDaq.c.DAQmx_Val_Volts,
|
||||
min_sample_rate: f64,
|
||||
max_sample_rate: f64,
|
||||
min_value: f64,
|
||||
max_value: f64,
|
||||
|
||||
active_task: ?*TaskPool.Entry = null,
|
||||
|
||||
fn deinit(self: DeviceChannel) void {
|
||||
self.samples.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
const ChannelView = struct {
|
||||
view_cache: Graph.Cache = .{},
|
||||
view_rect: Graph.ViewOptions,
|
||||
follow: bool = false,
|
||||
|
||||
height: f32 = 150,
|
||||
|
||||
source: union(enum) {
|
||||
file: usize,
|
||||
device: usize
|
||||
},
|
||||
|
||||
const SourceObject = union(enum) {
|
||||
file: *FileChannel,
|
||||
device: *DeviceChannel,
|
||||
|
||||
fn samples(self: SourceObject) []const f64 {
|
||||
return switch (self) {
|
||||
.file => |file| file.samples,
|
||||
.device => |device| device.samples.items,
|
||||
};
|
||||
}
|
||||
|
||||
fn lockSamples(self: SourceObject) void {
|
||||
if (self == .device) {
|
||||
self.device.mutex.lock();
|
||||
}
|
||||
}
|
||||
|
||||
fn unlockSamples(self: SourceObject) void {
|
||||
if (self == .device) {
|
||||
self.device.mutex.unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
ui: UI,
|
||||
channel_views: std.BoundedArray(ChannelView, max_channels) = .{},
|
||||
ni_daq: NIDaq,
|
||||
task_pool: TaskPool,
|
||||
loaded_files: [max_channels]?FileChannel = .{ null } ** max_channels,
|
||||
device_channels: [max_channels]?DeviceChannel = .{ null } ** max_channels,
|
||||
|
||||
shown_window: enum {
|
||||
channels,
|
||||
add_from_device
|
||||
} = .channels,
|
||||
|
||||
device_filter: NIDaq.BoundedDeviceName = .{},
|
||||
show_voltage_analog_inputs: bool = true,
|
||||
show_voltage_analog_outputs: bool = true,
|
||||
selected_channels: std.BoundedArray([:0]u8, max_channels) = .{},
|
||||
|
||||
pub fn init(self: *App, allocator: std.mem.Allocator) !void {
|
||||
var ni_daq = try NIDaq.init(allocator, .{
|
||||
.max_devices = 4,
|
||||
.max_analog_inputs = 32,
|
||||
.max_analog_outputs = 8,
|
||||
.max_counter_outputs = 8,
|
||||
.max_counter_inputs = 8,
|
||||
.max_analog_input_voltage_ranges = 4,
|
||||
.max_analog_output_voltage_ranges = 4
|
||||
});
|
||||
errdefer ni_daq.deinit(allocator);
|
||||
|
||||
self.* = App{
|
||||
.allocator = allocator,
|
||||
.ui = UI.init(allocator),
|
||||
.ni_daq = ni_daq,
|
||||
.task_pool = undefined
|
||||
};
|
||||
|
||||
try TaskPool.init(&self.task_pool, allocator, &self.ni_daq);
|
||||
errdefer self.task_pool.deinit();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App) void {
|
||||
self.task_pool.deinit();
|
||||
|
||||
for (self.channel_views.slice()) |*channel| {
|
||||
channel.view_cache.deinit();
|
||||
}
|
||||
|
||||
for (&self.loaded_files) |*loaded_file| {
|
||||
if (loaded_file.*) |*f| {
|
||||
f.deinit(self.allocator);
|
||||
loaded_file.* = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (&self.device_channels) |*device_channel| {
|
||||
if (device_channel.*) |*c| {
|
||||
c.deinit();
|
||||
device_channel.* = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (self.selected_channels.constSlice()) |channel| {
|
||||
self.allocator.free(channel);
|
||||
}
|
||||
self.selected_channels.len = 0;
|
||||
|
||||
self.ui.deinit();
|
||||
self.ni_daq.deinit(self.allocator);
|
||||
}
|
||||
|
||||
fn showButton(self: *App, text: []const u8) UI.Interaction {
|
||||
var button = self.ui.newWidget(self.ui.keyFromString(text));
|
||||
button.border = srcery.bright_blue;
|
||||
button.padding.vertical(8);
|
||||
button.padding.horizontal(16);
|
||||
button.flags.insert(.clickable);
|
||||
button.size = .{
|
||||
.x = .{ .text = {} },
|
||||
.y = .{ .text = {} },
|
||||
};
|
||||
|
||||
const interaction = self.ui.getInteraction(button);
|
||||
var text_color: rl.Color = undefined;
|
||||
if (interaction.held_down) {
|
||||
button.background = srcery.hard_black;
|
||||
text_color = srcery.white;
|
||||
} else if (interaction.hovering) {
|
||||
button.background = srcery.bright_black;
|
||||
text_color = srcery.bright_white;
|
||||
} else {
|
||||
button.background = srcery.blue;
|
||||
text_color = srcery.bright_white;
|
||||
}
|
||||
|
||||
button.text = .{
|
||||
.content = text,
|
||||
.color = text_color
|
||||
};
|
||||
|
||||
return interaction;
|
||||
}
|
||||
|
||||
fn readSamplesFromFile(allocator: std.mem.Allocator, file: std.fs.File) ![]f64 {
|
||||
try file.seekTo(0);
|
||||
const byte_count = try file.getEndPos();
|
||||
assert(byte_count % 8 == 0);
|
||||
|
||||
var samples = try allocator.alloc(f64, @divExact(byte_count, 8));
|
||||
errdefer allocator.free(samples);
|
||||
|
||||
var i: usize = 0;
|
||||
var buffer: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const count = try file.readAll(&buffer);
|
||||
if (count == 0) break;
|
||||
|
||||
for (0..@divExact(count, 8)) |j| {
|
||||
samples[i] = std.mem.bytesToValue(f64, buffer[(j*8)..]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
fn findFreeSlot(T: type, slice: []const ?T) ?usize {
|
||||
for (0.., slice) |i, loaded_file| {
|
||||
if (loaded_file == null) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn appendChannelFromFile(self: *App, path: []const u8) !void {
|
||||
const path_dupe = try self.allocator.dupe(u8, path);
|
||||
errdefer self.allocator.free(path_dupe);
|
||||
|
||||
const file = try std.fs.cwd().openFile(path, .{});
|
||||
defer file.close();
|
||||
|
||||
const samples = try readSamplesFromFile(self.allocator, file);
|
||||
errdefer self.allocator.free(samples);
|
||||
|
||||
var min_value: f64 = 0;
|
||||
var max_value: f64 = 0;
|
||||
if (samples.len > 0) {
|
||||
min_value = samples[0];
|
||||
max_value = samples[0];
|
||||
|
||||
for (samples) |sample| {
|
||||
min_value = @min(min_value, sample);
|
||||
max_value = @max(max_value, sample);
|
||||
}
|
||||
}
|
||||
|
||||
const loaded_file_index = findFreeSlot(FileChannel, &self.loaded_files) orelse return error.FileLimitReached;
|
||||
|
||||
self.loaded_files[loaded_file_index] = FileChannel{
|
||||
.min_value = min_value,
|
||||
.max_value = max_value,
|
||||
.path = path_dupe,
|
||||
.samples = samples
|
||||
};
|
||||
errdefer self.loaded_files[loaded_file_index] = null;
|
||||
|
||||
const margin = 0.1;
|
||||
const sample_range = max_value - min_value;
|
||||
self.channel_views.appendAssumeCapacity(ChannelView{
|
||||
.view_rect = .{
|
||||
.from = 0,
|
||||
.to = @floatFromInt(samples.len),
|
||||
.min_value = min_value - sample_range * margin,
|
||||
.max_value = max_value + sample_range * margin
|
||||
},
|
||||
.source = .{ .file = loaded_file_index }
|
||||
});
|
||||
errdefer _ = self.channel_views.pop();
|
||||
|
||||
}
|
||||
|
||||
pub fn appendChannelFromDevice(self: *App, channel_name: []const u8) !void {
|
||||
const device_channel_index = findFreeSlot(DeviceChannel, &self.device_channels) orelse return error.DeviceChannelLimitReached;
|
||||
|
||||
const name_buff = try DeviceChannel.Name.fromSlice(channel_name);
|
||||
const channel_name_z = name_buff.buffer[0..name_buff.len :0];
|
||||
|
||||
const device = NIDaq.getDeviceNameFromChannel(channel_name) orelse return error.InvalidChannelName;
|
||||
const device_buff = try NIDaq.BoundedDeviceName.fromSlice(device);
|
||||
const device_z = device_buff.buffer[0..device_buff.len :0];
|
||||
|
||||
var min_value: f64 = 0;
|
||||
var max_value: f64 = 1;
|
||||
const voltage_ranges = try self.ni_daq.listDeviceAOVoltageRanges(device_z);
|
||||
if (voltage_ranges.len > 0) {
|
||||
min_value = voltage_ranges[0].low;
|
||||
max_value = voltage_ranges[0].high;
|
||||
}
|
||||
|
||||
const max_sample_rate = try self.ni_daq.getMaxSampleRate(channel_name_z);
|
||||
|
||||
self.device_channels[device_channel_index] = DeviceChannel{
|
||||
.name = name_buff,
|
||||
.min_sample_rate = self.ni_daq.getMinSampleRate(channel_name_z) catch max_sample_rate,
|
||||
.max_sample_rate = max_sample_rate,
|
||||
.min_value = min_value,
|
||||
.max_value = max_value,
|
||||
.samples = std.ArrayList(f64).init(self.allocator)
|
||||
};
|
||||
errdefer self.device_channels[device_channel_index] = null;
|
||||
|
||||
self.channel_views.appendAssumeCapacity(ChannelView{
|
||||
.view_rect = .{
|
||||
.from = 0,
|
||||
.to = 0,
|
||||
.min_value = min_value,
|
||||
.max_value = max_value
|
||||
},
|
||||
.source = .{ .device = device_channel_index }
|
||||
});
|
||||
errdefer _ = self.channel_views.pop();
|
||||
}
|
||||
|
||||
fn getChannelSource(self: *App, channel_view: *ChannelView) ?ChannelView.SourceObject {
|
||||
switch (channel_view.source) {
|
||||
.file => |index| {
|
||||
if (self.loaded_files[index]) |*loaded_file| {
|
||||
return ChannelView.SourceObject{
|
||||
.file = loaded_file
|
||||
};
|
||||
}
|
||||
},
|
||||
.device => |index| {
|
||||
if (self.device_channels[index]) |*device_channel| {
|
||||
return ChannelView.SourceObject{
|
||||
.device = device_channel
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn showChannelViewSlider(self: *App, view_rect: *Graph.ViewOptions, sample_count: f32) void {
|
||||
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()) {
|
||||
view_rect.from += remap(
|
||||
f32,
|
||||
0, minimap_rect.width,
|
||||
0, sample_count,
|
||||
left_signal.drag.x
|
||||
);
|
||||
|
||||
view_rect.from = clamp(view_rect.from, 0, view_rect.to-min_visible_samples);
|
||||
}
|
||||
|
||||
const right_signal = self.ui.signalFromBox(right_knob_box);
|
||||
if (right_signal.dragged()) {
|
||||
view_rect.to += remap(
|
||||
f32,
|
||||
0, minimap_rect.width,
|
||||
0, sample_count,
|
||||
right_signal.drag.x
|
||||
);
|
||||
|
||||
view_rect.to = clamp(view_rect.to, 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, -view_rect.from, sample_count - view_rect.to);
|
||||
|
||||
view_rect.from += samples_moved;
|
||||
view_rect.to += samples_moved;
|
||||
}
|
||||
|
||||
left_knob_box.setFixedX(remap(f32,
|
||||
0, sample_count,
|
||||
0, minimap_rect.width - left_knob_size - right_knob_size,
|
||||
view_rect.from
|
||||
));
|
||||
|
||||
right_knob_box.setFixedX(remap(f32,
|
||||
0, sample_count,
|
||||
left_knob_size, minimap_rect.width - right_knob_size,
|
||||
view_rect.to
|
||||
));
|
||||
|
||||
middle_box.setFixedX(remap(f32,
|
||||
0, sample_count,
|
||||
left_knob_size, minimap_rect.width - right_knob_size,
|
||||
view_rect.from
|
||||
));
|
||||
middle_box.setFixedWidth(remap(f32,
|
||||
0, sample_count,
|
||||
0, minimap_rect.width - right_knob_size - left_knob_size,
|
||||
view_rect.to - view_rect.from
|
||||
));
|
||||
}
|
||||
|
||||
fn showChannelView(self: *App, channel_view: *ChannelView) !void {
|
||||
const source = self.getChannelSource(channel_view) orelse return;
|
||||
const samples = source.samples();
|
||||
source.lockSamples();
|
||||
defer source.unlockSamples();
|
||||
|
||||
const channel_box = self.ui.newBoxFromPtr(channel_view);
|
||||
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 tools_box = self.ui.newBoxFromString("Graph tools");
|
||||
tools_box.background = rl.Color.gray;
|
||||
tools_box.layout_axis = .X;
|
||||
tools_box.size.x = UI.Size.percent(1, 0);
|
||||
tools_box.size.y = UI.Size.pixels(32, 1);
|
||||
self.ui.pushParent(tools_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
if (source == .device) {
|
||||
const device_channel = source.device;
|
||||
|
||||
{
|
||||
const record_button = self.ui.newBoxFromString("Record");
|
||||
record_button.flags.insert(.clickable);
|
||||
record_button.size.x = UI.Size.text(1, 0);
|
||||
record_button.size.y = UI.Size.percent(1, 0);
|
||||
|
||||
if (device_channel.active_task == null) {
|
||||
record_button.setText(.text, "Record");
|
||||
} else {
|
||||
record_button.setText(.text, "Stop");
|
||||
}
|
||||
|
||||
const signal = self.ui.signalFromBox(record_button);
|
||||
if (signal.clicked()) {
|
||||
if (device_channel.active_task) |task| {
|
||||
try task.stop();
|
||||
device_channel.active_task = null;
|
||||
} else {
|
||||
const channel_name = device_channel.name.buffer[0..device_channel.name.len :0];
|
||||
device_channel.active_task = try self.task_pool.launchAIVoltageChannel(
|
||||
&device_channel.mutex,
|
||||
&device_channel.samples,
|
||||
.{
|
||||
.continous = .{ .sample_rate = device_channel.max_sample_rate }
|
||||
},
|
||||
.{
|
||||
.min_value = device_channel.min_value,
|
||||
.max_value = device_channel.max_value,
|
||||
.units = device_channel.units,
|
||||
.channel = channel_name
|
||||
}
|
||||
);
|
||||
|
||||
channel_view.follow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const follow_button = self.ui.newBoxFromString("Follow");
|
||||
follow_button.flags.insert(.clickable);
|
||||
follow_button.size.x = UI.Size.text(1, 0);
|
||||
follow_button.size.y = UI.Size.percent(1, 0);
|
||||
follow_button.setText(.text, if (channel_view.follow) "Unfollow" else "Follow");
|
||||
|
||||
const signal = self.ui.signalFromBox(follow_button);
|
||||
if (signal.clicked()) {
|
||||
channel_view.follow = !channel_view.follow;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const graph_box = self.ui.newBoxFromString("Graph");
|
||||
graph_box.background = rl.Color.blue;
|
||||
graph_box.size.x = UI.Size.percent(1, 0);
|
||||
graph_box.size.y = UI.Size.pixels(channel_view.height, 1);
|
||||
|
||||
Graph.drawCached(&channel_view.view_cache, graph_box.persistent.size, channel_view.view_rect, samples);
|
||||
if (channel_view.view_cache.texture) |texture| {
|
||||
graph_box.texture = texture.texture;
|
||||
}
|
||||
}
|
||||
|
||||
self.showChannelViewSlider(
|
||||
&channel_view.view_rect,
|
||||
@floatFromInt(samples.len)
|
||||
);
|
||||
}
|
||||
|
||||
fn showChannelsWindow(self: *App) !void {
|
||||
const scroll_area = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels"));
|
||||
defer self.ui.popScrollbar();
|
||||
scroll_area.layout_axis = .Y;
|
||||
scroll_area.layout_gap = 16;
|
||||
|
||||
for (self.channel_views.slice()) |*channel_view| {
|
||||
try self.showChannelView(channel_view);
|
||||
}
|
||||
|
||||
{
|
||||
const prompt_box = self.ui.newBoxFromString("Add prompt");
|
||||
prompt_box.layout_axis = .X;
|
||||
prompt_box.size.x = UI.Size.percent(1, 0);
|
||||
prompt_box.size.y = UI.Size.pixels(150, 1);
|
||||
self.ui.pushParent(prompt_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
self.ui.spacer(.{ .x = UI.Size.percent(1, 0) });
|
||||
|
||||
const from_file_button = self.ui.button(.text, "Add from file");
|
||||
from_file_button.background = srcery.green;
|
||||
if (self.ui.signalFromBox(from_file_button).clicked()) {
|
||||
log.debug("TODO: Not implemented", .{});
|
||||
}
|
||||
|
||||
self.ui.spacer(.{ .x = UI.Size.pixels(32, 1) });
|
||||
|
||||
const from_device_button = self.ui.button(.text, "Add from device");
|
||||
from_device_button.background = srcery.green;
|
||||
if (self.ui.signalFromBox(from_device_button).clicked()) {
|
||||
log.debug("TODO: Not implemented", .{});
|
||||
}
|
||||
|
||||
self.ui.spacer(.{ .x = UI.Size.percent(1, 0) });
|
||||
}
|
||||
}
|
||||
|
||||
fn findChannelIndexByName(haystack: []const [:0]const u8, needle: [:0]const u8) ?usize {
|
||||
for (0.., haystack) |i, item| {
|
||||
if (std.mem.eql(u8, item, needle)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn showAddFromDeviceWindow(self: *App) !void {
|
||||
const window = self.ui.newBoxFromString("Device window");
|
||||
window.size.x = UI.Size.percent(1, 0);
|
||||
window.size.y = UI.Size.percent(1, 0);
|
||||
window.layout_axis = .X;
|
||||
self.ui.pushParent(window);
|
||||
defer self.ui.popParent();
|
||||
|
||||
{
|
||||
const filters_box = self.ui.newBoxFromString("Filters box");
|
||||
filters_box.size.x = UI.Size.percent(0.5, 1);
|
||||
filters_box.size.y = UI.Size.percent(1, 0);
|
||||
filters_box.layout_axis = .Y;
|
||||
self.ui.pushParent(filters_box);
|
||||
defer self.ui.popParent();
|
||||
|
||||
for (try self.ni_daq.listDeviceNames()) |device| {
|
||||
const device_box = self.ui.newBoxFromString(device);
|
||||
device_box.flags.insert(.clickable);
|
||||
device_box.size.x = UI.Size.text(2, 1);
|
||||
device_box.size.y = UI.Size.text(2, 1);
|
||||
device_box.setText(.text, device);
|
||||
|
||||
const signal = self.ui.signalFromBox(device_box);
|
||||
if (signal.clicked()) {
|
||||
self.device_filter = try NIDaq.BoundedDeviceName.fromSlice(device);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const toggle_inputs_box = self.ui.newBoxFromString("Toggle inputs");
|
||||
toggle_inputs_box.flags.insert(.clickable);
|
||||
toggle_inputs_box.size.x = UI.Size.text(2, 1);
|
||||
toggle_inputs_box.size.y = UI.Size.text(2, 1);
|
||||
toggle_inputs_box.setText(.text, if (self.show_voltage_analog_inputs) "Hide inputs" else "Show inputs");
|
||||
const signal = self.ui.signalFromBox(toggle_inputs_box);
|
||||
if (signal.clicked()) {
|
||||
self.show_voltage_analog_inputs = !self.show_voltage_analog_inputs;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const toggle_outputs_box = self.ui.newBoxFromString("Toggle outputs");
|
||||
toggle_outputs_box.flags.insert(.clickable);
|
||||
toggle_outputs_box.size.x = UI.Size.text(2, 1);
|
||||
toggle_outputs_box.size.y = UI.Size.text(2, 1);
|
||||
toggle_outputs_box.setText(.text, if (self.show_voltage_analog_outputs) "Hide outputs" else "Show outputs");
|
||||
const signal = self.ui.signalFromBox(toggle_outputs_box);
|
||||
if (signal.clicked()) {
|
||||
self.show_voltage_analog_outputs = !self.show_voltage_analog_outputs;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const add_button = self.ui.newBoxFromString("Add");
|
||||
add_button.flags.insert(.clickable);
|
||||
add_button.size.x = UI.Size.text(2, 1);
|
||||
add_button.size.y = UI.Size.text(2, 1);
|
||||
add_button.setText(.text, "Add selected");
|
||||
const signal = self.ui.signalFromBox(add_button);
|
||||
if (signal.clicked()) {
|
||||
const selected_devices = self.selected_channels.constSlice();
|
||||
|
||||
for (selected_devices) |channel| {
|
||||
try self.appendChannelFromDevice(channel);
|
||||
}
|
||||
|
||||
for (selected_devices) |channel| {
|
||||
self.allocator.free(channel);
|
||||
}
|
||||
self.selected_channels.len = 0;
|
||||
|
||||
self.shown_window = .channels;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const channels_box = self.ui.pushScrollbar(self.ui.newKeyFromString("Channels list"));
|
||||
defer self.ui.popScrollbar();
|
||||
channels_box.layout_axis = .Y;
|
||||
channels_box.size.x = UI.Size.percent(1, 0);
|
||||
|
||||
var devices: []const [:0]const u8 = &.{};
|
||||
if (self.device_filter.len > 0) {
|
||||
devices = &.{
|
||||
self.device_filter.buffer[0..self.device_filter.len :0]
|
||||
};
|
||||
} else {
|
||||
devices = try self.ni_daq.listDeviceNames();
|
||||
}
|
||||
|
||||
for (devices) |device| {
|
||||
var ai_voltage_physical_channels: []const [:0]const u8 = &.{};
|
||||
if (self.show_voltage_analog_inputs) {
|
||||
if (try self.ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
|
||||
ai_voltage_physical_channels = try self.ni_daq.listDeviceAIPhysicalChannels(device);
|
||||
}
|
||||
}
|
||||
|
||||
var ao_physical_channels: []const [:0]const u8 = &.{};
|
||||
if (self.show_voltage_analog_outputs) {
|
||||
if (try self.ni_daq.checkDeviceAOOutputType(device, .Voltage)) {
|
||||
ao_physical_channels = try self.ni_daq.listDeviceAOPhysicalChannels(device);
|
||||
}
|
||||
}
|
||||
|
||||
inline for (.{ ai_voltage_physical_channels, ao_physical_channels }) |channels| {
|
||||
for (channels) |channel| {
|
||||
const selected_channels_slice = self.selected_channels.constSlice();
|
||||
|
||||
const channel_box = self.ui.newBoxFromString(channel);
|
||||
channel_box.flags.insert(.clickable);
|
||||
channel_box.size.x = UI.Size.text(1, 1);
|
||||
channel_box.size.y = UI.Size.text(0.5, 1);
|
||||
channel_box.setText(.text, channel);
|
||||
|
||||
if (findChannelIndexByName(selected_channels_slice, channel) != null) {
|
||||
channel_box.background = srcery.xgray3;
|
||||
}
|
||||
|
||||
const signal = self.ui.signalFromBox(channel_box);
|
||||
if (signal.clicked()) {
|
||||
if (findChannelIndexByName(selected_channels_slice, channel)) |index| {
|
||||
self.allocator.free(self.selected_channels.swapRemove(index));
|
||||
} else {
|
||||
self.selected_channels.appendAssumeCapacity(try self.allocator.dupeZ(u8, channel));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(.text, "Add from file",);
|
||||
|
||||
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(.text, "Add from device");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn updateUI(self: *App) !void {
|
||||
self.ui.begin();
|
||||
defer self.ui.end();
|
||||
|
||||
const root_box = self.ui.getParent().?;
|
||||
root_box.layout_axis = .Y;
|
||||
|
||||
self.showToolbar();
|
||||
|
||||
if (self.shown_window == .channels) {
|
||||
try self.showChannelsWindow();
|
||||
} else if (self.shown_window == .add_from_device) {
|
||||
try self.showAddFromDeviceWindow();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(self: *App) !void {
|
||||
for (self.channel_views.slice()) |*_view| {
|
||||
const view: *ChannelView = _view;
|
||||
const source = self.getChannelSource(view) orelse continue;
|
||||
if (source == .device) {
|
||||
if (view.follow) {
|
||||
source.lockSamples();
|
||||
defer source.unlockSamples();
|
||||
|
||||
const sample_count: f32 = @floatFromInt(source.samples().len);
|
||||
const view_size = view.view_rect.to - view.view_rect.from;
|
||||
view.view_rect.from = sample_count - view_size;
|
||||
view.view_rect.to = sample_count;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
rl.clearBackground(srcery.black);
|
||||
|
||||
if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) {
|
||||
Platform.toggleConsoleWindow();
|
||||
}
|
||||
|
||||
if (rl.isFileDropped()) {
|
||||
const file_list = rl.loadDroppedFiles();
|
||||
defer rl.unloadDroppedFiles(file_list);
|
||||
|
||||
for (file_list.paths[0..file_list.count]) |path| {
|
||||
const path_len = std.mem.indexOfSentinel(u8, 0, path);
|
||||
try self.appendChannelFromFile(path[0..path_len]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// On the first frame, render the UI twice.
|
||||
// So that on the second pass widgets that depend on sizes from other widgets have settled
|
||||
if (self.ui.frame_index == 0) {
|
||||
try self.updateUI();
|
||||
}
|
||||
|
||||
try self.updateUI();
|
||||
self.ui.draw();
|
||||
}
|
66
src/aseprite.zig
Normal file
66
src/aseprite.zig
Normal file
@ -0,0 +1,66 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const c = @cImport({
|
||||
@cInclude("cute_aseprite.h");
|
||||
});
|
||||
|
||||
const assert = std.debug.assert;
|
||||
|
||||
ase: *c.ase_t,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, memory: []const u8) !@This() {
|
||||
_ = allocator; // TODO: Pass allocator to function
|
||||
|
||||
const parsed = c.cute_aseprite_load_from_memory(@ptrCast(memory), @intCast(memory.len), null);
|
||||
if (parsed == null) {
|
||||
return error.CuteLoadFromMemory;
|
||||
}
|
||||
|
||||
return @This(){
|
||||
.ase = @ptrCast(parsed)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: @This()) void {
|
||||
c.cute_aseprite_free(self.ase);
|
||||
}
|
||||
|
||||
pub fn getTag(self: @This(), name: []const u8) ?c.struct_ase_tag_t {
|
||||
const tag_count: usize = @intCast(self.ase.tag_count);
|
||||
for (self.ase.tags[0..tag_count]) |tag| {
|
||||
const tag_name = std.mem.span(tag.name);
|
||||
if (std.mem.eql(u8, tag_name, name)) {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getFrameImage(self: @This(), frame_index: usize) rl.Image {
|
||||
const width: usize = @intCast(self.ase.w);
|
||||
const height: usize = @intCast(self.ase.h);
|
||||
|
||||
var image = rl.genImageColor(@intCast(width), @intCast(height), rl.Color.black.alpha(0));
|
||||
assert(@intFromPtr(image.data) != 0);
|
||||
|
||||
image.setFormat(rl.PixelFormat.pixelformat_uncompressed_r8g8b8a8);
|
||||
|
||||
const pixel_count = width * height;
|
||||
const frame = self.ase.frames[frame_index];
|
||||
for (0.., frame.pixels[0..pixel_count]) |pixel_index, pixel| {
|
||||
const x = @mod(pixel_index, width);
|
||||
const y = @divFloor(pixel_index, height);
|
||||
image.drawPixel(@intCast(x), @intCast(y), .{
|
||||
.r = pixel.r,
|
||||
.g = pixel.g,
|
||||
.b = pixel.b,
|
||||
.a = pixel.a,
|
||||
});
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
pub fn getTagImage(self: @This(), tag: c.struct_ase_tag_t) rl.Image {
|
||||
return getFrameImage(self, @intCast(tag.from_frame));
|
||||
}
|
88
src/assets.zig
Normal file
88
src/assets.zig
Normal file
@ -0,0 +1,88 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const srcery = @import("./srcery.zig");
|
||||
const FontFace = @import("./font-face.zig");
|
||||
const Aseprite = @import("./aseprite.zig");
|
||||
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const FontId = enum {
|
||||
text
|
||||
};
|
||||
|
||||
var loaded_fonts: std.BoundedArray(rl.Font, 32) = .{};
|
||||
|
||||
const FontArray = std.EnumArray(FontId, FontFace);
|
||||
var fonts: FontArray = FontArray.initUndefined();
|
||||
|
||||
pub var grab_texture: struct {
|
||||
normal: rl.Texture2D,
|
||||
hot: rl.Texture2D,
|
||||
active: rl.Texture2D,
|
||||
} = undefined;
|
||||
|
||||
pub fn font(font_id: FontId) FontFace {
|
||||
return fonts.get(font_id);
|
||||
}
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !void {
|
||||
const roboto_regular = @embedFile("./assets/fonts/roboto/Roboto-Regular.ttf");
|
||||
|
||||
const default_font = try loadFont(roboto_regular, 16);
|
||||
|
||||
fonts = FontArray.init(.{
|
||||
.text = FontFace{ .font = default_font, .line_height = 1.2 }
|
||||
});
|
||||
|
||||
const grab_ase = try Aseprite.init(allocator, @embedFile("./assets/grab-marker.ase"));
|
||||
defer grab_ase.deinit();
|
||||
|
||||
const grab_normal_image = grab_ase.getTagImage(grab_ase.getTag("normal") orelse return error.TagNotFound);
|
||||
defer grab_normal_image.unload();
|
||||
const grab_normal_texture = rl.loadTextureFromImage(grab_normal_image);
|
||||
errdefer grab_normal_texture.unload();
|
||||
|
||||
const grab_hot_image = grab_ase.getTagImage(grab_ase.getTag("hot") orelse return error.TagNotFound);
|
||||
defer grab_hot_image.unload();
|
||||
const grab_hot_texture = rl.loadTextureFromImage(grab_hot_image);
|
||||
errdefer grab_hot_texture.unload();
|
||||
|
||||
const grab_active_image = grab_ase.getTagImage(grab_ase.getTag("active") orelse return error.TagNotFound);
|
||||
defer grab_active_image.unload();
|
||||
const grab_active_texture = rl.loadTextureFromImage(grab_active_image);
|
||||
errdefer grab_active_texture.unload();
|
||||
|
||||
grab_texture = .{
|
||||
.normal = grab_normal_texture,
|
||||
.hot = grab_hot_texture,
|
||||
.active = grab_active_texture
|
||||
};
|
||||
}
|
||||
|
||||
fn loadFont(ttf_data: []const u8, font_size: u32) !rl.Font {
|
||||
var codepoints: [95]i32 = undefined;
|
||||
for (0..codepoints.len) |i| {
|
||||
codepoints[i] = @as(i32, @intCast(i)) + 32;
|
||||
}
|
||||
|
||||
const loaded_font = rl.loadFontFromMemory(".ttf", ttf_data, @intCast(font_size), &codepoints);
|
||||
if (!loaded_font.isReady()) {
|
||||
return error.LoadFontFromMemory;
|
||||
}
|
||||
|
||||
loaded_fonts.appendAssumeCapacity(loaded_font);
|
||||
|
||||
return loaded_font;
|
||||
}
|
||||
|
||||
pub fn deinit(allocator: std.mem.Allocator) void {
|
||||
_ = allocator;
|
||||
|
||||
for (loaded_fonts.slice()) |loaded_font| {
|
||||
loaded_font.unload();
|
||||
}
|
||||
|
||||
grab_texture.active.unload();
|
||||
grab_texture.hot.unload();
|
||||
grab_texture.normal.unload();
|
||||
}
|
202
src/assets/fonts/roboto/LICENSE.txt
Normal file
202
src/assets/fonts/roboto/LICENSE.txt
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
BIN
src/assets/fonts/roboto/Roboto-Regular.ttf
Normal file
BIN
src/assets/fonts/roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/grab-marker.ase
Normal file
BIN
src/assets/grab-marker.ase
Normal file
Binary file not shown.
BIN
src/assets/srcery-pallete.ase
Normal file
BIN
src/assets/srcery-pallete.ase
Normal file
Binary file not shown.
10
src/cute_aseprite.c
Normal file
10
src/cute_aseprite.c
Normal file
@ -0,0 +1,10 @@
|
||||
#include <stddef.h>
|
||||
|
||||
// TODO: Use zig allocators
|
||||
// extern void *zig_cute_aseprite_malloc(void* ctx, size_t size);
|
||||
// extern void zig_cute_aseprite_free(void* ctx, void* mem);
|
||||
|
||||
#define CUTE_ASEPRITE_IMPLEMENTATION
|
||||
// #define CUTE_ASEPRITE_ALLOC(size, ctx) zig_cute_aseprite_malloc(ctx, size)
|
||||
// #define CUTE_ASEPRITE_FREE(mem, ctx) zig_cute_aseprite_free(ctx, mem)
|
||||
#include "cute_aseprite.h"
|
1358
src/cute_aseprite.h
Normal file
1358
src/cute_aseprite.h
Normal file
File diff suppressed because it is too large
Load Diff
@ -18,6 +18,10 @@ pub fn getSize(self: @This()) f32 {
|
||||
return @floatFromInt(self.font.baseSize);
|
||||
}
|
||||
|
||||
pub fn getLineSize(self: @This()) f32 {
|
||||
return self.getSize() * self.line_height;
|
||||
}
|
||||
|
||||
pub fn drawTextLines(self: @This(), lines: []const []const u8, position: rl.Vector2, tint: rl.Color) void {
|
||||
var offset_y: f32 = 0;
|
||||
|
||||
|
259
src/graph.zig
Normal file
259
src/graph.zig
Normal file
@ -0,0 +1,259 @@
|
||||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const srcery = @import("./srcery.zig");
|
||||
|
||||
const remap = @import("./utils.zig").remap;
|
||||
const assert = std.debug.assert;
|
||||
const Vec2 = rl.Vector2;
|
||||
const clamp = std.math.clamp;
|
||||
|
||||
const disable_caching = false;
|
||||
|
||||
comptime {
|
||||
if (builtin.mode != .Debug) {
|
||||
assert(disable_caching == false);
|
||||
}
|
||||
}
|
||||
|
||||
pub const ViewOptions = struct {
|
||||
from: f32, // inclusive
|
||||
to: f32, // inclusive
|
||||
min_value: f64,
|
||||
max_value: f64,
|
||||
left_aligned: bool = true,
|
||||
color: rl.Color = srcery.red,
|
||||
dot_size: f32 = 2
|
||||
};
|
||||
|
||||
pub const Cache = struct {
|
||||
texture: ?rl.RenderTexture2D = null,
|
||||
options: ?ViewOptions = null,
|
||||
|
||||
pub fn deinit(self: *Cache) void {
|
||||
if (self.texture) |texture| {
|
||||
texture.unload();
|
||||
self.texture = null;
|
||||
}
|
||||
self.options = null;
|
||||
}
|
||||
|
||||
pub fn invalidate(self: *Cache) void {
|
||||
self.options = null;
|
||||
}
|
||||
|
||||
pub fn draw(self: Cache, rect: rl.Rectangle) void {
|
||||
if (self.texture) |texture| {
|
||||
const source = rl.Rectangle{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = @floatFromInt(texture.texture.width),
|
||||
.height = @floatFromInt(texture.texture.height)
|
||||
};
|
||||
rl.drawTexturePro(
|
||||
texture.texture,
|
||||
source,
|
||||
rect,
|
||||
rl.Vector2.zero(),
|
||||
0, rl.Color.white
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64) f64 {
|
||||
return remap(
|
||||
f64,
|
||||
view_rect.from, view_rect.to,
|
||||
draw_rect.x, draw_rect.x + draw_rect.width,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
fn mapSampleY(draw_rect: rl.Rectangle, view_rect: ViewOptions, sample: f64) f64 {
|
||||
return remap(
|
||||
f64,
|
||||
view_rect.min_value, view_rect.max_value,
|
||||
draw_rect.y + draw_rect.height, draw_rect.y,
|
||||
sample
|
||||
);
|
||||
}
|
||||
|
||||
fn mapSamplePointToGraph(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64, sample: f64) Vec2 {
|
||||
return .{
|
||||
.x = @floatCast(mapSampleX(draw_rect, view_rect, index)),
|
||||
.y = @floatCast(mapSampleY(draw_rect, view_rect, sample))
|
||||
};
|
||||
}
|
||||
|
||||
fn clampIndex(value: f32, size: usize) f32 {
|
||||
const size_f32: f32 = @floatFromInt(size);
|
||||
return clamp(value, 0, size_f32);
|
||||
}
|
||||
|
||||
fn clampIndexUsize(value: f32, size: usize) usize {
|
||||
const size_f32: f32 = @floatFromInt(size);
|
||||
return @intFromFloat(clamp(value, 0, size_f32));
|
||||
}
|
||||
|
||||
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
assert(options.left_aligned); // TODO:
|
||||
assert(options.to >= options.from);
|
||||
|
||||
if (options.from > @as(f32, @floatFromInt(samples.len))) return;
|
||||
if (options.to < 0) return;
|
||||
|
||||
const sample_count = options.to - options.from;
|
||||
const samples_per_column = sample_count / draw_rect.width;
|
||||
|
||||
const samples_threshold = 2;
|
||||
if (samples_per_column >= samples_threshold) {
|
||||
var i = clampIndex(options.from, samples.len);
|
||||
while (i < clampIndex(options.to, samples.len)) : (i += samples_per_column) {
|
||||
const from_index = clampIndexUsize(i, samples.len);
|
||||
const to_index = clampIndexUsize(i+samples_per_column, samples.len);
|
||||
const column_samples = samples[from_index..to_index];
|
||||
if (column_samples.len == 0) continue;
|
||||
|
||||
var column_min = column_samples[0];
|
||||
var column_max = column_samples[0];
|
||||
|
||||
for (column_samples) |sample| {
|
||||
column_min = @min(column_min, sample);
|
||||
column_max = @max(column_max, sample);
|
||||
}
|
||||
|
||||
const x = mapSampleX(draw_rect, options, @floatFromInt(from_index));
|
||||
const y_min = mapSampleY(draw_rect, options, column_min);
|
||||
const y_max = mapSampleY(draw_rect, options, column_max);
|
||||
|
||||
if (column_samples.len == 1) {
|
||||
rl.drawLineV(
|
||||
mapSamplePointToGraph(draw_rect, options, i, samples[from_index]),
|
||||
mapSamplePointToGraph(draw_rect, options, i-1, samples[clampIndexUsize(i-1, samples.len-1)]),
|
||||
options.color
|
||||
);
|
||||
|
||||
rl.drawLineV(
|
||||
mapSamplePointToGraph(draw_rect, options, i, samples[from_index]),
|
||||
mapSamplePointToGraph(draw_rect, options, i+1, samples[clampIndexUsize(i+1, samples.len-1)]),
|
||||
options.color
|
||||
);
|
||||
} else if (@abs(y_max - y_min) < 1) {
|
||||
rl.drawPixelV(
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
|
||||
options.color
|
||||
);
|
||||
} else {
|
||||
rl.drawLineV(
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
|
||||
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
|
||||
options.color
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rl.beginScissorMode(
|
||||
@intFromFloat(draw_rect.x),
|
||||
@intFromFloat(draw_rect.y),
|
||||
@intFromFloat(draw_rect.width),
|
||||
@intFromFloat(draw_rect.height),
|
||||
);
|
||||
defer rl.endScissorMode();
|
||||
|
||||
{
|
||||
const from_index = clampIndexUsize(@floor(options.from), samples.len);
|
||||
const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len);
|
||||
|
||||
if (to_index - from_index > 0) {
|
||||
for (from_index..(to_index-1)) |i| {
|
||||
const from_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i), samples[i]);
|
||||
const to_point = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i + 1), samples[i + 1]);
|
||||
rl.drawLineV(from_point, to_point, options.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const from_index = clampIndexUsize(@ceil(options.from), samples.len);
|
||||
const to_index = clampIndexUsize(@ceil(options.to), samples.len);
|
||||
|
||||
const min_circle_size = 0.5;
|
||||
const max_circle_size = options.dot_size;
|
||||
var circle_size = remap(f32, samples_threshold, 0.2, min_circle_size, max_circle_size, samples_per_column);
|
||||
circle_size = @min(circle_size, max_circle_size);
|
||||
|
||||
for (from_index..to_index) |i| {
|
||||
const center = mapSamplePointToGraph(draw_rect, options, @floatFromInt(i), samples[i]);
|
||||
rl.drawCircleV(center, circle_size, options.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
|
||||
const render_width: i32 = @intFromFloat(@ceil(render_size.x));
|
||||
const render_height: i32 = @intFromFloat(@ceil(render_size.y));
|
||||
|
||||
if (render_width <= 0 or render_height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unload render texture if rendering width or height changed
|
||||
if (cache.texture) |render_texture| {
|
||||
const texure = render_texture.texture;
|
||||
if (texure.width != render_width or texure.height != render_height) {
|
||||
render_texture.unload();
|
||||
cache.texture = null;
|
||||
cache.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (cache.texture == null) {
|
||||
const texture = rl.loadRenderTexture(render_width, render_height);
|
||||
// TODO: Maybe fallback to just drawing without caching, if GPU doesn't have enough memory?
|
||||
assert(rl.isRenderTextureReady(texture));
|
||||
cache.texture = texture;
|
||||
}
|
||||
|
||||
const render_texture = cache.texture.?;
|
||||
|
||||
if (cache.options != null and std.meta.eql(cache.options.?, options)) {
|
||||
// Cached graph hasn't changed, no need to redraw.
|
||||
return;
|
||||
}
|
||||
|
||||
cache.options = options;
|
||||
|
||||
render_texture.begin();
|
||||
defer render_texture.end();
|
||||
|
||||
rl.gl.rlPushMatrix();
|
||||
defer rl.gl.rlPopMatrix();
|
||||
|
||||
rl.clearBackground(rl.Color.black.alpha(0));
|
||||
rl.gl.rlTranslatef(0, render_size.y, 0);
|
||||
rl.gl.rlScalef(1, -1, 1);
|
||||
|
||||
const draw_rect = rl.Rectangle{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = render_size.x,
|
||||
.height = render_size.y
|
||||
};
|
||||
drawSamples(draw_rect, options, samples);
|
||||
}
|
||||
|
||||
pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
|
||||
if (draw_rect.width < 0 or draw_rect.height < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cache != null and !disable_caching) {
|
||||
const c = cache.?;
|
||||
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
|
||||
c.draw(draw_rect);
|
||||
} else {
|
||||
drawSamples(draw_rect, options, samples);
|
||||
}
|
||||
}
|
339
src/main.zig
339
src/main.zig
@ -1,23 +1,26 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const NIDaq = @import("ni-daq.zig");
|
||||
const TaskPool = @import("./task-pool.zig");
|
||||
const Platform = @import("./platform.zig");
|
||||
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;
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const FontFace = @import("font-face.zig");
|
||||
const assert = std.debug.assert;
|
||||
const Vec2 = rl.Vector2;
|
||||
// TODO: Maybe move this to a config.zig or options.zig file.
|
||||
// Have all of the contstants in a single file.
|
||||
pub const version = std.SemanticVersion{
|
||||
.major = 0,
|
||||
.minor = 1,
|
||||
.patch = 0
|
||||
};
|
||||
|
||||
const icon_png = @embedFile("./assets/icon.png");
|
||||
|
||||
fn toRaylibTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
|
||||
fn toRaylibLogLevel(log_level: log.Level) rl.TraceLogLevel {
|
||||
return switch (log_level) {
|
||||
.err => rl.TraceLogLevel.log_error,
|
||||
.warn => rl.TraceLogLevel.log_warning,
|
||||
@ -26,14 +29,14 @@ fn toRaylibTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
|
||||
};
|
||||
}
|
||||
|
||||
fn toZigLogLevel(log_type: c_int) ?std.log.Level {
|
||||
fn toZigLogLevel(log_type: c_int) ?log.Level {
|
||||
return switch (log_type) {
|
||||
@intFromEnum(rl.TraceLogLevel.log_trace) => std.log.Level.debug,
|
||||
@intFromEnum(rl.TraceLogLevel.log_debug) => std.log.Level.debug,
|
||||
@intFromEnum(rl.TraceLogLevel.log_info) => std.log.Level.info,
|
||||
@intFromEnum(rl.TraceLogLevel.log_warning) => std.log.Level.warn,
|
||||
@intFromEnum(rl.TraceLogLevel.log_error) => std.log.Level.err,
|
||||
@intFromEnum(rl.TraceLogLevel.log_fatal) => std.log.Level.err,
|
||||
@intFromEnum(rl.TraceLogLevel.log_trace) => log.Level.debug,
|
||||
@intFromEnum(rl.TraceLogLevel.log_debug) => log.Level.debug,
|
||||
@intFromEnum(rl.TraceLogLevel.log_info) => log.Level.info,
|
||||
@intFromEnum(rl.TraceLogLevel.log_warning) => log.Level.warn,
|
||||
@intFromEnum(rl.TraceLogLevel.log_error) => log.Level.err,
|
||||
@intFromEnum(rl.TraceLogLevel.log_fatal) => log.Level.err,
|
||||
else => null
|
||||
};
|
||||
}
|
||||
@ -41,144 +44,56 @@ fn toZigLogLevel(log_type: c_int) ?std.log.Level {
|
||||
fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_list) callconv(.C) void {
|
||||
const log_level = toZigLogLevel(logType) orelse return;
|
||||
|
||||
// TODO: Skip formatting buffer, if logging is not enabled for that level.
|
||||
const scope = .raylib;
|
||||
const raylib_log = std.log.scoped(scope);
|
||||
|
||||
const max_tracelog_msg_length = 256; // from utils.c in raylib
|
||||
var buffer: [max_tracelog_msg_length:0]u8 = undefined;
|
||||
|
||||
inline for (std.meta.fields(std.log.Level)) |field| {
|
||||
const message_level: std.log.Level = @enumFromInt(field.value);
|
||||
if (std.log.logEnabled(message_level, scope) and log_level == message_level) {
|
||||
@memset(&buffer, 0);
|
||||
const text_length = raylib_h.vsnprintf(&buffer, buffer.len, text, args);
|
||||
|
||||
const formatted_text = buffer[0..@intCast(text_length)];
|
||||
|
||||
const raylib_log = std.log.scoped(.raylib);
|
||||
switch (log_level) {
|
||||
.debug => raylib_log.debug("{s}", .{ formatted_text }),
|
||||
.info => raylib_log.info("{s}", .{ formatted_text }),
|
||||
.warn => raylib_log.warn("{s}", .{ formatted_text }),
|
||||
.err => raylib_log.err("{s}", .{ formatted_text })
|
||||
const log_function = @field(raylib_log, field.name);
|
||||
@call(.auto, log_function, .{ "{s}", .{formatted_text} });
|
||||
}
|
||||
}
|
||||
|
||||
fn remap(from_min: f32, from_max: f32, to_min: f32, to_max: f32, value: f32) f32 {
|
||||
const t = (value - from_min) / (from_max - from_min);
|
||||
return std.math.lerp(to_min, to_max, t);
|
||||
}
|
||||
|
||||
const Channel = struct {
|
||||
color: rl.Color,
|
||||
min_sample: f64,
|
||||
max_sample: f64,
|
||||
|
||||
fn init() Channel {
|
||||
return Channel{
|
||||
.color = rl.Color.red,
|
||||
.min_sample = 0,
|
||||
.max_sample = 0
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Application = struct {
|
||||
allocator: Allocator,
|
||||
channels: std.ArrayList(Channel),
|
||||
channel_samples: ?*TaskPool.ChannelSamples = null,
|
||||
|
||||
task_pool: TaskPool,
|
||||
|
||||
fn init(allocator: Allocator, task_pool_options: TaskPool.Options) !Application {
|
||||
return Application{
|
||||
.allocator = allocator,
|
||||
.task_pool = try TaskPool.init(allocator, task_pool_options),
|
||||
.channels = std.ArrayList(Channel).init(allocator)
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *Application) void {
|
||||
self.channels.deinit();
|
||||
self.task_pool.deinit(self.allocator);
|
||||
}
|
||||
|
||||
fn appendChannel(self: *Application) !*Channel {
|
||||
try self.channels.append(Channel.init());
|
||||
return &self.channels.items[self.channels.items.len-1];
|
||||
}
|
||||
};
|
||||
|
||||
pub fn nanoToSeconds(ns: i128) f32 {
|
||||
return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s;
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
const start_time = std.time.nanoTimestamp();
|
||||
|
||||
// TODO: Setup logging to a file
|
||||
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
|
||||
rl.setTraceLogLevel(toRaylibTraceLogLevel(std.log.default_level));
|
||||
rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level));
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
var ni_daq = try NIDaq.init(allocator, .{
|
||||
.max_devices = 4,
|
||||
.max_analog_inputs = 32,
|
||||
.max_analog_outputs = 8,
|
||||
.max_counter_outputs = 8,
|
||||
.max_counter_inputs = 8,
|
||||
.max_analog_input_voltage_ranges = 4,
|
||||
.max_analog_output_voltage_ranges = 4
|
||||
});
|
||||
defer ni_daq.deinit(allocator);
|
||||
// const devices = try ni_daq.listDeviceNames();
|
||||
|
||||
const devices = try ni_daq.listDeviceNames();
|
||||
// for (devices) |device| {
|
||||
// if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
|
||||
// const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device);
|
||||
// assert(voltage_ranges.len > 0);
|
||||
|
||||
log.info("NI-DAQ version: {}", .{try NIDaq.version()});
|
||||
// const min_sample = voltage_ranges[0].low;
|
||||
// const max_sample = voltage_ranges[0].high;
|
||||
|
||||
std.debug.print("Devices ({}):\n", .{devices.len});
|
||||
for (devices) |device| {
|
||||
std.debug.print(" * '{s}' ({})\n", .{device, device.len});
|
||||
|
||||
const analog_inputs = try ni_daq.listDeviceAIPhysicalChannels(device);
|
||||
for (analog_inputs) |channel_name| {
|
||||
std.debug.print(" * '{s}' (Analog input)\n", .{channel_name});
|
||||
}
|
||||
|
||||
for (try ni_daq.listDeviceAOPhysicalChannels(device)) |channel_name| {
|
||||
std.debug.print(" * '{s}' (Analog output)\n", .{channel_name});
|
||||
}
|
||||
|
||||
// for (try ni_daq.listDeviceCOPhysicalChannels(device)) |channel_name| {
|
||||
// std.debug.print(" * '{s}' (Counter output)\n", .{channel_name});
|
||||
// for (try ni_daq.listDeviceAIPhysicalChannels(device)) |channel_name| {
|
||||
// var channel = try app.appendChannel();
|
||||
// channel.min_sample = min_sample;
|
||||
// channel.max_sample = max_sample;
|
||||
// try app.task_pool.createAIVoltageChannel(ni_daq, .{
|
||||
// .channel = channel_name,
|
||||
// .min_value = min_sample,
|
||||
// .max_value = max_sample,
|
||||
// });
|
||||
// break;
|
||||
// }
|
||||
|
||||
// for (try ni_daq.listDeviceCIPhysicalChannels(device)) |channel_name| {
|
||||
// std.debug.print(" * '{s}' (Counter input)\n", .{channel_name});
|
||||
// }
|
||||
}
|
||||
|
||||
var app = try Application.init(allocator, .{
|
||||
.max_tasks = devices.len * 2,
|
||||
.max_channels = 64
|
||||
});
|
||||
defer app.deinit();
|
||||
|
||||
for (devices) |device| {
|
||||
if (try ni_daq.checkDeviceAIMeasurementType(device, .Voltage)) {
|
||||
const voltage_ranges = try ni_daq.listDeviceAIVoltageRanges(device);
|
||||
assert(voltage_ranges.len > 0);
|
||||
|
||||
const min_sample = voltage_ranges[0].low;
|
||||
const max_sample = voltage_ranges[0].high;
|
||||
|
||||
for (try ni_daq.listDeviceAIPhysicalChannels(device)) |channel_name| {
|
||||
var channel = try app.appendChannel();
|
||||
channel.min_sample = min_sample;
|
||||
channel.max_sample = max_sample;
|
||||
try app.task_pool.createAIVoltageChannel(ni_daq, .{
|
||||
.channel = channel_name,
|
||||
.min_value = min_sample,
|
||||
.max_value = max_sample,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if (try ni_daq.checkDeviceAOOutputType(device, .Voltage)) {
|
||||
// const voltage_ranges = try ni_daq.listDeviceAOVoltageRanges(device);
|
||||
@ -198,143 +113,83 @@ pub fn main() !void {
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// }
|
||||
|
||||
for (0.., app.channels.items) |i, *channel| {
|
||||
channel.color = rl.Color.fromHSV(@as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(app.channels.items.len)) * 360, 0.75, 0.8);
|
||||
}
|
||||
// for (0.., app.channels.items) |i, *channel| {
|
||||
// channel.color = rl.Color.fromHSV(@as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(app.channels.items.len)) * 360, 0.75, 0.8);
|
||||
// }
|
||||
|
||||
const sample_rate: f64 = 5000;
|
||||
try app.task_pool.setContinousSampleRate(sample_rate);
|
||||
// const sample_rate: f64 = 5000;
|
||||
// try app.task_pool.setContinousSampleRate(sample_rate);
|
||||
|
||||
var channel_samples = try app.task_pool.start(0.01, allocator);
|
||||
defer channel_samples.deinit();
|
||||
defer app.task_pool.stop() catch @panic("stop task failed");
|
||||
// var channel_samples = try app.task_pool.start(0.01, allocator);
|
||||
// defer channel_samples.deinit();
|
||||
// defer app.task_pool.stop() catch @panic("stop task failed");
|
||||
|
||||
app.channel_samples = channel_samples;
|
||||
// app.channel_samples = channel_samples;
|
||||
|
||||
const icon_png = @embedFile("./assets/icon.png");
|
||||
var icon_image = rl.loadImageFromMemory(".png", icon_png);
|
||||
defer icon_image.unload();
|
||||
|
||||
rl.initWindow(800, 450, "DAQ view");
|
||||
defer rl.closeWindow();
|
||||
rl.setWindowState(.{ .window_resizable = true });
|
||||
rl.setWindowState(.{ .window_resizable = true, .vsync_hint = true });
|
||||
rl.setWindowMinSize(256, 256);
|
||||
rl.setWindowIcon(icon_image);
|
||||
|
||||
rl.setTargetFPS(60);
|
||||
const target_fps = 60;
|
||||
rl.setTargetFPS(target_fps);
|
||||
|
||||
var font_face = FontFace{
|
||||
.font = rl.getFontDefault()
|
||||
};
|
||||
if (builtin.mode != .Debug) {
|
||||
rl.setExitKey(.key_null);
|
||||
}
|
||||
|
||||
var zoom: f64 = 1.0;
|
||||
try Assets.init(allocator);
|
||||
defer Assets.deinit(allocator);
|
||||
|
||||
var app: Application = undefined;
|
||||
try Application.init(&app, allocator);
|
||||
defer app.deinit();
|
||||
|
||||
if (builtin.mode == .Debug) {
|
||||
try app.appendChannelFromDevice("Dev1/ai0");
|
||||
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin");
|
||||
// try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin");
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
rl.clearBackground(rl.Color.white);
|
||||
|
||||
const window_width: f32 = @floatFromInt(rl.getScreenWidth());
|
||||
const window_height: f32 = @floatFromInt(rl.getScreenHeight());
|
||||
|
||||
rl.drawLineV(
|
||||
Vec2.init(0, window_height/2),
|
||||
Vec2.init(window_width, window_height/2),
|
||||
rl.Color.gray
|
||||
);
|
||||
|
||||
channel_samples.mutex.lock();
|
||||
for (0.., channel_samples.samples) |channel_index, samples| {
|
||||
const channel = app.channels.items[channel_index];
|
||||
const min_sample: f32 = @floatCast(channel.min_sample);
|
||||
const max_sample: f32 = @floatCast(channel.max_sample);
|
||||
|
||||
const max_visible_samples: u32 = @intFromFloat(sample_rate * 20);
|
||||
var shown_samples = samples.items;
|
||||
if (shown_samples.len > max_visible_samples) {
|
||||
shown_samples = samples.items[(samples.items.len-max_visible_samples-1)..(samples.items.len-1)];
|
||||
if (profiler) |*p| {
|
||||
p.start();
|
||||
}
|
||||
|
||||
if (shown_samples.len >= 2) {
|
||||
const color = channel.color; // rl.Color.alpha(channel.color, 1.5 / @as(f32, @floatFromInt(channels.items.len)));
|
||||
try app.tick();
|
||||
|
||||
const samples_per_pixel = max_visible_samples / window_width;
|
||||
|
||||
var i: f32 = 0;
|
||||
while (i < @as(f32, @floatFromInt(shown_samples.len)) - samples_per_pixel) : (i += samples_per_pixel) {
|
||||
const next_i = i + samples_per_pixel;
|
||||
|
||||
var min_slice_sample = shown_samples[@intFromFloat(i)];
|
||||
var max_slice_sample = shown_samples[@intFromFloat(i)];
|
||||
|
||||
for (@intFromFloat(i)..@intFromFloat(next_i)) |sub_i| {
|
||||
min_slice_sample = @min(min_slice_sample, shown_samples[sub_i]);
|
||||
max_slice_sample = @max(max_slice_sample, shown_samples[sub_i]);
|
||||
if (profiler) |*p| {
|
||||
p.stop();
|
||||
if (rl.isKeyPressed(.key_p) and rl.isKeyDown(.key_left_control)) {
|
||||
profiler_shown = !profiler_shown;
|
||||
}
|
||||
|
||||
const offset_i: f32 = @floatFromInt(max_visible_samples - shown_samples.len);
|
||||
|
||||
const start_pos = Vec2.init(
|
||||
(offset_i + i) / max_visible_samples * window_width,
|
||||
remap(min_sample, max_sample, 0, window_height, @as(f32, @floatCast(min_slice_sample)) * @as(f32, @floatCast(zoom)))
|
||||
);
|
||||
|
||||
const end_pos = Vec2.init(
|
||||
(offset_i + i) / max_visible_samples * window_width,
|
||||
remap(min_sample, max_sample, 0, window_height, @as(f32, @floatCast(max_slice_sample)) * @as(f32, @floatCast(zoom)))
|
||||
);
|
||||
rl.drawLineV(start_pos, end_pos, color);
|
||||
if (profiler_shown) {
|
||||
try p.showResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
channel_samples.mutex.unlock();
|
||||
|
||||
if (rl.isKeyPressedRepeat(rl.KeyboardKey.key_e) or rl.isKeyPressed(rl.KeyboardKey.key_e)) {
|
||||
zoom *= 1.1;
|
||||
}
|
||||
if (rl.isKeyPressedRepeat(rl.KeyboardKey.key_q) or rl.isKeyPressed(rl.KeyboardKey.key_q)) {
|
||||
zoom *= 0.9;
|
||||
}
|
||||
|
||||
if (rl.isKeyPressed(rl.KeyboardKey.key_f3)) {
|
||||
Platform.toggleConsoleWindow();
|
||||
}
|
||||
|
||||
const now_ns = std.time.nanoTimestamp();
|
||||
const now_since_start = nanoToSeconds(now_ns - start_time);
|
||||
const now_since_samping_start = nanoToSeconds(now_ns - channel_samples.started_sampling_ns.?);
|
||||
|
||||
{
|
||||
var y: f32 = 10;
|
||||
try font_face.drawTextAlloc(allocator, "Time: {d:.03}", .{now_since_start}, Vec2.init(10, y), rl.Color.black);
|
||||
y += 10;
|
||||
|
||||
try font_face.drawTextAlloc(allocator, "Zoom: {d:.03}", .{zoom}, Vec2.init(10, y), rl.Color.black);
|
||||
y += 10;
|
||||
|
||||
try font_face.drawTextAlloc(allocator, "Dropped samples: {d:.03}", .{app.task_pool.droppedSamples()}, Vec2.init(10, y), rl.Color.black);
|
||||
y += 10;
|
||||
|
||||
for (0..app.channels.items.len) |i| {
|
||||
const sample_count = channel_samples.samples[i].items.len;
|
||||
y += 10;
|
||||
|
||||
try font_face.drawTextAlloc(allocator, "Channel {}:", .{i + 1}, Vec2.init(10, y), rl.Color.black);
|
||||
y += 10;
|
||||
|
||||
try font_face.drawTextAlloc(allocator, "Sample count: {}", .{sample_count}, Vec2.init(20, y), rl.Color.black);
|
||||
y += 10;
|
||||
|
||||
try font_face.drawTextAlloc(allocator, "Sample rate: {d:.03}", .{@as(f64, @floatFromInt(sample_count)) / now_since_samping_start}, Vec2.init(20, y), rl.Color.black);
|
||||
y += 10;
|
||||
}
|
||||
}
|
||||
|
||||
rl.drawFPS(@as(i32, @intFromFloat(window_width)) - 100, 10);
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = NIDaq;
|
||||
_ = @import("./ni-daq.zig");
|
||||
}
|
@ -11,7 +11,16 @@ const log = std.log.scoped(.ni_daq);
|
||||
const max_device_name_size = 255;
|
||||
const max_task_name_size = 255;
|
||||
|
||||
pub const BoundedDeviceName = std.BoundedArray(u8, max_device_name_size);
|
||||
pub const max_channel_name_size = count: {
|
||||
var count: u32 = 0;
|
||||
count += max_device_name_size;
|
||||
count += 1; // '/'
|
||||
count += 2; // 'ai' or 'ao' or 'co' or 'ci'
|
||||
count += 3; // '0' -> '999', I can't imagine this counter being over 1000. What device has over 1000 channels????
|
||||
break :count count;
|
||||
};
|
||||
|
||||
pub const BoundedDeviceName = std.BoundedArray(u8, max_device_name_size + 1); // +1 for null byte
|
||||
const StringArrayListUnmanaged = std.ArrayListUnmanaged([:0]const u8);
|
||||
|
||||
const NIDaq = @This();
|
||||
@ -35,11 +44,8 @@ pub const Task = struct {
|
||||
|
||||
dropped_samples: u32 = 0,
|
||||
|
||||
pub fn clear(self: Task) !void {
|
||||
try checkDAQmxError(
|
||||
c.DAQmxClearTask(self.handle),
|
||||
error.DAQmxClearTask
|
||||
);
|
||||
pub fn clear(self: Task) void {
|
||||
logDAQmxError(c.DAQmxClearTask(self.handle));
|
||||
}
|
||||
|
||||
pub fn name(self: *Task) ![]const u8 {
|
||||
@ -276,17 +282,7 @@ const DeviceBuffers = struct {
|
||||
array_list: StringArrayListUnmanaged,
|
||||
|
||||
fn init(allocator: std.mem.Allocator, capacity: u32) !ChannelNames {
|
||||
const max_channel_name_size = count: {
|
||||
var count: u32 = 0;
|
||||
count += max_device_name_size;
|
||||
count += 1; // '/'
|
||||
count += 2; // 'ai' or 'ao' or 'co' or 'ci'
|
||||
count += std.math.log10_int(capacity) + 1;
|
||||
|
||||
break :count count;
|
||||
};
|
||||
|
||||
const buffer_size = capacity * (max_channel_name_size + 2);
|
||||
const buffer_size = capacity * (max_channel_name_size + 2); // +2 for ', ' separator
|
||||
|
||||
const buffer = try allocator.alloc(u8, buffer_size);
|
||||
errdefer allocator.free(buffer);
|
||||
@ -368,6 +364,7 @@ const DeviceBuffers = struct {
|
||||
}
|
||||
};
|
||||
|
||||
options: Options,
|
||||
device_names_buffer: []u8,
|
||||
device_names: StringArrayListUnmanaged,
|
||||
|
||||
@ -390,6 +387,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !NIDaq {
|
||||
}
|
||||
|
||||
return NIDaq{
|
||||
.options = options,
|
||||
.device_names_buffer = device_names_buffer,
|
||||
.device_names = device_names,
|
||||
.device_buffers = device_buffers
|
||||
@ -432,13 +430,7 @@ pub fn logDAQmxError(error_code: i32) void {
|
||||
|
||||
var msg: [512:0]u8 = .{ 0 } ** 512;
|
||||
if (c.DAQmxGetErrorString(error_code, &msg, msg.len) == 0) {
|
||||
if (error_code < 0) {
|
||||
log.err("DAQmx ({}): {s}", .{error_code, msg});
|
||||
} else if (!std.mem.startsWith(u8, &msg, "Error code could not be found.")) {
|
||||
// Ignore positive error codes if it could not be found.
|
||||
// This commonly happens when trying to preallocate bytes for buffer and it returns a positive number.
|
||||
log.warn("DAQmx ({}): {s}", .{error_code, msg});
|
||||
}
|
||||
} else {
|
||||
log.err("DAQmx ({}): Unknown (Buffer too small for message)", .{error_code});
|
||||
}
|
||||
@ -452,6 +444,14 @@ pub fn checkDAQmxError(error_code: i32, err: anyerror) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checkDAQmxErrorIgnoreWarnings(error_code: i32, err: anyerror) !void {
|
||||
if (error_code > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try checkDAQmxError(error_code, err);
|
||||
}
|
||||
|
||||
fn splitCommaDelimitedList(array_list: *std.ArrayListUnmanaged([:0]const u8), buffer: []u8) !void {
|
||||
const count = std.mem.count(u8, buffer, ",") + 1;
|
||||
if (count > array_list.capacity) {
|
||||
@ -502,6 +502,7 @@ pub fn listDeviceNames(self: *NIDaq) ![]const [:0]const u8 {
|
||||
self.clearAllDeviceBuffers();
|
||||
|
||||
const required_size = c.DAQmxGetSysDevNames(null, 0);
|
||||
try checkDAQmxErrorIgnoreWarnings(required_size, error.DAQmxGetSysDevNames);
|
||||
if (required_size == 0) {
|
||||
return self.device_names.items;
|
||||
}
|
||||
@ -558,7 +559,7 @@ fn listDevicePhysicalChannels(
|
||||
array_list.clearRetainingCapacity();
|
||||
|
||||
const required_size = getPhysicalChannels(device, null, 0);
|
||||
try checkDAQmxError(required_size, error.GetPhysicalChannels);
|
||||
try checkDAQmxErrorIgnoreWarnings(required_size, error.GetPhysicalChannels);
|
||||
if (required_size == 0) {
|
||||
return array_list.items;
|
||||
}
|
||||
@ -708,7 +709,7 @@ fn listDeviceVoltageRanges(
|
||||
voltage_ranges.clearRetainingCapacity();
|
||||
|
||||
const count = getVoltageRanges(device, null, 0);
|
||||
try checkDAQmxError(count, error.GetVoltageRanges);
|
||||
try checkDAQmxErrorIgnoreWarnings(count, error.GetVoltageRanges);
|
||||
if (count == 0) {
|
||||
return voltage_ranges.items;
|
||||
}
|
||||
@ -769,7 +770,7 @@ pub fn listDeviceAIMeasurementTypes(self: NIDaq, device: [:0]const u8) !AIMeasur
|
||||
_ = self;
|
||||
|
||||
const count = c.DAQmxGetDevAISupportedMeasTypes(device, null, 0);
|
||||
try checkDAQmxError(count, error.DAQmxGetDevAISupportedMeasTypes);
|
||||
try checkDAQmxErrorIgnoreWarnings(count, error.DAQmxGetDevAISupportedMeasTypes);
|
||||
assert(count <= result.buffer.len);
|
||||
|
||||
try checkDAQmxError(
|
||||
@ -792,7 +793,7 @@ pub fn listDeviceAOOutputTypes(self: NIDaq, device: [:0]const u8) !AOOutputTypeL
|
||||
_ = self;
|
||||
|
||||
const count = c.DAQmxGetDevAOSupportedOutputTypes(device, null, 0);
|
||||
try checkDAQmxError(count, error.DAQmxGetDevAOSupportedOutputTypes);
|
||||
try checkDAQmxErrorIgnoreWarnings(count, error.DAQmxGetDevAOSupportedOutputTypes);
|
||||
assert(count <= result.buffer.len);
|
||||
|
||||
try checkDAQmxError(
|
||||
@ -821,3 +822,9 @@ pub fn getDeviceProductCategory(self: NIDaq, device: [:0]const u8) !ProductCateg
|
||||
|
||||
return product_category;
|
||||
}
|
||||
|
||||
pub fn getDeviceNameFromChannel(channel_name: []const u8) ?[]const u8 {
|
||||
const slash = std.mem.indexOfScalar(u8, channel_name, '/') orelse return null;
|
||||
|
||||
return channel_name[0..slash];
|
||||
}
|
106
src/platform.zig
106
src/platform.zig
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
const builtin = @import("builtin");
|
||||
const windows_h = @cImport({
|
||||
@cDefine("_WIN32_WINNT", "0x0500");
|
||||
@ -8,16 +9,70 @@ const windows_h = @cImport({
|
||||
const assert = std.debug.assert;
|
||||
const log = std.log.scoped(.platform);
|
||||
|
||||
// Because `windows_h.HWND` has an alignment of 4,
|
||||
// we need to redefined every struct that uses `HWND` if we want to change the alignment of `HWND`.
|
||||
// Ugh... WHYYYYYY
|
||||
const HWND = [*c]align(2) windows_h.struct_HWND__;
|
||||
const OPENFILENAMEW = extern struct {
|
||||
lStructSize: windows_h.DWORD,
|
||||
hwndOwner: HWND,
|
||||
hInstance: windows_h.HINSTANCE,
|
||||
lpstrFilter: windows_h.LPCWSTR,
|
||||
lpstrCustomFilter: windows_h.LPWSTR,
|
||||
nMaxCustFilter: windows_h.DWORD,
|
||||
nFilterIndex: windows_h.DWORD,
|
||||
lpstrFile: windows_h.LPWSTR,
|
||||
nMaxFile: windows_h.DWORD,
|
||||
lpstrFileTitle: windows_h.LPWSTR,
|
||||
nMaxFileTitle: windows_h.DWORD,
|
||||
lpstrInitialDir: windows_h.LPCWSTR,
|
||||
lpstrTitle: windows_h.LPCWSTR,
|
||||
Flags: windows_h.DWORD,
|
||||
nFileOffset: windows_h.WORD,
|
||||
nFileExtension: windows_h.WORD,
|
||||
lpstrDefExt: windows_h.LPCWSTR,
|
||||
lCustData: windows_h.LPARAM,
|
||||
lpfnHook: windows_h.LPOFNHOOKPROC,
|
||||
lpTemplateName: windows_h.LPCWSTR,
|
||||
pvReserved: ?*anyopaque,
|
||||
dwReserved: windows_h.DWORD,
|
||||
FlagsEx: windows_h.DWORD,
|
||||
};
|
||||
extern fn GetOpenFileNameW([*c]OPENFILENAMEW) windows_h.WINBOOL;
|
||||
|
||||
fn printLastWindowsError(function_name: []const u8) void {
|
||||
const err = windows_h.GetLastError();
|
||||
if (err == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var message: [*c]u8 = null;
|
||||
|
||||
// TODO: Use `FormatMessageW`
|
||||
const size = windows_h.FormatMessageA(
|
||||
windows_h.FORMAT_MESSAGE_ALLOCATE_BUFFER | windows_h.FORMAT_MESSAGE_FROM_SYSTEM | windows_h.FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
null,
|
||||
err,
|
||||
windows_h.MAKELANGID(windows_h.LANG_ENGLISH, windows_h.SUBLANG_ENGLISH_US),
|
||||
@ptrCast(&message),
|
||||
0,
|
||||
null
|
||||
);
|
||||
log.err("{s}() failed ({}): {s}", .{ function_name, err, message[0..size] });
|
||||
|
||||
_ = windows_h.LocalFree(message);
|
||||
}
|
||||
|
||||
pub fn toggleConsoleWindow() void {
|
||||
if (builtin.os.tag != .windows) {
|
||||
// TODO: Maybe just toggle outputing or not outputing to terminal on linux?
|
||||
return;
|
||||
}
|
||||
|
||||
var hWnd = windows_h.GetConsoleWindow();
|
||||
if (hWnd == null) {
|
||||
if (windows_h.AllocConsole() == 0) {
|
||||
// TODO: Use windows.FormatMessages
|
||||
log.err("AllocConsole() failed: {}", .{ windows_h.GetLastError() });
|
||||
printLastWindowsError("AllocConsole");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -31,3 +86,50 @@ pub fn toggleConsoleWindow() void {
|
||||
_ = windows_h.ShowWindow(hWnd, windows_h.SW_SHOWNOACTIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Maybe return the file path instead of an opened file handle?
|
||||
// So the user of this function could do something more interesting.
|
||||
pub fn openFilePicker() !std.fs.File {
|
||||
if (builtin.os.tag != .windows) {
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
const hWnd: HWND = @alignCast(@ptrCast(rl.getWindowHandle()));
|
||||
assert(hWnd != null);
|
||||
|
||||
var ofn = std.mem.zeroes(OPENFILENAMEW);
|
||||
var filename_w_buffer = std.mem.zeroes([std.os.windows.PATH_MAX_WIDE]u16);
|
||||
|
||||
// Zig doesn't let you have NULL bytes in the middle of a string literal, so...
|
||||
// I guess you are forced to do this kind of string concatenation to insert those NULL bytes
|
||||
const lpstrFilter = "All" ++ .{ 0 } ++ "*" ++ .{ 0 } ++ "Binary" ++ .{ 0 } ++ "*.bin" ++ .{ 0 };
|
||||
|
||||
ofn.lStructSize = @sizeOf(@TypeOf(ofn));
|
||||
ofn.hwndOwner = hWnd;
|
||||
ofn.lpstrFile = &filename_w_buffer;
|
||||
ofn.nMaxFile = filename_w_buffer.len;
|
||||
ofn.lpstrFilter = std.unicode.utf8ToUtf16LeStringLiteral(lpstrFilter);
|
||||
ofn.nFilterIndex = 2;
|
||||
ofn.Flags = windows_h.OFN_PATHMUSTEXIST | windows_h.OFN_FILEMUSTEXIST | windows_h.OFN_EXPLORER | windows_h.OFN_LONGNAMES;
|
||||
|
||||
if (GetOpenFileNameW(&ofn) != windows_h.TRUE) {
|
||||
const err = windows_h.CommDlgExtendedError();
|
||||
if (err == err) {
|
||||
return error.Canceled;
|
||||
}
|
||||
|
||||
log.err("GetOpenFileNameW() failed, erro code: {}", .{ err });
|
||||
return error.GetOpenFileNameW;
|
||||
}
|
||||
|
||||
const filename_len = std.mem.indexOfScalar(u16, &filename_w_buffer, 0).?;
|
||||
const filename_w = filename_w_buffer[0..filename_len];
|
||||
|
||||
var filename_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
// It should be safe to do "catch unreachable" here because `filename_buffer` will always be big enough.
|
||||
const filename = std.fmt.bufPrint(&filename_buffer, "{s}", .{std.fs.path.fmtWtf16LeAsUtf8Lossy(filename_w)}) catch unreachable;
|
||||
|
||||
// TODO: Use the `openFileAbsoluteW` function.
|
||||
// Could not get it to work, because it always threw OBJECT_PATH_SYNTAX_BAD error
|
||||
return try std.fs.openFileAbsolute(filename, .{ });
|
||||
}
|
110
src/profiler.zig
Normal file
110
src/profiler.zig
Normal file
@ -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;
|
||||
}
|
||||
}
|
213
src/rect-utils.zig
Normal file
213
src/rect-utils.zig
Normal file
@ -0,0 +1,213 @@
|
||||
const rl = @import("raylib");
|
||||
const Rect = rl.Rectangle;
|
||||
|
||||
pub const AlignX = enum { left, center, right };
|
||||
pub const AlignY = enum { top, center, bottom };
|
||||
|
||||
// ----------------- Positioning functions ----------------- //
|
||||
|
||||
pub fn position(rect: rl.Rectangle) rl.Vector2 {
|
||||
return rl.Vector2.init(rect.x, rect.y);
|
||||
}
|
||||
|
||||
pub fn size(rect: rl.Rectangle) rl.Vector2 {
|
||||
return rl.Vector2.init(rect.width, rect.height);
|
||||
}
|
||||
|
||||
pub fn isInside(rect: rl.Rectangle, x: f32, y: f32) bool {
|
||||
return (rect.x <= x and x < rect.x + rect.width) and (rect.y < y and y < rect.y + rect.height);
|
||||
}
|
||||
|
||||
pub fn isInsideVec2(rect: rl.Rectangle, vec2: rl.Vector2) bool {
|
||||
return isInside(rect, vec2.x, vec2.y);
|
||||
}
|
||||
|
||||
pub fn top(rect: rl.Rectangle) f32 {
|
||||
return rect.y;
|
||||
}
|
||||
|
||||
pub fn bottom(rect: rl.Rectangle) f32 {
|
||||
return rect.y + rect.height;
|
||||
}
|
||||
|
||||
pub fn left(rect: rl.Rectangle) f32 {
|
||||
return rect.x;
|
||||
}
|
||||
|
||||
pub fn right(rect: rl.Rectangle) f32 {
|
||||
return rect.x + rect.width;
|
||||
}
|
||||
|
||||
pub fn verticalSplit(rect: rl.Rectangle, left_side_width: f32) [2]rl.Rectangle {
|
||||
var left_side = rect;
|
||||
left_side.width = left_side_width;
|
||||
|
||||
var right_side = rect;
|
||||
right_side.x += left_side_width;
|
||||
right_side.width -= left_side_width;
|
||||
|
||||
return .{
|
||||
left_side,
|
||||
right_side
|
||||
};
|
||||
}
|
||||
|
||||
pub fn horizontalSplit(rect: rl.Rectangle, top_side_height: f32) [2]rl.Rectangle {
|
||||
var top_side = rect;
|
||||
top_side.height = top_side_height;
|
||||
|
||||
var bottom_side = rect;
|
||||
bottom_side.y += top_side_height;
|
||||
bottom_side.height -= top_side_height;
|
||||
|
||||
return .{
|
||||
top_side,
|
||||
bottom_side
|
||||
};
|
||||
}
|
||||
|
||||
pub fn center(rect: Rect) rl.Vector2 {
|
||||
return rl.Vector2{
|
||||
.x = rect.x + rect.width / 2,
|
||||
.y = rect.y + rect.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn bottomLeft(rect: Rect) rl.Vector2 {
|
||||
return rl.Vector2.init(left(rect), bottom(rect));
|
||||
}
|
||||
|
||||
pub fn bottomRight(rect: Rect) rl.Vector2 {
|
||||
return rl.Vector2.init(right(rect), bottom(rect));
|
||||
}
|
||||
|
||||
pub fn topLeft(rect: Rect) rl.Vector2 {
|
||||
return rl.Vector2.init(left(rect), top(rect));
|
||||
}
|
||||
|
||||
pub fn topRight(rect: Rect) rl.Vector2 {
|
||||
return rl.Vector2.init(right(rect), top(rect));
|
||||
}
|
||||
|
||||
// ----------------- Shrinking/Growing functions ----------------- //
|
||||
|
||||
pub fn shrink(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return Rect.init(
|
||||
rect.x + amount,
|
||||
rect.y + amount,
|
||||
rect.width - 2 * amount,
|
||||
rect.height - 2 * amount
|
||||
);
|
||||
}
|
||||
pub fn grow(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return shrink(rect, -amount);
|
||||
}
|
||||
|
||||
pub fn shrinkY(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return Rect.init(
|
||||
rect.x,
|
||||
rect.y + amount,
|
||||
rect.width,
|
||||
rect.height - 2 * amount
|
||||
);
|
||||
}
|
||||
pub fn growY(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return shrinkY(rect, -amount);
|
||||
}
|
||||
|
||||
pub fn shrinkX(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return Rect.init(
|
||||
rect.x + amount,
|
||||
rect.y,
|
||||
rect.width - 2 * amount,
|
||||
rect.height
|
||||
);
|
||||
}
|
||||
pub fn growX(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return shrinkX(rect, -amount);
|
||||
}
|
||||
|
||||
pub fn shrinkTop(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return Rect.init(
|
||||
rect.x,
|
||||
rect.y + amount,
|
||||
rect.width,
|
||||
rect.height - amount
|
||||
);
|
||||
}
|
||||
pub fn growTop(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return shrinkTop(rect, -amount);
|
||||
}
|
||||
|
||||
pub fn shrinkBottom(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return Rect.init(
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height - amount
|
||||
);
|
||||
}
|
||||
pub fn growBottom(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return shrinkBottom(rect, -amount);
|
||||
}
|
||||
|
||||
pub fn shrinkLeft(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return Rect.init(
|
||||
rect.x + amount,
|
||||
rect.y,
|
||||
rect.width - amount,
|
||||
rect.height
|
||||
);
|
||||
}
|
||||
pub fn growLeft(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return shrinkLeft(rect, -amount);
|
||||
}
|
||||
|
||||
pub fn shrinkRight(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return Rect.init(
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width - amount,
|
||||
rect.height
|
||||
);
|
||||
}
|
||||
pub fn growRight(rect: Rect, amount: f32) rl.Rectangle {
|
||||
return shrinkRight(rect, -amount);
|
||||
}
|
||||
|
||||
// ----------------- Other functions (idk) ----------------- //
|
||||
|
||||
pub fn initCentered(rect: Rect, width: f32, height: f32) Rect {
|
||||
const unused_width = rect.width - width;
|
||||
const unused_height = rect.height - height;
|
||||
return Rect.init(rect.x + unused_width / 2, rect.y + unused_height / 2, width, height);
|
||||
}
|
||||
|
||||
pub fn aligned(rect: Rect, align_x: AlignX, align_y: AlignY) rl.Vector2 {
|
||||
const x = switch(align_x) {
|
||||
.left => rect.x,
|
||||
.center => rect.x + rect.width/2,
|
||||
.right => rect.x + rect.width,
|
||||
};
|
||||
|
||||
const y = switch(align_y) {
|
||||
.top => rect.y,
|
||||
.center => rect.y + rect.height/2,
|
||||
.bottom => rect.y + rect.height,
|
||||
};
|
||||
|
||||
return rl.Vector2.init(x, y);
|
||||
}
|
||||
|
||||
pub fn intersect(rect: Rect, other: Rect) Rect {
|
||||
const left_pos = @max(left(rect), left(other));
|
||||
const top_pos = @max(top(rect), top(other));
|
||||
const right_pos = @min(right(rect), right(other));
|
||||
const bottom_pos = @min(bottom(rect), bottom(other));
|
||||
return Rect{
|
||||
.x = left_pos,
|
||||
.y = top_pos,
|
||||
.width = right_pos - left_pos,
|
||||
.height = bottom_pos - top_pos
|
||||
};
|
||||
}
|
40
src/srcery.zig
Normal file
40
src/srcery.zig
Normal file
@ -0,0 +1,40 @@
|
||||
const rl = @import("raylib");
|
||||
const rgb = @import("./utils.zig").rgb;
|
||||
|
||||
// Primary
|
||||
pub const black = rgb(28 , 27 , 25 );
|
||||
pub const red = rgb(239, 47 , 39 );
|
||||
pub const green = rgb(81 , 159, 80 );
|
||||
pub const yellow = rgb(251, 184, 41 );
|
||||
pub const blue = rgb(44 , 120, 191);
|
||||
pub const magenta = rgb(224, 44 , 109);
|
||||
pub const cyan = rgb(10 , 174, 179);
|
||||
pub const white = rgb(186, 166, 127);
|
||||
pub const bright_black = rgb(145, 129, 117);
|
||||
pub const bright_red = rgb(247, 83 , 65 );
|
||||
pub const bright_green = rgb(152, 188, 55 );
|
||||
pub const bright_yellow = rgb(254, 208, 110);
|
||||
pub const bright_blue = rgb(104, 168, 228);
|
||||
pub const bright_magenta = rgb(255, 92 , 143);
|
||||
pub const bright_cyan = rgb(43 , 228, 208);
|
||||
pub const bright_white = rgb(252, 232, 195);
|
||||
|
||||
// Secondary
|
||||
pub const orange = rgb(255, 95, 0);
|
||||
pub const bright_orange = rgb(255, 135, 0);
|
||||
pub const hard_black = rgb(18, 18, 18);
|
||||
pub const teal = rgb(0, 128, 128);
|
||||
|
||||
// Grays
|
||||
pub const xgray1 = rgb(38 , 38 , 38 );
|
||||
pub const xgray2 = rgb(48 , 48 , 48 );
|
||||
pub const xgray3 = rgb(58 , 58 , 58 );
|
||||
pub const xgray4 = rgb(68 , 68 , 68 );
|
||||
pub const xgray5 = rgb(78 , 78 , 78 );
|
||||
pub const xgray6 = rgb(88 , 88 , 88 );
|
||||
pub const xgray7 = rgb(98 , 98 , 98 );
|
||||
pub const xgray8 = rgb(108, 108, 108);
|
||||
pub const xgray9 = rgb(118, 118, 118);
|
||||
pub const xgray10 = rgb(128, 128, 128);
|
||||
pub const xgray11 = rgb(138, 138, 138);
|
||||
pub const xgray12 = rgb(148, 148, 148);
|
@ -1,351 +1,172 @@
|
||||
const std = @import("std");
|
||||
const NIDaq = @import("./ni-daq.zig");
|
||||
|
||||
const TaskPool = @This();
|
||||
|
||||
const assert = std.debug.assert;
|
||||
const log = std.log.scoped(.task_pool);
|
||||
|
||||
const ChannelType = enum { analog_input, analog_output };
|
||||
const TaskPool = @This();
|
||||
const max_tasks = 32;
|
||||
|
||||
const Entry = struct {
|
||||
device: NIDaq.BoundedDeviceName,
|
||||
channel_type: ChannelType,
|
||||
task: NIDaq.Task,
|
||||
channel_order: std.ArrayListUnmanaged(usize)
|
||||
};
|
||||
|
||||
channel_count: usize = 0,
|
||||
max_channel_count: usize,
|
||||
entries: std.ArrayListUnmanaged(Entry),
|
||||
|
||||
read_thread: ?std.Thread = null,
|
||||
thread_running: bool = false,
|
||||
sampling: ?union(enum) {
|
||||
pub const Sampling = union(enum) {
|
||||
finite: struct {
|
||||
sample_rate: f64,
|
||||
samples_per_channel: u64
|
||||
sample_count: u64
|
||||
},
|
||||
continous: struct {
|
||||
sample_rate: f64
|
||||
}
|
||||
} = null,
|
||||
};
|
||||
|
||||
pub const ChannelSamples = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
mutex: std.Thread.Mutex = .{},
|
||||
read_arrays_by_task: [][]f64,
|
||||
samples: []std.ArrayList(f64),
|
||||
started_sampling_ns: ?i128 = null,
|
||||
pub const Entry = struct {
|
||||
task: NIDaq.Task,
|
||||
in_use: bool = false,
|
||||
running: bool = false,
|
||||
started_sampling_ns: i128,
|
||||
stopped_sampling_ns: ?i128 = null,
|
||||
dropped_samples: u32 = 0,
|
||||
|
||||
pub fn deinit(self: *ChannelSamples) void {
|
||||
for (self.read_arrays_by_task) |read_arrays| {
|
||||
self.allocator.free(read_arrays);
|
||||
sampling: Sampling,
|
||||
|
||||
mutex: *std.Thread.Mutex,
|
||||
samples: *std.ArrayList(f64),
|
||||
|
||||
pub fn stop(self: *Entry) !void {
|
||||
self.running = false;
|
||||
if (self.in_use) {
|
||||
try self.task.stop();
|
||||
self.task.clear();
|
||||
}
|
||||
self.allocator.free(self.read_arrays_by_task);
|
||||
|
||||
for (self.samples) |samples_per_channel| {
|
||||
samples_per_channel.deinit();
|
||||
}
|
||||
self.allocator.free(self.samples);
|
||||
|
||||
self.allocator.destroy(self);
|
||||
self.in_use = false;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Options = struct {
|
||||
max_tasks: usize,
|
||||
max_channels: usize
|
||||
};
|
||||
running: bool = false,
|
||||
read_thread: std.Thread,
|
||||
ni_daq: *NIDaq,
|
||||
entries: [max_tasks]Entry = undefined,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, options: Options) !TaskPool {
|
||||
var entries = try std.ArrayListUnmanaged(Entry).initCapacity(allocator, options.max_tasks);
|
||||
errdefer entries.deinit(allocator);
|
||||
|
||||
for (entries.allocatedSlice()) |*entry| {
|
||||
// TODO: .deinit() on failure
|
||||
entry.channel_order = try std.ArrayListUnmanaged(usize).initCapacity(allocator, options.max_channels);
|
||||
}
|
||||
|
||||
return TaskPool{
|
||||
.entries = entries,
|
||||
.max_channel_count = options.max_channels
|
||||
pub fn init(self: *TaskPool, allocator: std.mem.Allocator, ni_daq: *NIDaq) !void {
|
||||
self.* = TaskPool{
|
||||
.ni_daq = ni_daq,
|
||||
.read_thread = undefined
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TaskPool, allocator: std.mem.Allocator) void {
|
||||
if (self.read_thread != null) {
|
||||
self.stop() catch @panic("Failed to stop task");
|
||||
}
|
||||
|
||||
for (self.entries.items) |e| {
|
||||
e.task.clear() catch @panic("Failed to clear task");
|
||||
}
|
||||
for (self.entries.allocatedSlice()) |*e| {
|
||||
e.channel_order.deinit(allocator);
|
||||
}
|
||||
self.entries.deinit(allocator);
|
||||
}
|
||||
|
||||
pub fn setContinousSampleRate(self: *TaskPool, sample_rate: f64) !void {
|
||||
assert(self.read_thread == null);
|
||||
|
||||
for (self.entries.items) |e| {
|
||||
try e.task.setContinousSampleRate(sample_rate);
|
||||
}
|
||||
|
||||
self.sampling = .{
|
||||
.continous = .{
|
||||
.sample_rate = sample_rate
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setFiniteSampleRate(self: *TaskPool, sample_rate: f64, samples_per_channel: u64) !void {
|
||||
assert(self.read_thread == null);
|
||||
|
||||
for (self.entries.items) |e| {
|
||||
try e.task.setFiniteSampleRate(sample_rate, samples_per_channel);
|
||||
}
|
||||
|
||||
self.sampling = .{
|
||||
.finite = .{
|
||||
.sample_rate = sample_rate,
|
||||
.samples_per_channel = samples_per_channel
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn start(self: *TaskPool, read_timeout: f64, allocator: std.mem.Allocator) !*ChannelSamples {
|
||||
assert(self.read_thread == null);
|
||||
|
||||
var channel_samples = try self.createChannelSamples(allocator);
|
||||
errdefer channel_samples.deinit();
|
||||
|
||||
for (self.entries.items) |e| {
|
||||
try e.task.start();
|
||||
}
|
||||
|
||||
self.thread_running = true;
|
||||
var read_thread = try std.Thread.spawn(
|
||||
self.running = true;
|
||||
self.read_thread = try std.Thread.spawn(
|
||||
.{ .allocator = allocator },
|
||||
readThreadCallback,
|
||||
.{ self, read_timeout, channel_samples }
|
||||
.{ self }
|
||||
);
|
||||
errdefer read_thread.join();
|
||||
|
||||
self.read_thread = read_thread;
|
||||
|
||||
return channel_samples;
|
||||
for (&self.entries) |*entry| {
|
||||
entry.in_use = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(self: *TaskPool) !void {
|
||||
assert(self.read_thread != null);
|
||||
|
||||
for (self.entries.items) |e| {
|
||||
try e.task.stop();
|
||||
pub fn deinit(self: *TaskPool) void {
|
||||
for (&self.entries) |*entry| {
|
||||
entry.stop() catch log.err("Failed to stop entry", .{});
|
||||
}
|
||||
|
||||
self.thread_running = false;
|
||||
self.read_thread.?.join();
|
||||
self.read_thread = null;
|
||||
self.running = false;
|
||||
self.read_thread.join();
|
||||
}
|
||||
|
||||
fn getDeviceFromChannel(channel: [:0]const u8) ?[]const u8 {
|
||||
const slash = std.mem.indexOfScalar(u8, channel, '/') orelse return null;
|
||||
return channel[0..slash];
|
||||
}
|
||||
fn readAnalog(entry: *Entry, timeout: f64) !void {
|
||||
if (!entry.in_use) return;
|
||||
if (!entry.running) return;
|
||||
|
||||
fn getOrPutTask(self: *TaskPool, ni_daq: NIDaq, device: []const u8, channel_type: ChannelType) !*Entry {
|
||||
for (self.entries.items) |*entry| {
|
||||
const entry_device = entry.device.slice();
|
||||
if (entry.channel_type == channel_type and std.mem.eql(u8, entry_device, device)) {
|
||||
return entry;
|
||||
entry.mutex.lock();
|
||||
defer entry.mutex.unlock();
|
||||
|
||||
switch (entry.sampling) {
|
||||
.finite => |args| {
|
||||
try entry.samples.ensureTotalCapacity(args.sample_count);
|
||||
},
|
||||
.continous => |args| {
|
||||
try entry.samples.ensureUnusedCapacity(@intFromFloat(@ceil(args.sample_rate)));
|
||||
}
|
||||
}
|
||||
|
||||
if (self.entries.items.len == self.entries.capacity) {
|
||||
return error.TaskLimitReached;
|
||||
}
|
||||
const unused_capacity = entry.samples.unusedCapacitySlice();
|
||||
if (unused_capacity.len == 0) return;
|
||||
|
||||
const task = try ni_daq.createTask(null);
|
||||
errdefer task.clear() catch {};
|
||||
|
||||
var entry = self.entries.addOneAssumeCapacity();
|
||||
entry.channel_type = channel_type;
|
||||
entry.device = try NIDaq.BoundedDeviceName.fromSlice(device);
|
||||
entry.task = task;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
pub fn createAIVoltageChannel(self: *TaskPool, ni_daq: NIDaq, options: NIDaq.Task.AIVoltageChannelOptions) !void {
|
||||
assert(self.read_thread == null);
|
||||
|
||||
const device = getDeviceFromChannel(options.channel) orelse return error.UnknownDevice;
|
||||
|
||||
var entry = try self.getOrPutTask(ni_daq, device, .analog_input);
|
||||
if (entry.channel_order.items.len == entry.channel_order.capacity) {
|
||||
return error.MaxChannelsLimitReached;
|
||||
}
|
||||
|
||||
try entry.task.createAIVoltageChannel(options);
|
||||
entry.channel_order.appendAssumeCapacity(self.channel_count);
|
||||
self.channel_count += 1;
|
||||
}
|
||||
|
||||
pub fn createAOVoltageChannel(self: *TaskPool, ni_daq: NIDaq, options: NIDaq.Task.AOVoltageChannelOptions) !void {
|
||||
assert(self.read_thread == null);
|
||||
|
||||
const device = getDeviceFromChannel(options.channel) orelse return error.UnknownDevice;
|
||||
|
||||
var entry = try self.getOrPutTask(ni_daq, device, .analog_output);
|
||||
if (entry.channel_order.items.len == entry.channel_order.capacity) {
|
||||
return error.MaxChannelsLimitReached;
|
||||
}
|
||||
|
||||
try entry.task.createAOVoltageChannel(options);
|
||||
entry.channel_order.appendAssumeCapacity(self.channel_count);
|
||||
self.channel_count += 1;
|
||||
}
|
||||
|
||||
pub fn createChannelSamples(self: TaskPool, allocator: std.mem.Allocator) !*ChannelSamples {
|
||||
assert(self.channel_count > 0);
|
||||
assert(self.sampling != null);
|
||||
|
||||
var read_arrays_by_task = try allocator.alloc([]f64, self.entries.items.len);
|
||||
errdefer allocator.free(read_arrays_by_task);
|
||||
|
||||
const sampling = self.sampling.?;
|
||||
const array_size_per_channel: usize = switch (sampling) {
|
||||
// TODO: For now reserve 1s worth of samples per channel, maybe this should be configurable?
|
||||
// Maybe it should be proportional to timeout?
|
||||
.continous => |args| @intFromFloat(@ceil(args.sample_rate)),
|
||||
.finite => |args| args.samples_per_channel,
|
||||
};
|
||||
|
||||
for (0.., self.entries.items) |i, entry| {
|
||||
const channel_count = entry.channel_order.items.len;
|
||||
// TODO: Add allocator.free on failure
|
||||
read_arrays_by_task[i] = try allocator.alloc(f64, array_size_per_channel * channel_count);
|
||||
}
|
||||
|
||||
const samples = try allocator.alloc(std.ArrayList(f64), self.channel_count);
|
||||
errdefer allocator.free(samples);
|
||||
|
||||
if (sampling == .finite) {
|
||||
for (samples) |*samples_per_channel| {
|
||||
// TODO: Add .deinit() on failure
|
||||
samples_per_channel.* = try std.ArrayList(f64).initCapacity(allocator, sampling.finite.samples_per_channel);
|
||||
}
|
||||
} else {
|
||||
for (samples) |*samples_per_channel| {
|
||||
// TODO: Maybe it would be good to reserve a large amount of space for samples?
|
||||
// Even if it is continous. Maybe use ringbuffer?
|
||||
|
||||
// TODO: Add .deinit() on failure
|
||||
samples_per_channel.* = std.ArrayList(f64).init(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
const channel_samples = try allocator.create(ChannelSamples);
|
||||
errdefer allocator.destroy(channel_samples);
|
||||
|
||||
channel_samples.* = ChannelSamples{
|
||||
.allocator = allocator,
|
||||
.read_arrays_by_task = read_arrays_by_task,
|
||||
.samples = samples
|
||||
};
|
||||
|
||||
return channel_samples;
|
||||
}
|
||||
|
||||
pub fn readAnalog(self: *TaskPool, timeout: f64, samples: *ChannelSamples) !void {
|
||||
assert(self.read_thread != null);
|
||||
assert(self.channel_count > 0);
|
||||
|
||||
samples.mutex.lock();
|
||||
defer samples.mutex.unlock();
|
||||
|
||||
for (0.., self.entries.items) |i, *entry| {
|
||||
const read_array = samples.read_arrays_by_task[i];
|
||||
const samples_per_channel = try entry.task.readAnalog(.{
|
||||
.read_array = read_array,
|
||||
.timeout = timeout
|
||||
const read_amount = try entry.task.readAnalog(.{
|
||||
.timeout = timeout,
|
||||
.read_array = unused_capacity,
|
||||
});
|
||||
|
||||
if (samples_per_channel == 0) continue;
|
||||
if (read_amount == 0) return;
|
||||
|
||||
const channel_count = entry.channel_order.items.len;
|
||||
const read_array_used = samples_per_channel * channel_count;
|
||||
|
||||
var channel_index_of_task: usize = 0;
|
||||
var samples_window = std.mem.window(f64, read_array[0..read_array_used], samples_per_channel, samples_per_channel);
|
||||
while (samples_window.next()) |channel_samples| : (channel_index_of_task += 1) {
|
||||
const channel_index: usize = entry.channel_order.items[channel_index_of_task];
|
||||
|
||||
// TODO: Maybe use .appendSliceAssumeCapacity(), when doing finite sampling?
|
||||
try samples.samples[channel_index].appendSlice(channel_samples);
|
||||
}
|
||||
}
|
||||
entry.samples.items.len += read_amount;
|
||||
}
|
||||
|
||||
pub fn isDone(self: TaskPool) !bool {
|
||||
for (self.entries.items) |entry| {
|
||||
const is_done = try entry.task.isDone();
|
||||
if (!is_done) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
fn readThreadCallback(task_pool: *TaskPool) void {
|
||||
const timeout = 0.05;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn droppedSamples(self: TaskPool) u32 {
|
||||
var sum: u32 = 0;
|
||||
for (self.entries.items) |entry| {
|
||||
sum += entry.task.dropped_samples;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
fn readThreadCallback(task_pool: *TaskPool, timeout: f64, channel_samples: *ChannelSamples) void {
|
||||
defer task_pool.thread_running = false;
|
||||
|
||||
channel_samples.started_sampling_ns = std.time.nanoTimestamp();
|
||||
defer channel_samples.stopped_sampling_ns = std.time.nanoTimestamp();
|
||||
|
||||
var error_count: u32 = 0;
|
||||
const max_error_count = 3;
|
||||
while (error_count < max_error_count and task_pool.thread_running) {
|
||||
const is_done = task_pool.isDone() catch |e| {
|
||||
error_count += 1;
|
||||
|
||||
log.err(".isDone() failed in thread: {}", .{e});
|
||||
while (task_pool.running) {
|
||||
for (&task_pool.entries) |*entry| {
|
||||
readAnalog(entry, timeout) catch |e| {
|
||||
log.err("readAnalog() failed in thread: {}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
|
||||
continue;
|
||||
};
|
||||
if (is_done) {
|
||||
break;
|
||||
}
|
||||
|
||||
task_pool.readAnalog(timeout, channel_samples) catch |e| {
|
||||
error_count += 1;
|
||||
|
||||
log.err(".readAnalog() failed in thread: {}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
|
||||
continue;
|
||||
entry.stop() catch log.err("failed to stop collecting", .{});
|
||||
};
|
||||
}
|
||||
|
||||
if (max_error_count == error_count) {
|
||||
log.err("Stopping read thread, too many errors occured", .{});
|
||||
std.time.sleep(0.05 * std.time.ns_per_s);
|
||||
}
|
||||
}
|
||||
|
||||
fn findFreeEntry(self: *TaskPool) ?*Entry {
|
||||
for (&self.entries) |*entry| {
|
||||
if (!entry.in_use) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn launchAIVoltageChannel(
|
||||
self: *TaskPool,
|
||||
mutex: *std.Thread.Mutex,
|
||||
samples: *std.ArrayList(f64),
|
||||
sampling: Sampling,
|
||||
options: NIDaq.Task.AIVoltageChannelOptions
|
||||
) !*Entry {
|
||||
const task = try self.ni_daq.createTask(null);
|
||||
errdefer task.clear();
|
||||
|
||||
const entry = self.findFreeEntry() orelse return error.NotEnoughSpace;
|
||||
errdefer entry.in_use = false;
|
||||
|
||||
try task.createAIVoltageChannel(options);
|
||||
switch (sampling) {
|
||||
.continous => |args| {
|
||||
try task.setContinousSampleRate(args.sample_rate);
|
||||
},
|
||||
.finite => |args| {
|
||||
try task.setFiniteSampleRate(args.sample_rate, args.sample_count);
|
||||
}
|
||||
}
|
||||
|
||||
samples.clearRetainingCapacity();
|
||||
|
||||
try task.start();
|
||||
const started_at = std.time.nanoTimestamp();
|
||||
|
||||
entry.* = Entry{
|
||||
.task = task,
|
||||
.started_sampling_ns = started_at,
|
||||
.in_use = true,
|
||||
.running = true,
|
||||
.mutex = mutex,
|
||||
.samples = samples,
|
||||
.sampling = sampling,
|
||||
};
|
||||
|
||||
return entry;
|
||||
}
|
1280
src/ui.zig
Normal file
1280
src/ui.zig
Normal file
File diff suppressed because it is too large
Load Diff
31
src/utils.zig
Normal file
31
src/utils.zig
Normal file
@ -0,0 +1,31 @@
|
||||
const std = @import("std");
|
||||
const rl = @import("raylib");
|
||||
|
||||
pub fn vec2Round(vec2: rl.Vector2) rl.Vector2 {
|
||||
return rl.Vector2{
|
||||
.x = @round(vec2.x),
|
||||
.y = @round(vec2.y),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn rgb(r: u8, g: u8, b: u8) rl.Color {
|
||||
return rl.Color.init(r, g, b, 255);
|
||||
}
|
||||
|
||||
pub fn rgba(r: u8, g: u8, b: u8, a: f32) rl.Color {
|
||||
return rl.Color.init(r, g, b, a * 255);
|
||||
}
|
||||
|
||||
pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void {
|
||||
rl.drawRectangleRec(rl.Rectangle{
|
||||
.x = rect.x,
|
||||
.y = rect.y + rect.height - size,
|
||||
.width = rect.width,
|
||||
.height = size
|
||||
}, color);
|
||||
}
|
||||
|
||||
pub fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, value: T) T {
|
||||
const t = (value - from_min) / (from_max - from_min);
|
||||
return std.math.lerp(to_min, to_max, t);
|
||||
}
|
Loading…
Reference in New Issue
Block a user