From e48887cdee35646a83e50cb4b1f8dc789762bce1 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Thu, 1 May 2025 19:09:32 +0300 Subject: [PATCH] refactor sample list to store in blocks --- src/app.zig | 599 ++++++++++++++++++++++++++-------- src/components/view.zig | 6 +- src/components/view_ruler.zig | 6 +- src/graph.zig | 444 ++++++++++--------------- src/main.zig | 6 +- src/screens/main_screen.zig | 55 ++-- 6 files changed, 665 insertions(+), 451 deletions(-) diff --git a/src/app.zig b/src/app.zig index e2ac964..c84752c 100644 --- a/src/app.zig +++ b/src/app.zig @@ -167,19 +167,293 @@ 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 const Block = struct { + pub const Id = usize; + pub const capacity = 512; + pub const Len = std.math.IntFittingRange(0, capacity); + const Buffer = [capacity]f64; - pub fn clear(self: *SampleList, allocator: Allocator) void { - self.deinit(allocator); - self.* = .{}; + comptime { + assert(std.math.isPowerOfTwo(capacity)); + } + + buffer: *Buffer, + len: Len = 0, + + min: ?f64 = null, + max: ?f64 = null, + + pub fn samplesSlice(self: *Block) []f64 { + return self.buffer[0..self.len]; + } + + pub fn append(self: *Block, samples: []const f64) usize { + const unused_slice = self.buffer[self.len..]; + + const copy_amount = @min(unused_slice.len, samples.len); + const added_samples = samples[0..copy_amount]; + @memcpy(unused_slice[0..copy_amount], added_samples); + + self.len += @intCast(copy_amount); + assert(self.len <= capacity); + + if (added_samples.len > 0) { + var min = self.min orelse added_samples[0]; + var max = self.max orelse added_samples[0]; + for (added_samples) |sample| { + min = @min(min, sample); + max = @max(max, sample); + } + self.min = min; + self.max = max; + } + + return copy_amount; + } + + pub fn clear(self: *Block) void { + self.* = Block{ + .buffer = self.buffer + }; + } + }; + + const Iterator = struct { + sample_list: *SampleList, + current: usize, + to: usize, + + pub fn next(self: *Iterator) ?[]f64 { + while (self.current < self.to) { + const block = self.sample_list.getBlockAtSample(self.current) orelse return null; + const from_index_in_block = @mod(self.current, Block.capacity); + const to_index_in_block = @min(self.to - self.current + from_index_in_block, block.len); + self.current += Block.capacity - from_index_in_block; + + if (from_index_in_block >= block.len) { + continue; + } + + const segment = block.buffer[from_index_in_block..to_index_in_block]; + if (segment.len == 0) { + continue; + } + + return segment; + } + + return null; + + } + }; + + const BlockIdIterator = struct { + current: Block.Id, + to: Block.Id, + + pub fn next(self: *BlockIdIterator) ?Block.Id { + if (self.current > self.to) { + return null; + } + + defer self.current += 1; + return self.current; + } + }; + + arena: std.heap.ArenaAllocator, + blocks: std.ArrayListUnmanaged(Block) = .{}, + + min: ?f64 = null, + max: ?f64 = null, + + pub fn init(allocator: Allocator) SampleList { + return SampleList{ + .arena = std.heap.ArenaAllocator.init(allocator), + }; } - pub fn deinit(self: *SampleList, allocator: Allocator) void { - self.samples.clearAndFree(allocator); - self.graph_min_max_cache.deinit(allocator); + pub fn deinit(self: *SampleList) void { + const allocator = self.arena.child_allocator; + + self.arena.deinit(); + self.blocks.deinit(allocator); + } + + pub fn clear(self: *SampleList, allocator: Allocator) void { + self.deinit(); + self.* = SampleList.init(allocator); + } + + fn getBlockAtSample(self: *SampleList, sample_index: usize) ?*Block { + return self.getBlock(@divFloor(sample_index, Block.capacity)); + } + + pub fn getBlock(self: *SampleList, block_id: Block.Id) ?*Block { + if (block_id < self.blocks.items.len) { + return &self.blocks.items[block_id]; + } + + return null; + } + + pub fn append(self: *SampleList, samples: []const f64) !void { + if (samples.len == 0) return; + + var appended_count: usize = 0; + while (appended_count < samples.len) { + if (self.blocks.items.len == 0 or self.blocks.getLast().len == Block.capacity) { + const allocator = self.arena.child_allocator; + const buffer_allocator = self.arena.allocator(); + + const block_buffer = try buffer_allocator.create(Block.Buffer); + errdefer buffer_allocator.destroy(block_buffer); + + try self.blocks.append(allocator, Block{ + .buffer = block_buffer + }); + } + + const last_block = &self.blocks.items[self.blocks.items.len - 1]; + appended_count += last_block.append(samples[appended_count..]); + + if (last_block.min) |block_min| { + self.min = @min(self.min orelse block_min, block_min); + } + + if (last_block.max) |block_max| { + self.max = @max(self.max orelse block_max, block_max); + } + } + } + + pub fn getLength(self: *const SampleList) usize { + var length: usize = 0; + if (self.blocks.items.len > 1) { + length += (self.blocks.items.len - 1) * Block.capacity; + } + + if (self.blocks.items.len > 0) { + length += self.blocks.getLast().len; + } + return length; + } + + pub fn iterator(self: *SampleList, from: usize, to: usize) Iterator { + return Iterator{ + .sample_list = self, + .current = from, + .to = to + }; + } + + pub fn blockIdIterator(self: *SampleList, from_sample: usize, to_sample: usize) BlockIdIterator { + _ = self; + return BlockIdIterator{ + .current = @divFloor(from_sample, Block.capacity), + .to = @divFloor(to_sample, Block.capacity), + }; + } + + pub fn writeToFile(self: *SampleList, file: std.fs.File) !void { + const writer = file.writer(); + var last_sample: f64 = 0; + for (0.., self.blocks.items) |i, *block| { + const block_samples = block.samplesSlice(); + if (block_samples.len > 0) { + try writer.writeAll(std.mem.sliceAsBytes(block_samples)); + last_sample = block_samples[block_samples.len - 1]; + } + + if (i < self.blocks.items.len - 1) { + const unused_size = SampleList.Block.capacity - block_samples.len; + if (unused_size > 0) { + try writer.writeBytesNTimes(std.mem.asBytes(&last_sample), unused_size); + } + } + } + } + + test { + var sample_list = SampleList.init(std.testing.allocator); + defer sample_list.deinit(); + + try sample_list.append(&[1]f64{ 0 } ** 256); + try std.testing.expect(sample_list.block_sizes.items[0] == 256); + + try sample_list.append(&[1]f64{ 0 } ** 256); + try std.testing.expect(sample_list.block_sizes.items[0] == 512); + + try sample_list.append(&[1]f64{ 0 } ** 100); + try std.testing.expect(sample_list.block_sizes.items[1] == 100); + } + + test { + var sample_list = SampleList.init(std.testing.allocator); + defer sample_list.deinit(); + + try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3)); + + try std.testing.expect( + sample_list.getBlockAtSample(Block.capacity - 1).?.ptr == sample_list.getBlock(0).ptr + ); + try std.testing.expect( + sample_list.getBlockAtSample(Block.capacity).?.ptr == sample_list.getBlock(1).ptr + ); + try std.testing.expect( + sample_list.getBlockAtSample(2*Block.capacity).?.ptr == sample_list.getBlock(2).ptr + ); + } + + test { + var sample_list = SampleList.init(std.testing.allocator); + defer sample_list.deinit(); + + try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3)); + + var iter = sample_list.iterator(0, Block.capacity); + + try std.testing.expectEqual(sample_list.getBlock(0), iter.next()); + try std.testing.expectEqual(null, iter.next()); + } + + test { + var sample_list = SampleList.init(std.testing.allocator); + defer sample_list.deinit(); + + try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3)); + + var iter = sample_list.iterator(256, Block.capacity); + + try std.testing.expectEqual(sample_list.getBlock(0)[256..], iter.next()); + try std.testing.expectEqual(null, iter.next()); + } + + test { + var sample_list = SampleList.init(std.testing.allocator); + defer sample_list.deinit(); + + try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3)); + + var iter = sample_list.iterator(256, Block.capacity + 256); + + try std.testing.expectEqual(sample_list.getBlock(0)[256..], iter.next()); + try std.testing.expectEqual(sample_list.getBlock(1)[0..256], iter.next()); + try std.testing.expectEqual(null, iter.next()); + } + + test { + var sample_list = SampleList.init(std.testing.allocator); + defer sample_list.deinit(); + + try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3)); + + var iter = sample_list.iterator(400, 2*Block.capacity + 100); + + try std.testing.expectEqual(sample_list.getBlock(0)[400..], iter.next()); + try std.testing.expectEqual(sample_list.getBlock(1), iter.next()); + try std.testing.expectEqual(sample_list.getBlock(2)[0..100], iter.next()); + try std.testing.expectEqual(null, iter.next()); } }; @@ -297,26 +571,32 @@ pub const View = struct { self.standard_deviation = null; } - pub fn refresh(self: *MarkedRange, samples: []const f64) void { + pub fn refresh(self: *MarkedRange, sample_list: *SampleList) void { self.clear(); if (self.axis == .X) { - const from = std.math.clamp(@as(usize, @intFromFloat(@floor(self.range.lower))), 0, samples.len); - const to = std.math.clamp(@as(usize, @intFromFloat(@ceil(self.range.upper))), 0, samples.len); - if (to - from == 0) return; + var sample_count: f64 = 0; + var sum: f64 = 0; + var min = std.math.inf(f64); + var max = -std.math.inf(f64); - const samples_in_range = samples[from..to]; - if (samples_in_range.len > 0) { - const sample_count: f64 = @floatFromInt(samples_in_range.len); + const from_sample: usize = @intFromFloat(@floor(std.math.clamp(self.range.lower, 0, @as(f64, @floatFromInt(sample_list.getLength()))))); + const to_sample: usize = @intFromFloat(@ceil(std.math.clamp(self.range.upper, 0, @as(f64, @floatFromInt(sample_list.getLength()))))); - var sum: f64 = 0; - var min = samples_in_range[0]; - var max = samples_in_range[0]; - for (samples_in_range) |sample| { - min = @min(min, sample); - max = @max(max, sample); - sum += sample; + { + var iter = sample_list.iterator(from_sample, to_sample); + while (iter.next()) |segment| { + sample_count += @floatFromInt(segment.len); + + for (segment) |sample| { + min = @min(min, sample); + max = @max(max, sample); + sum += sample; + } } + } + + if (sample_count > 0) { const average = sum / sample_count; self.min = min; @@ -325,8 +605,11 @@ pub const View = struct { if (sample_count > 1) { var standard_deviation: f64 = 0; - for (samples_in_range) |sample| { - standard_deviation += (sample - average) * (sample - average); + var iter = sample_list.iterator(from_sample, to_sample); + while (iter.next()) |segment| { + for (segment) |sample| { + standard_deviation += (sample - average) * (sample - average); + } } standard_deviation /= (sample_count - 1); standard_deviation = @sqrt(standard_deviation); @@ -379,7 +662,8 @@ pub const View = struct { available_x_range: RangeF64 = RangeF64.init(0, 0), available_y_range: RangeF64 = RangeF64.init(0, 0), unit: ?NIDaq.Unit = .Voltage, - transformed_samples: ?[]f64 = null, + + transformed_samples: ?Id = null, pub fn clear(self: *View) void { self.graph_cache.clear(); @@ -401,19 +685,6 @@ pub const View = struct { }; } - pub fn appendMarkedRange(self: *View, axis: UI.Axis, range: RangeF64) ?*MarkedRange { - if (self.marked_ranges.unusedCapacitySlice().len == 0) { - return null; - } - - const marked_range = self.marked_ranges.addOneAssumeCapacity(); - marked_range.* = MarkedRange{ - .axis = axis, - .range = range - }; - return marked_range; - } - pub fn iterMarkedRanges(self: *View, axis: UI.Axis) MarkedRangeIterator { return MarkedRangeIterator{ .view = self, @@ -469,7 +740,7 @@ pub const Project = struct { return result_range; } - pub fn getViewSampleList(self: *Project, view_id: Id) Id { + pub fn getViewSampleListId(self: *Project, view_id: Id) Id { const view = self.views.get(view_id).?; switch (view.reference) { @@ -484,75 +755,150 @@ pub const Project = struct { } } - pub fn getViewSamples(self: *Project, view_id: Id) []const f64 { - const samples_list_id = self.getViewSampleList(view_id); - const samples_list = self.sample_lists.get(samples_list_id).?; - - return samples_list.samples.items; - } - 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) + @floatFromInt(sample_list.getLength()), + @floatFromInt(sample_list.getLength() + 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); + try sample_list.append(samples); + try self.refreshTransformedSamplesBySampleList(allocator, sample_list_id); self.refreshMarkedRanges(sample_list_id, affected_range); } - pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) void { + 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) + @floatFromInt(sample_list.getLength()) ); sample_list.clear(allocator); + try self.refreshTransformedSamplesBySampleList(allocator, sample_list_id); self.refreshMarkedRanges(sample_list_id, affected_range); } + pub fn addSampleList(self: *Project, allocator: Allocator) !Id { + return try self.sample_lists.insert(SampleList.init(allocator)); + } + + pub fn removeSampleList(self: *Project, sample_list_id: Id) void { + const sample_list = self.sample_lists.get(sample_list_id) orelse return; + sample_list.deinit(); + self.sample_lists.remove(sample_list_id); + } + 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; + _ = sample_list; + _ = affected_range; + // 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); + // } + // } + // } + } + + fn refreshTransformedSamplesBySampleList(self: *Project, allocator: Allocator, sample_list_id: Id) !void { 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); - } + const view_sample_list_id = self.getViewSampleListId(view_id); + if (view_sample_list_id.eql(sample_list_id)) { + try self.refreshTransformedSamplesByView(allocator, view_id); } } } + fn refreshTransformedSamplesByView(self: *Project, allocator: Allocator, view_id: Id) !void { + const view = self.views.get(view_id).?; + + if (view.sliding_window) |sliding_window| { + if (view.transformed_samples == null) { + view.transformed_samples = try self.addSampleList(allocator); + } + // var transformed_sample_list = self.sample_lists.get(view.transformed_samples.?).?; + + // const samples = self.getViewSamples(view_id); + + // try transformed_sample_list.append(samples); + + _ = sliding_window; + } else { + if (view.transformed_samples) |sample_list_id| { + self.removeSampleList(sample_list_id); + view.transformed_samples = null; + } + } + } + + pub fn readSamplesFromFile(self: *Project, allocator: Allocator, sample_list_id: Id, file: std.fs.File, sample_count: usize) !void { + var bytes_left: usize = sample_count * 8; + var buffer: [SampleList.Block.capacity * 8]u8 = undefined; + while (true) { + const count = try file.readAll(buffer[0..@min(bytes_left, buffer.len)]); + if (count == 0) break; + assert(count % 8 == 0); + bytes_left -= count; + + const samples = std.mem.bytesAsSlice(f64, buffer[0..count]); + try self.appendSamples(allocator, sample_list_id, @alignCast(samples)); + } + } + + pub fn updateSlidingWindow(self: *Project, view_id: Id, allocator: Allocator, sliding_window: ?f64) !void { + const view = self.views.get(view_id).?; + if (view.sliding_window == sliding_window) { + return; + } + + view.sliding_window = sliding_window; + + try self.refreshTransformedSamplesByView(allocator, view_id); + } + + pub fn appendMarkedRange(self: *Project, view_id: Id, axis: UI.Axis, range: RangeF64) ?*View.MarkedRange { + const view = self.views.get(view_id) orelse return null; + + if (view.marked_ranges.unusedCapacitySlice().len == 0) { + return null; + } + + const marked_range = view.marked_ranges.addOneAssumeCapacity(); + marked_range.* = View.MarkedRange{ + .axis = axis, + .range = range + }; + + const sample_list_id = self.getViewSampleListId(view_id); + if (self.sample_lists.get(sample_list_id)) |sample_list| { + marked_range.refresh(sample_list); + } + + return marked_range; + } + pub fn deinit(self: *Project, allocator: Allocator) void { var file_iter = self.files.iterator(); while (file_iter.next()) |file| { @@ -621,8 +967,8 @@ 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 sample_list_id = try self.addSampleList(allocator); + errdefer self.removeSampleList(sample_list_id); const channel = self.channels.get(id).?; channel.* = Channel{ @@ -643,8 +989,8 @@ 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 sample_list_id = try self.addSampleList(allocator); + errdefer self.removeSampleList(sample_list_id); const file = self.files.get(id).?; file.* = File{ @@ -735,8 +1081,8 @@ pub const Project = struct { const samples_file = try dir.createFile(path, .{}); defer samples_file.close(); - const samples = self.sample_lists.get(channel.collected_samples_id).?.samples.items; - try writeFileF64(samples_file, samples); + const sample_list = self.sample_lists.get(channel.collected_samples_id).?; + try sample_list.writeToFile(samples_file); } const stat = try dir.statFile(path); @@ -838,6 +1184,7 @@ pub const Project = struct { fn readString(reader: anytype, allocator: Allocator) ![]u8 { // TODO: This could be risky. `str_len` can be a really large number and in turn request a really large allocation. + // Maybe some sane maximum should be implemented? const str_len = try readInt(reader, u32); const buff = try allocator.alloc(u8, str_len); errdefer allocator.free(buff); @@ -1245,7 +1592,7 @@ fn startCollection(self: *App) !void { for (channels_in_task.constSlice()) |id| { const channel = self.getChannel(id).?; - self.project.clearSamples(self.allocator, channel.collected_samples_id); + try self.project.clearSamples(self.allocator, channel.collected_samples_id); } const read_array = try self.allocator.alloc(f64, @@ -1420,8 +1767,8 @@ 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 sample_list_id = try self.project.addSampleList(self.allocator); + errdefer self.project.removeSampleList(sample_list_id); const file = self.project.files.get(id).?; file.* = File{ @@ -1436,41 +1783,6 @@ pub fn addFile(self: *App, path: []const u8) !Id { return id; } -fn readFileF64(allocator: Allocator, file: std.fs.File) ![]f64 { - const byte_count = try file.getEndPos(); - if (byte_count % 8 != 0) { - return error.NotMultipleOf8; - } - - var samples = try allocator.alloc(f64, @divExact(byte_count, 8)); - errdefer allocator.free(samples); - - try file.seekTo(0); - - 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 writeFileF64(file: std.fs.File, data: []const f64) !void { - try file.seekTo(0); - - for (data) |number| { - const number_bytes = std.mem.asBytes(&number); - try file.writeAll(number_bytes); - } -} - 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; @@ -1483,10 +1795,12 @@ pub fn loadFile(self: *App, id: Id) !void { const samples_file = try cwd.openFile(file.path, .{ .mode = .read_only }); defer samples_file.close(); - const samples = try readFileF64(self.allocator, samples_file); - defer self.allocator.free(samples); + const byte_count = try samples_file.getEndPos(); + if (byte_count % 8 != 0) { + return error.NotMultipleOf8; + } - try self.project.appendSamples(self.allocator, file.samples_id, samples); + try self.project.readSamplesFromFile(self.allocator, file.samples_id, samples_file, @divExact(byte_count, 8)); } // ---------------- Channels --------------------------------- // @@ -1499,8 +1813,8 @@ 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 sample_list_id = try self.project.addSampleList(self.allocator); + errdefer self.project.removeSampleList(sample_list_id); const channel = self.project.channels.get(id).?; channel.* = Channel{ @@ -1574,10 +1888,12 @@ fn loadSavedSamples(self: *App, channel_id: Id) !void { const samples_file = try dir.openFile(saved_samples_location, .{ }); defer samples_file.close(); - const samples = try readFileF64(self.allocator, samples_file); - defer self.allocator.free(samples); + const byte_count = try samples_file.getEndPos(); + if (byte_count % 8 != 0) { + return error.NotMultipleOf8; + } - try self.project.appendSamples(self.allocator, channel.collected_samples_id, samples); + try self.project.readSamplesFromFile(self.allocator, channel.collected_samples_id, samples_file, @divExact(byte_count, 8)); const stat = try dir.statFile(saved_samples_location); channel.last_sample_save_at = stat.mtime; @@ -1646,23 +1962,12 @@ pub fn addView(self: *App, reference: View.Reference) !Id { return id; } -pub fn getViewSamples(self: *App, id: Id) []const f64 { - return self.project.getViewSamples(id); -} - -pub fn getViewMinMaxCache(self: *App, id: Id) Graph.MinMaxCache { - const samples_list_id = self.project.getViewSampleList(id); - const sample_list = self.project.sample_lists.get(samples_list_id).?; - - 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_id = self.project.getViewSampleListId(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)); + view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength())); switch (view.reference) { .channel => |channel_id| if (self.getChannel(channel_id)) |channel| { @@ -1671,8 +1976,8 @@ fn refreshViewAvailableXYRanges(self: *App, id: Id) void { view.available_y_range = RangeF64.init(allowed_sample_values.upper, allowed_sample_values.lower); }, .file => { - const min_sample = sample_list.min_sample orelse 0; - const max_sample = sample_list.max_sample orelse 1; + const min_sample = sample_list.min orelse 0; + const max_sample = sample_list.max orelse 1; view.available_y_range = RangeF64.init(max_sample, min_sample); } } @@ -1691,4 +1996,8 @@ pub fn loadView(self: *App, id: Id) !void { if (view.graph_opts.y_range.size() == 0) { view.graph_opts.y_range = view.available_y_range; } +} + +test { + _ = SampleList; } \ No newline at end of file diff --git a/src/components/view.zig b/src/components/view.zig index fda91d5..40ce18d 100644 --- a/src/components/view.zig +++ b/src/components/view.zig @@ -37,7 +37,6 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box { const app = ctx.app; const view = app.getView(view_id).?; - const samples = app.getViewSamples(view_id); const view_opts = &view.graph_opts; const graph_box = ui.createBox(.{ @@ -110,8 +109,9 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box { { // Render graph - view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id); - Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples); + const sample_list_id = app.project.getViewSampleListId(view_id); + const sample_list = app.project.sample_lists.get(sample_list_id).?; + Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, sample_list); if (view.graph_cache.texture) |texture| { graph_box.texture = texture.texture; } diff --git a/src/components/view_ruler.zig b/src/components/view_ruler.zig index 75def9d..26813b5 100644 --- a/src/components/view_ruler.zig +++ b/src/components/view_ruler.zig @@ -406,7 +406,6 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U } } } else if (ctx.view_controls.selected_tool == .select) { - // TODO: if (cursor != null) { if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| { @@ -418,10 +417,7 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U const hold_end_mouse = view_range.remapTo(mouse_range, range.upper); const mouse_move_distance = @abs(hold_end_mouse - hold_start_mouse); if (signal.flags.contains(.left_released) and mouse_move_distance > 5) { - if (view.appendMarkedRange(axis, range)) |marked_range| { - marked_range.refresh(ctx.project.getViewSamples(view_id)); - } - + _ = ctx.project.appendMarkedRange(view_id, axis, range); } } } diff --git a/src/graph.zig b/src/graph.zig index 0b018f4..86d1e2f 100644 --- a/src/graph.zig +++ b/src/graph.zig @@ -3,6 +3,7 @@ const std = @import("std"); const rl = @import("raylib"); const srcery = @import("./srcery.zig"); const RangeF64 = @import("./range.zig").RangeF64; +const SampleList = @import("./app.zig").SampleList; const remap = @import("./utils.zig").remap; const assert = std.debug.assert; @@ -24,130 +25,6 @@ pub const ViewOptions = struct { y_range: RangeF64 = RangeF64.init(0, 0), color: rl.Color = srcery.red, - - fn writeStruct(self: ViewOptions, writer: anytype) !void { - _ = self; - _ = writer; - // try writer.writeStructEndian(self.id, file_endian); - // try writer.writeStructEndian(self.channel_name.constSlice(), file_endian); - } - - fn readStruct(reader: anytype) !ViewOptions { - _ = reader; - // const id = try reader.readStructEndian(Id, file_endian); - // const channel_name = try reader.readStructEndian([]const u8, file_endian); - - return ViewOptions{ - .x_range = RangeF64.init(0, 0), - .y_range = RangeF64.init(0, 0), - }; - } -}; - -pub const MinMaxCache = struct { - const MinMaxPair = struct { - min: f64, - max: f64 - }; - - chunk_size: usize = 256, - min_max_pairs: std.ArrayListUnmanaged(MinMaxPair) = .{}, - sample_count: usize = 0, - - pub fn deinit(self: *MinMaxCache, allocator: std.mem.Allocator) void { - self.min_max_pairs.clearAndFree(allocator); - self.sample_count = 0; - } - - fn getMinMaxPair(self: *MinMaxCache, chunk: []const f64) MinMaxPair { - assert(chunk.len > 0); - assert(chunk.len <= self.chunk_size); - - var min_sample = chunk[0]; - var max_sample = chunk[0]; - for (chunk) |sample| { - min_sample = @min(min_sample, sample); - max_sample = @max(max_sample, sample); - } - return MinMaxPair{ - .min = min_sample, - .max = max_sample - }; - } - - pub fn updateAll(self: *MinMaxCache, allocator: std.mem.Allocator, samples: []const f64) !void { - self.min_max_pairs.clearRetainingCapacity(); - self.sample_count = 0; - - if (samples.len == 0) return; - - var iter = std.mem.window(f64, samples, self.chunk_size, self.chunk_size); - while (iter.next()) |chunk| { - try self.min_max_pairs.append(allocator, self.getMinMaxPair(chunk)); - } - - self.sample_count = samples.len; - } - - pub fn updateLast(self: *MinMaxCache, allocator: std.mem.Allocator, samples: []const f64) !void { - if (self.sample_count > samples.len) { - try self.updateAll(allocator, samples); - } - if (self.sample_count == samples.len) { - return; - } - - const from_chunk = @divFloor(self.sample_count, self.chunk_size); - const to_chunk = @divFloor(samples.len - 1, self.chunk_size); - - for (from_chunk..(to_chunk+1)) |i| { - const chunk = samples[ - (self.chunk_size*i)..(@min(self.chunk_size*(i+1), samples.len)) - ]; - const min_max_pair = self.getMinMaxPair(chunk); - - if (i >= self.min_max_pairs.items.len) { - try self.min_max_pairs.append(allocator, min_max_pair); - } else { - self.min_max_pairs.items[i] = min_max_pair; - } - } - - self.sample_count = samples.len; - } - - test { - const allocator = std.testing.allocator; - var min_max_cache: MinMaxCache = .{ .chunk_size = 4 }; - defer min_max_cache.deinit(allocator); - - try min_max_cache.updateLast(allocator, &.{ 1, 2 }); - try std.testing.expectEqualSlices( - MinMaxPair, - &.{ - MinMaxPair{ .min = 1, .max = 2 } - }, - min_max_cache.min_max_pairs.items - ); - - try min_max_cache.updateLast(allocator, &.{ 1, 2, 3 }); - try std.testing.expectEqualSlices( - MinMaxPair, - &.{ - MinMaxPair{ .min = 1, .max = 3 } - }, - min_max_cache.min_max_pairs.items - ); - - try min_max_cache.updateLast(allocator, &.{ 1, 2, 3, -1 }); - try std.testing.expectEqualSlices( - MinMaxPair, - &.{ - MinMaxPair{ .min = -1, .max = 3 } - }, - min_max_cache.min_max_pairs.items - ); - } }; pub const RenderCache = struct { @@ -156,7 +33,6 @@ pub const RenderCache = struct { drawn_x_range: RangeF64 }; - min_max_cache: ?MinMaxCache = null, texture: ?rl.RenderTexture2D = null, key: ?Key = null, @@ -235,7 +111,164 @@ pub const SampleIterator = struct { } }; -fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { +fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void { + const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect); + + // TODO: Wtf is this, I don't remember why I fiddled with the x range. + var view_x_range = options.x_range; + view_x_range.upper += 2; + view_x_range.lower -= 1; + + const i_range = view_x_range.intersectPositive( + RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)) + ); + if (i_range.lower > i_range.upper) { + return; + } + + var block_id_iter = sample_list.blockIdIterator( + @intFromFloat(i_range.lower), + @intFromFloat(i_range.upper) + ); + while (block_id_iter.next()) |block_id| { + const block = sample_list.getBlock(block_id) orelse continue; + const block_samples = block.samplesSlice(); + if (block_samples.len == 0) continue; + + if (block_samples.len > 1) { + for (0..(block_samples.len-1)) |i| { + const i_f64: f64 = @floatFromInt(block_id * SampleList.Block.capacity + i); + rl.drawLineV( + .{ + .x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)), + .y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[i])), + }, + .{ + .x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)), + .y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[i + 1])), + }, + options.color + ); + } + } + + if (block_samples.len == SampleList.Block.capacity) { + if (sample_list.getBlock(block_id + 1)) |next_block| { + const next_block_samples = next_block.samplesSlice(); + if (next_block_samples.len > 0) { + const i_f64: f64 = @floatFromInt((block_id + 1) * SampleList.Block.capacity - 1); + rl.drawLineV( + .{ + .x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)), + .y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[SampleList.Block.capacity - 1])), + }, + .{ + .x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)), + .y = @floatCast(options.y_range.remapTo(draw_y_range, next_block_samples[0])), + }, + options.color + ); + } + } + } + } +} + +fn drawColumn(draw_rect: rl.Rectangle, options: ViewOptions, sample_index: f64, column_min: f64, column_max: f64) void { + const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect); + + const x = options.x_range.remapTo(draw_x_range, sample_index); + const y_min = options.y_range.remapTo(draw_y_range, column_min); + const y_max = options.y_range.remapTo(draw_y_range, column_max); + + if (@abs(y_max - y_min) < 1) { + const avg = (y_min + y_max) / 2; + rl.drawLineV( + .{ .x = @floatCast(x), .y = @floatCast(avg) }, + .{ .x = @floatCast(x), .y = @floatCast(avg+1) }, + options.color + ); + } else { + rl.drawLineV( + .{ .x = @floatCast(x), .y = @floatCast(y_min) }, + .{ .x = @floatCast(x), .y = @floatCast(y_max) }, + options.color + ); + } +} + +fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void { + const i_range = options.x_range.intersectPositive( + RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)) + ); + if (i_range.isNegative()) { + return; + } + + const samples_per_column = options.x_range.size() / @as(f64, @floatCast(draw_rect.width)); + assert(samples_per_column >= 1); + + var sample_index = i_range.lower; + while (sample_index < i_range.upper) : (sample_index += samples_per_column) { + var column_min: f64 = std.math.inf(f64); + var column_max: f64 = -std.math.inf(f64); + + var iter = sample_list.iterator( + @intFromFloat(sample_index), + @intFromFloat(sample_index + samples_per_column + 1) + ); + while (iter.next()) |segment| { + for (segment) |sample| { + column_min = @min(column_min, sample); + column_max = @max(column_max, sample); + } + } + + if (!std.math.isInf(column_min)) { + drawColumn(draw_rect, options, sample_index, column_min, column_max); + } + } +} + +fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void { + const i_range = options.x_range.intersectPositive( + RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)) + ); + if (i_range.isNegative()) { + return; + } + + const samples_per_column = options.x_range.size() / @as(f64, @floatCast(draw_rect.width)); + assert(samples_per_column >= @as(f64, @floatFromInt(SampleList.Block.capacity))); + + var i = i_range.lower; + while (i < i_range.upper) : (i += samples_per_column) { + var column_min: f64 = std.math.inf(f64); + var column_max: f64 = -std.math.inf(f64); + + var block_id_iter = sample_list.blockIdIterator( + @intFromFloat(i), + @intFromFloat(i + samples_per_column) + ); + while (block_id_iter.next()) |block_id| { + const block = sample_list.getBlock(block_id) orelse continue; + + if (block.min) |block_min| { + column_min = @min(column_min, block_min); + } + + if (block.max) |block_max| { + column_max = @max(column_max, block_max); + } + } + + if (!std.math.isInf(column_min)) { + drawColumn(draw_rect, options, i, column_min, column_max); + } + } +} + +fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void { rl.beginScissorMode( @intFromFloat(draw_rect.x), @intFromFloat(draw_rect.y), @@ -244,157 +277,24 @@ fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []co ); defer rl.endScissorMode(); - const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect); - - var view_x_range = options.x_range; - view_x_range.upper += 2; - view_x_range.lower -= 1; - - const i_range = view_x_range.intersectPositive( - RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0)) - ); - if (i_range.lower > i_range.upper) { - return; - } - - const from_i: usize = @intFromFloat(i_range.lower); - const to_i: usize = @intFromFloat(i_range.upper); - if (to_i == 0 or from_i == to_i) { - return; - } - - for (from_i..(to_i-1)) |i| { - const i_f64: f64 = @floatFromInt(i); - rl.drawLineV( - .{ - .x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)), - .y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i])), - }, - .{ - .x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)), - .y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i + 1])), - }, - options.color - ); - } -} - -fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { - const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect); - - const i_range = options.x_range.intersectPositive( - RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0)) - ); - if (i_range.isNegative()) { - return; - } - - const samples_per_column = options.x_range.size() / draw_x_range.size(); - assert(samples_per_column >= 1); - - 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]; - - for (column_samples) |sample| { - column_min = @min(column_min, sample); - column_max = @max(column_max, sample); - } - - 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); - - if (@abs(y_max - y_min) < 1) { - const avg = (y_min + y_max) / 2; - rl.drawLineV( - .{ .x = @floatCast(x), .y = @floatCast(avg) }, - .{ .x = @floatCast(x), .y = @floatCast(avg+1) }, - options.color - ); - } else { - rl.drawLineV( - .{ .x = @floatCast(x), .y = @floatCast(y_min) }, - .{ .x = @floatCast(x), .y = @floatCast(y_max) }, - options.color - ); - } - } -} - -fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, min_max_cache: MinMaxCache) void { - const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect); - - const i_range = options.x_range.intersectPositive( - RangeF64.init(0, @max(@as(f64, @floatFromInt(min_max_cache.sample_count)) - 1, 0)) - ); - 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) { - var column_start: usize = @intFromFloat(i); - var column_end: usize = @intFromFloat(i + samples_per_column); - - column_start = @divFloor(column_start, min_max_cache.chunk_size); - column_end = @divFloor(column_end, min_max_cache.chunk_size); - - const min_max_pairs = min_max_cache.min_max_pairs.items[column_start..column_end]; - if (min_max_pairs.len == 0) continue; - - var column_min = min_max_pairs[0].min; - var column_max = min_max_pairs[0].max; - - for (min_max_pairs) |min_max_pair| { - column_min = @min(column_min, min_max_pair.min); - column_max = @max(column_max, min_max_pair.max); - } - - const x = options.x_range.remapTo(draw_x_range, 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); - - if (@abs(y_max - y_min) < 1) { - const avg = (y_min + y_max) / 2; - rl.drawLineV( - .{ .x = @floatCast(x), .y = @floatCast(avg) }, - .{ .x = @floatCast(x), .y = @floatCast(avg+1) }, - options.color - ); - } else { - rl.drawLineV( - .{ .x = @floatCast(x), .y = @floatCast(y_min) }, - .{ .x = @floatCast(x), .y = @floatCast(y_max) }, - options.color - ); - } - } -} - -fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64, min_max_cache: ?MinMaxCache) void { const x_range = options.x_range; - if (x_range.lower >= @as(f64, @floatFromInt(samples.len))) return; + if (x_range.lower >= @as(f64, @floatFromInt(sample_list.getLength()))) return; if (x_range.upper < 0) return; const samples_per_column = x_range.size() / draw_rect.width; if (samples_per_column >= 2) { - if (min_max_cache != null and samples_per_column > @as(f64, @floatFromInt(2*min_max_cache.?.chunk_size))) { - drawSamplesMinMax(draw_rect, options, min_max_cache.?); + if (samples_per_column >= @as(f64, @floatFromInt(2*SampleList.Block.capacity))) { + drawSamplesMinMax(draw_rect, options, sample_list); } else { - drawSamplesApproximate(draw_rect, options, samples); + drawSamplesApproximate(draw_rect, options, sample_list); } } else { - drawSamplesExact(draw_rect, options, samples); + drawSamplesExact(draw_rect, options, sample_list); } } -pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, samples: []const f64) void { +pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, sample_list: *SampleList) void { const render_width: i32 = @intFromFloat(@ceil(render_size.x)); const render_height: i32 = @intFromFloat(@ceil(render_size.y)); @@ -423,7 +323,7 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, const cache_key = RenderCache.Key{ .options = options, - .drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0)).intersectPositive(options.x_range) + .drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)).intersectPositive(options.x_range) }; if (cache.key != null and std.meta.eql(cache.key.?, cache_key)) { @@ -449,23 +349,19 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, .width = render_size.x, .height = render_size.y }; - drawSamples(draw_rect, options, samples, cache.min_max_cache); + drawSamples(draw_rect, options, sample_list); } -pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { +pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) 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); + drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, sample_list); c.draw(draw_rect); } else { - drawSamples(draw_rect, options, samples, null); + drawSamples(draw_rect, options, sample_list, null); } } - -test { - _ = MinMaxCache; -} \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index 30a8b69..d3099e1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -66,7 +66,7 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{ - .thread_safe = true + .thread_safe = true, }){}; const allocator = gpa.allocator(); defer _ = gpa.deinit(); @@ -78,6 +78,8 @@ pub fn main() !void { raylib_h.SetTraceLogCallback(raylibTraceLogCallback); rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level)); + // std.debug.print("{any}\n", .{ std.mem.bytesAsSlice(f64, std.mem.sliceAsBytes(&[_]f64{ 1, 2, 3}) ) }); + const icon_png = @embedFile("./assets/icon.png"); var icon_image = rl.loadImageFromMemory(".png", icon_png); defer icon_image.unload(); @@ -183,11 +185,11 @@ pub fn main() !void { rl.endDrawing(); } - } test { _ = @import("./ni-daq/root.zig"); _ = @import("./range.zig"); _ = @import("./graph.zig"); + _ = Application; } \ No newline at end of file diff --git a/src/screens/main_screen.zig b/src/screens/main_screen.zig index d3e781a..5b31403 100644 --- a/src/screens/main_screen.zig +++ b/src/screens/main_screen.zig @@ -28,7 +28,7 @@ frequency_input: UI.TextInputStorage, amplitude_input: UI.TextInputStorage, protocol_error_message: ?[]const u8 = null, protocol_graph_cache: Graph.RenderCache = .{}, -preview_samples: std.ArrayListUnmanaged(f64) = .{}, +preview_sample_list_id: App.Id, preview_samples_y_range: RangeF64 = RangeF64.init(0, 0), // Project settings @@ -49,7 +49,8 @@ pub fn init(app: *App) !MainScreen { .amplitude_input = UI.TextInputStorage.init(allocator), .sample_rate_input = UI.TextInputStorage.init(allocator), .view_controls = ViewControlsSystem.init(&app.project), - .sliding_window_input = UI.TextInputStorage.init(allocator) + .sliding_window_input = UI.TextInputStorage.init(allocator), + .preview_sample_list_id = try app.project.addSampleList(allocator) }; try self.frequency_input.setText("10"); @@ -59,12 +60,11 @@ pub fn init(app: *App) !MainScreen { } pub fn deinit(self: *MainScreen) void { - const allocator = self.app.allocator; - self.frequency_input.deinit(); self.amplitude_input.deinit(); self.sample_rate_input.deinit(); - self.preview_samples.clearAndFree(allocator); + self.sliding_window_input.deinit(); + self.app.project.removeSampleList(self.preview_sample_list_id); self.clearProtocolErrorMessage(); } @@ -100,14 +100,15 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void { .borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 }) }); - const samples = self.preview_samples.items; - const view_rect = Graph.ViewOptions{ - .x_range = RangeF64.init(0, @floatFromInt(samples.len)), - .y_range = self.preview_samples_y_range - }; - Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, samples); - if (self.protocol_graph_cache.texture) |texture| { - protocol_view.texture = texture.texture; + if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| { + const view_rect = Graph.ViewOptions{ + .x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength())), + .y_range = self.preview_samples_y_range + }; + Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, sample_list); + if (self.protocol_graph_cache.texture) |texture| { + protocol_view.texture = texture.texture; + } } } @@ -182,9 +183,17 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void { } if (self.protocol_error_message == null and any_input_modified) { - try App.Channel.generateSine(&self.preview_samples, allocator, sample_rate, frequency, amplitude); - self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1); - self.protocol_graph_cache.invalidate(); + if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| { + var preview_samples: std.ArrayListUnmanaged(f64) = .{}; + defer preview_samples.deinit(allocator); + + try App.Channel.generateSine(&preview_samples, allocator, sample_rate, frequency, amplitude); + sample_list.clear(allocator); + try sample_list.append(preview_samples.items); + + self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1); + self.protocol_graph_cache.invalidate(); + } } if (self.protocol_error_message) |message| { @@ -319,7 +328,6 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { .label = "Sync controls" }); - const samples = project.getViewSamples(view_id); switch (view.reference) { .channel => |channel_id| { @@ -363,24 +371,27 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void { self.app.pushCommand(.{ .reload_file = file_id }); } } - } - _ = ui.label("Samples: {d}", .{ samples.len }); + const sample_list_id = project.getViewSampleListId(view_id); + const sample_list = project.sample_lists.get(sample_list_id).?; + const sample_count = sample_list.getLength(); + + _ = ui.label("Samples: {d}", .{ sample_count }); var duration_str: []const u8 = "-"; if (sample_rate != null) { - const duration = @as(f64, @floatFromInt(samples.len)) / sample_rate.?; + 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 }); - _ = try ui.numberInput(f64, .{ + try project.updateSlidingWindow(view_id, self.app.allocator, try ui.numberInput(f64, .{ .key = ui.keyFromString("Sliding window"), .storage = &self.sliding_window_input - }); + })); } fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {