diff --git a/src/app.zig b/src/app.zig index 496ca8c..e2ac964 100644 --- a/src/app.zig +++ b/src/app.zig @@ -166,6 +166,23 @@ fn GenerationalArray(Item: type) type { }; } +pub const SampleList = struct { + samples: std.ArrayListUnmanaged(f64) = .{}, + graph_min_max_cache: Graph.MinMaxCache = .{}, + min_sample: ?f64 = null, + max_sample: ?f64 = null, + + pub fn clear(self: *SampleList, allocator: Allocator) void { + self.deinit(allocator); + self.* = .{}; + } + + pub fn deinit(self: *SampleList, allocator: Allocator) void { + self.samples.clearAndFree(allocator); + self.graph_min_max_cache.deinit(allocator); + } +}; + pub const Channel = struct { const Name = std.BoundedArray(u8, NIDaq.max_channel_name_size + 1); const Device = std.BoundedArray(u8, NIDaq.max_device_name_size + 1); @@ -176,15 +193,12 @@ pub const Channel = struct { // Runtime device: Device = .{}, + collected_samples_id: Id, allowed_sample_values: ?RangeF64 = null, allowed_sample_rates: ?RangeF64 = null, - // TODO: Use a linked list, so the whole list wouldn't need to be reallocated when appending collected samples - collected_samples: std.ArrayListUnmanaged(f64) = .{}, write_pattern: std.ArrayListUnmanaged(f64) = .{}, output_task: ?NIDaq.Task = null, - graph_min_max_cache: Graph.MinMaxCache = .{}, last_sample_save_at: ?i128 = null, - processed_samples_up_to: usize = 0, pub fn deinit(self: *Channel, allocator: Allocator) void { self.clear(allocator); @@ -200,14 +214,11 @@ pub const Channel = struct { self.allowed_sample_values = null; self.last_sample_save_at = null; - self.collected_samples.clearAndFree(allocator); self.write_pattern.clearAndFree(allocator); if (self.output_task) |task| { task.clear(); self.output_task = null; } - - self.graph_min_max_cache.deinit(allocator); } pub fn generateSine( @@ -247,10 +258,6 @@ pub const Channel = struct { pub fn invalidateSavedSamples(self: *Channel) void { self.last_sample_save_at = null; } - - fn hasNewCollectedSamples(self: *Channel) bool { - return self.processed_samples_up_to != self.collected_samples.items.len; - } }; pub const File = struct { @@ -258,24 +265,16 @@ pub const File = struct { path: []u8, // Runtime - samples: ?[]f64 = null, - graph_min_max_cache: Graph.MinMaxCache = .{}, - min_sample: f64 = 0, - max_sample: f64 = 0, + samples_id: Id, pub fn deinit(self: *File, allocator: Allocator) void { self.clear(allocator); - self.graph_min_max_cache.deinit(allocator); allocator.free(self.path); } pub fn clear(self: *File, allocator: Allocator) void { - if (self.samples) |samples| { - allocator.free(samples); - self.samples = null; - } - self.min_sample = 0; - self.max_sample = 0; + _ = self; + _ = allocator; } }; @@ -373,12 +372,14 @@ pub const View = struct { graph_opts: Graph.ViewOptions = .{}, sync_controls: bool = false, marked_ranges: std.BoundedArray(MarkedRange, 32) = .{}, + sliding_window: ?f64 = null, // Runtime graph_cache: Graph.RenderCache = .{}, available_x_range: RangeF64 = RangeF64.init(0, 0), available_y_range: RangeF64 = RangeF64.init(0, 0), unit: ?NIDaq.Unit = .Voltage, + transformed_samples: ?[]f64 = null, pub fn clear(self: *View) void { self.graph_cache.clear(); @@ -431,6 +432,7 @@ pub const Project = struct { // TODO: How this to computer local settings, like appdata. Because this option shouldn't be project specific. show_rulers: bool = true, + sample_lists: GenerationalArray(SampleList) = .{}, channels: GenerationalArray(Channel) = .{}, files: GenerationalArray(File) = .{}, views: GenerationalArray(View) = .{}, @@ -467,23 +469,88 @@ pub const Project = struct { return result_range; } + pub fn getViewSampleList(self: *Project, view_id: Id) Id { + const view = self.views.get(view_id).?; + + switch (view.reference) { + .channel => |channel_id| { + const channel = self.channels.get(channel_id).?; + return channel.collected_samples_id; + }, + .file => |file_id| { + const file = self.files.get(file_id).?; + return file.samples_id; + } + } + } + pub fn getViewSamples(self: *Project, view_id: Id) []const f64 { - const empty = &[0]f64{}; + const samples_list_id = self.getViewSampleList(view_id); + const samples_list = self.sample_lists.get(samples_list_id).?; - var result: []const f64 = empty; + return samples_list.samples.items; + } - if (self.views.get(view_id)) |view| { - switch (view.reference) { - .channel => |channel_id| if (self.channels.get(channel_id)) |channel| { - result = channel.collected_samples.items; - }, - .file => |file_id| if (self.files.get(file_id)) |file| { - result = file.samples orelse empty; + pub fn appendSamples(self: *Project, allocator: Allocator, sample_list_id: Id, samples: []const f64) !void { + if (samples.len == 0) return; + + const sample_list = self.sample_lists.get(sample_list_id).?; + + const affected_range = RangeF64.init( + @floatFromInt(sample_list.samples.items.len), + @floatFromInt(sample_list.samples.items.len + samples.len) + ); + + try sample_list.samples.appendSlice(allocator, samples); + + var min_sample = sample_list.min_sample orelse samples[0]; + var max_sample = sample_list.max_sample orelse samples[0]; + for (samples) |sample| { + min_sample = @min(min_sample, sample); + max_sample = @max(max_sample, sample); + } + sample_list.min_sample = min_sample; + sample_list.max_sample = max_sample; + + try sample_list.graph_min_max_cache.updateLast(allocator, sample_list.samples.items); + + self.refreshMarkedRanges(sample_list_id, affected_range); + } + + pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) void { + const sample_list = self.sample_lists.get(sample_list_id).?; + + const affected_range = RangeF64.init( + 0, + @floatFromInt(sample_list.samples.items.len) + ); + + sample_list.clear(allocator); + + self.refreshMarkedRanges(sample_list_id, affected_range); + } + + fn refreshMarkedRanges(self: *Project, sample_list_id: Id, affected_range: RangeF64) void { + const sample_list = self.sample_lists.get(sample_list_id).?; + const total_samples = sample_list.samples.items; + + var view_iter = self.views.idIterator(); + while (view_iter.next()) |view_id| { + const view_sample_list_id = self.getViewSampleList(view_id); + if (!view_sample_list_id.eql(sample_list_id)) { + continue; + } + + const view = self.views.get(view_id).?; + const marked_ranges: []View.MarkedRange = view.marked_ranges.slice(); + for (marked_ranges) |*marked_range| { + if (marked_range.axis != .X) continue; + + if (affected_range.intersectPositive(marked_range.range).isPositive()) { + marked_range.refresh(total_samples); } } } - - return result; } pub fn deinit(self: *Project, allocator: Allocator) void { @@ -501,6 +568,12 @@ pub const Project = struct { self.views.clear(); + var sample_lists_iter = self.sample_lists.iterator(); + while (sample_lists_iter.next()) |sample_list| { + sample_list.clear(allocator); + } + self.sample_lists.clear(); + if (self.save_location) |str| { allocator.free(str); self.save_location = null; @@ -548,10 +621,14 @@ pub const Project = struct { } errdefer if (saved_collected_samples) |str| allocator.free(str); + const sample_list_id = try self.sample_lists.insert(.{}); + errdefer self.sample_lists.remove(sample_list_id); + const channel = self.channels.get(id).?; channel.* = Channel{ .name = try utils.initBoundedStringZ(Channel.Name, channel_name), - .saved_collected_samples = saved_collected_samples + .saved_collected_samples = saved_collected_samples, + .collected_samples_id = sample_list_id }; } } @@ -566,9 +643,13 @@ pub const Project = struct { const path = try readString(reader, allocator); errdefer allocator.free(path); + const sample_list_id = try self.sample_lists.insert(.{}); + errdefer self.sample_lists.remove(sample_list_id); + const file = self.files.get(id).?; file.* = File{ - .path = path + .path = path, + .samples_id = sample_list_id }; } } @@ -654,7 +735,8 @@ pub const Project = struct { const samples_file = try dir.createFile(path, .{}); defer samples_file.close(); - try writeFileF64(samples_file, channel.collected_samples.items); + const samples = self.sample_lists.get(channel.collected_samples_id).?.samples.items; + try writeFileF64(samples_file, samples); } const stat = try dir.statFile(path); @@ -848,7 +930,7 @@ pub fn init(self: *App, allocator: Allocator) !void { self.ni_daq = try NIDaq.init(allocator, &self.ni_daq_api.?, .{ .max_devices = 4, .max_analog_inputs = 32, - .max_analog_outputs = 8, + .max_analog_outputs = 16, .max_counter_outputs = 8, .max_counter_inputs = 8, .max_analog_input_voltage_ranges = 4, @@ -1012,42 +1094,6 @@ pub fn tick(self: *App) !void { self.collection_samples_mutex.lock(); defer self.collection_samples_mutex.unlock(); - { - var view_iter = self.project.views.idIterator(); - while (view_iter.next()) |id| { - const view = self.getView(id) orelse continue; - if (view.reference != .channel) continue; - - const channel_id = view.reference.channel; - const channel = self.getChannel(channel_id) orelse continue; - if (!channel.hasNewCollectedSamples()) continue; - - const samples = channel.collected_samples.items; - - const new_samples_range = RangeF64.init(@floatFromInt(channel.processed_samples_up_to), @floatFromInt(samples.len)); - const marked_ranges: []View.MarkedRange = view.marked_ranges.slice(); - for (marked_ranges) |*marked_range| { - if (marked_range.axis != .X) continue; - - if (new_samples_range.intersectPositive(marked_range.range).isPositive()) { - marked_range.refresh(samples); - } - } - } - } - - // Update channel min max caches - { - var channel_iter = self.project.channels.iterator(); - while (channel_iter.next()) |channel| { - if (channel.hasNewCollectedSamples()) { - channel.invalidateSavedSamples(); - try channel.graph_min_max_cache.updateLast(self.allocator, channel.collected_samples.items); - channel.processed_samples_up_to = channel.collected_samples.items.len; - } - } - } - { var view_iter = self.project.views.idIterator(); while (view_iter.next()) |id| { @@ -1199,7 +1245,7 @@ fn startCollection(self: *App) !void { for (channels_in_task.constSlice()) |id| { const channel = self.getChannel(id).?; - channel.collected_samples.clearAndFree(self.allocator); + self.project.clearSamples(self.allocator, channel.collected_samples_id); } const read_array = try self.allocator.alloc(f64, @@ -1345,7 +1391,7 @@ pub fn collectionThreadCallback(self: *App) void { const channel_id = collection_task.channels.get(channel_index); const channel = self.getChannel(channel_id) orelse continue; - channel.collected_samples.appendSlice(self.allocator, channel_samples) catch |e| { + self.project.appendSamples(self.allocator, channel.collected_samples_id, channel_samples) catch |e| { log.err("Failed to append samples for channel: {}", .{e}); continue; }; @@ -1374,9 +1420,13 @@ pub fn addFile(self: *App, path: []const u8) !Id { const path_dupe = try self.allocator.dupe(u8, path); errdefer self.allocator.free(path_dupe); + const sample_list_id = try self.project.sample_lists.insert(.{}); + errdefer self.project.sample_lists.remove(sample_list_id); + const file = self.project.files.get(id).?; file.* = File{ - .path = path_dupe + .path = path_dupe, + .samples_id = sample_list_id }; self.loadFile(id) catch |e| { @@ -1423,7 +1473,10 @@ fn writeFileF64(file: std.fs.File, data: []const f64) !void { pub fn loadFile(self: *App, id: Id) !void { const file = self.getFile(id) orelse return; + const sample_list = self.project.sample_lists.get(file.samples_id) orelse return; + file.clear(self.allocator); + sample_list.clear(self.allocator); const cwd = std.fs.cwd(); @@ -1431,18 +1484,9 @@ pub fn loadFile(self: *App, id: Id) !void { defer samples_file.close(); const samples = try readFileF64(self.allocator, samples_file); - file.samples = samples; + defer self.allocator.free(samples); - if (samples.len > 0) { - file.min_sample = samples[0]; - file.max_sample = samples[0]; - for (samples) |sample| { - file.min_sample = @min(file.min_sample, sample); - file.max_sample = @max(file.max_sample, sample); - } - - try file.graph_min_max_cache.updateAll(self.allocator, samples); - } + try self.project.appendSamples(self.allocator, file.samples_id, samples); } // ---------------- Channels --------------------------------- // @@ -1455,9 +1499,13 @@ pub fn addChannel(self: *App, channel_name: []const u8) !Id { const id = try self.project.channels.insertUndefined(); errdefer self.project.channels.remove(id); + const sample_list_id = try self.project.sample_lists.insert(.{}); + errdefer self.project.sample_lists.remove(sample_list_id); + const channel = self.project.channels.get(id).?; channel.* = Channel{ .name = try utils.initBoundedStringZ(Channel.Name, channel_name), + .collected_samples_id = sample_list_id }; if (self.project.save_location) |project_file_path| { @@ -1527,7 +1575,9 @@ fn loadSavedSamples(self: *App, channel_id: Id) !void { defer samples_file.close(); const samples = try readFileF64(self.allocator, samples_file); - channel.collected_samples = std.ArrayListUnmanaged(f64).fromOwnedSlice(samples); + defer self.allocator.free(samples); + + try self.project.appendSamples(self.allocator, channel.collected_samples_id, samples); const stat = try dir.statFile(saved_samples_location); channel.last_sample_save_at = stat.mtime; @@ -1601,36 +1651,29 @@ pub fn getViewSamples(self: *App, id: Id) []const f64 { } pub fn getViewMinMaxCache(self: *App, id: Id) Graph.MinMaxCache { - const view = self.getView(id).?; + const samples_list_id = self.project.getViewSampleList(id); + const sample_list = self.project.sample_lists.get(samples_list_id).?; - switch (view.reference) { - .channel => |channel_id| { - const channel = self.getChannel(channel_id).?; - return channel.graph_min_max_cache; - }, - .file => |file_id| { - const file = self.getFile(file_id).?; - return file.graph_min_max_cache; - } - } + return sample_list.graph_min_max_cache; } fn refreshViewAvailableXYRanges(self: *App, id: Id) void { const view = self.getView(id) orelse return; + const sample_list_id = self.project.getViewSampleList(id); + const sample_list = self.project.sample_lists.get(sample_list_id).?; + + view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.samples.items.len)); switch (view.reference) { .channel => |channel_id| if (self.getChannel(channel_id)) |channel| { const allowed_sample_values = channel.allowed_sample_values orelse RangeF64.init(0, 0); - const samples = channel.collected_samples.items; - view.available_x_range = RangeF64.init(0, @floatFromInt(samples.len)); view.available_y_range = RangeF64.init(allowed_sample_values.upper, allowed_sample_values.lower); }, - .file => |file_id| if (self.getFile(file_id)) |file| { - const samples = file.samples orelse &[_]f64{ }; - - view.available_x_range = RangeF64.init(0, @floatFromInt(samples.len)); - view.available_y_range = RangeF64.init(file.max_sample, file.min_sample); + .file => { + const min_sample = sample_list.min_sample orelse 0; + const max_sample = sample_list.max_sample orelse 1; + view.available_y_range = RangeF64.init(max_sample, min_sample); } } } diff --git a/src/assets/checkbox-mark.ase b/src/assets/checkbox-mark.ase new file mode 100644 index 0000000..7c67f9b Binary files /dev/null and b/src/assets/checkbox-mark.ase differ diff --git a/src/assets/cross.ase b/src/assets/cross.ase new file mode 100644 index 0000000..e899233 Binary files /dev/null and b/src/assets/cross.ase differ diff --git a/src/assets/file.ase b/src/assets/file.ase new file mode 100644 index 0000000..de22715 Binary files /dev/null and b/src/assets/file.ase differ diff --git a/src/components/view.zig b/src/components/view.zig index 8336a6d..fda91d5 100644 --- a/src/components/view.zig +++ b/src/components/view.zig @@ -240,11 +240,20 @@ fn showToolbar(ctx: Context, view_id: Id) void { .size_x = UI.Sizing.initGrowFull() }); + _ = ui.createBox(.{ + .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) + }); + const label = ui.label("{s}", .{text}); label.size.y = UI.Sizing.initGrowFull(); label.alignment.x = .center; label.alignment.y = .center; - label.padding = UI.Padding.horizontal(ui.rem(1)); + // TODO: + // label.padding = UI.Padding.horizontal(ui.rem(1)); + + _ = ui.createBox(.{ + .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) + }); } } diff --git a/src/graph.zig b/src/graph.zig index 266ccc4..0b018f4 100644 --- a/src/graph.zig +++ b/src/graph.zig @@ -191,6 +191,50 @@ pub const RenderCache = struct { } }; +pub const SampleIterator = struct { + samples: []const f64, + to: f64, + step: f64, + i: f64, + next_i: f64, + + pub fn init(samples: []const f64, from: f64, to: f64, step: f64) SampleIterator { + return SampleIterator{ + .samples = samples, + .i = from, + .next_i = from + step, + .to = to, + .step = step + }; + } + + pub fn initRange(samples: []const f64, range: RangeF64, step: f64) SampleIterator { + return SampleIterator.init(samples, range.lower, range.upper, step); + } + + pub fn next(self: *SampleIterator) ?[]const f64 { + if (self.next_i < self.to) { + self.i = self.next_i; + self.next_i = self.i + self.step; + + const i_usize: usize = @intFromFloat(self.i); + const next_i_usize: usize = @intFromFloat(self.next_i); + if (next_i_usize >= self.samples.len) { + return null; + } + + const samples = self.samples[i_usize..next_i_usize]; + if (samples.len == 0) { + return null; + } + + return samples; + } else { + return null; + } + } +}; + fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { rl.beginScissorMode( @intFromFloat(draw_rect.x), @@ -241,20 +285,15 @@ fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples const i_range = options.x_range.intersectPositive( RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0)) ); - if (i_range.lower > i_range.upper) { + if (i_range.isNegative()) { return; } const samples_per_column = options.x_range.size() / draw_x_range.size(); assert(samples_per_column >= 1); - var i = i_range.lower; - while (i < i_range.upper - samples_per_column) : (i += samples_per_column) { - const column_start: usize = @intFromFloat(i); - const column_end: usize = @intFromFloat(i + samples_per_column); - const column_samples = samples[column_start..column_end]; - if (column_samples.len == 0) continue; - + var iter = SampleIterator.init(samples, i_range.lower, i_range.upper, samples_per_column); + while (iter.next()) |column_samples| { var column_min = column_samples[0]; var column_max = column_samples[0]; @@ -263,7 +302,7 @@ fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples column_max = @max(column_max, sample); } - const x = options.x_range.remapTo(draw_x_range, i); + const x = options.x_range.remapTo(draw_x_range, iter.i); const y_min = options.y_range.remapTo(draw_y_range, column_min); const y_max = options.y_range.remapTo(draw_y_range, column_max); @@ -290,7 +329,7 @@ fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, min_max_cach const i_range = options.x_range.intersectPositive( RangeF64.init(0, @max(@as(f64, @floatFromInt(min_max_cache.sample_count)) - 1, 0)) ); - if (i_range.lower > i_range.upper) { + if (i_range.isNegative()) { return; } diff --git a/src/main.zig b/src/main.zig index c062c30..30a8b69 100644 --- a/src/main.zig +++ b/src/main.zig @@ -112,29 +112,29 @@ pub fn main() !void { app.project.save_location = save_location; app.project.sample_rate = 5000; - _ = try app.addView(.{ - .channel = try app.addChannel("Dev1/ai0") - }); + // _ = try app.addView(.{ + // .channel = try app.addChannel("Dev1/ai0") + // }); - _ = try app.addView(.{ - .file = try app.addFile("./samples-5k.bin") - }); + // _ = try app.addView(.{ + // .file = try app.addFile("./samples-5k.bin") + // }); - _ = try app.addView(.{ - .file = try app.addFile("./samples-50k.bin") - }); + // _ = try app.addView(.{ + // .file = try app.addFile("./samples-50k.bin") + // }); - _ = try app.addView(.{ - .file = try app.addFile("./samples-300k.bin") - }); + // _ = try app.addView(.{ + // .file = try app.addFile("./samples-300k.bin") + // }); - _ = try app.addView(.{ - .file = try app.addFile("./samples-9m.bin") - }); + // _ = try app.addView(.{ + // .file = try app.addFile("./samples-9m.bin") + // }); - _ = try app.addView(.{ - .file = try app.addFile("./samples-18m.bin") - }); + // _ = try app.addView(.{ + // .file = try app.addFile("./samples-18m.bin") + // }); } var profiler: ?Profiler = null; diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index 8df46ec..d3e781a 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -36,6 +36,7 @@ sample_rate_input: UI.TextInputStorage, parsed_sample_rate: ?f64 = null, // View settings +sliding_window_input: UI.TextInputStorage, channel_save_file_picker: ?Platform.FilePickerId = null, file_save_file_picker: ?Platform.FilePickerId = null, @@ -47,7 +48,8 @@ pub fn init(app: *App) !MainScreen { .frequency_input = UI.TextInputStorage.init(allocator), .amplitude_input = UI.TextInputStorage.init(allocator), .sample_rate_input = UI.TextInputStorage.init(allocator), - .view_controls = ViewControlsSystem.init(&app.project) + .view_controls = ViewControlsSystem.init(&app.project), + .sliding_window_input = UI.TextInputStorage.init(allocator) }; try self.frequency_input.setText("10"); @@ -317,13 +319,13 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { .label = "Sync controls" }); - var sample_count: ?usize = null; + const samples = project.getViewSamples(view_id); + switch (view.reference) { .channel => |channel_id| { const channel = project.channels.get(channel_id).?; const channel_name = utils.getBoundedStringZ(&channel.name); const channel_type = NIDaq.getChannelType(channel_name); - const samples = channel.collected_samples.items; _ = ui.label("Channel: {s}", .{ channel_name }); @@ -346,16 +348,10 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { channel.saved_collected_samples = path; } - - sample_count = samples.len; }, .file => |file_id| { const file = project.files.get(file_id).?; - if (file.samples) |samples| { - sample_count = samples.len; - } - if (ui.fileInput(.{ .key = ui.keyFromString("Filename"), .allocator = self.app.allocator, @@ -370,18 +366,21 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { } - if (sample_count != null) { - _ = ui.label("Samples: {d}", .{ sample_count.? }); + _ = ui.label("Samples: {d}", .{ samples.len }); - var duration_str: []const u8 = "-"; - if (sample_rate != null) { - const duration = @as(f64, @floatFromInt(sample_count.?)) / sample_rate.?; - if (utils.formatDuration(ui.frameAllocator(), duration)) |str| { - duration_str = str; - } else |_| {} - } - _ = ui.label("Duration: {s}", .{ duration_str }); + var duration_str: []const u8 = "-"; + if (sample_rate != null) { + const duration = @as(f64, @floatFromInt(samples.len)) / sample_rate.?; + if (utils.formatDuration(ui.frameAllocator(), duration)) |str| { + duration_str = str; + } else |_| {} } + _ = ui.label("Duration: {s}", .{ duration_str }); + + _ = try ui.numberInput(f64, .{ + .key = ui.keyFromString("Sliding window"), + .storage = &self.sliding_window_input + }); } fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {