add cache for min/max calculations when drawing graphs

This commit is contained in:
Rokas Puzonas 2025-04-08 21:36:20 +03:00
parent 8d1cad16b3
commit a2fc2befd1
5 changed files with 181 additions and 13 deletions

View File

@ -173,9 +173,11 @@ pub const Channel = struct {
device: Device = .{},
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 = .{},
pub fn deinit(self: *Channel, allocator: Allocator) void {
self.clear(allocator);
@ -186,6 +188,8 @@ pub const Channel = struct {
task.clear();
self.output_task = null;
}
self.graph_min_max_cache.deinit(allocator);
}
pub fn clear(self: *Channel, allocator: Allocator) void {
@ -221,11 +225,13 @@ pub const File = struct {
// Runtime
samples: ?[]f64 = null,
graph_min_max_cache: Graph.MinMaxCache = .{},
min_sample: f64 = 0,
max_sample: f64 = 0,
pub fn deinit(self: *File, allocator: Allocator) void {
self.clear(allocator);
self.graph_min_max_cache.deinit(allocator);
allocator.free(self.path);
}
@ -253,7 +259,7 @@ pub const View = struct {
sync_controls: bool = false,
// Runtime
graph_cache: Graph.Cache = .{},
graph_cache: Graph.RenderCache = .{},
available_x_range: RangeF64 = RangeF64.init(0, 0),
available_y_range: RangeF64 = RangeF64.init(0, 0),
unit: ?NIDaq.Unit = .Voltage,
@ -756,6 +762,14 @@ pub fn tick(self: *App) !void {
self.collection_samples_mutex.lock();
defer self.collection_samples_mutex.unlock();
// Update channel min max caches
{
var channel_iter = self.project.channels.iterator();
while (channel_iter.next()) |channel| {
try channel.graph_min_max_cache.updateLast(self.allocator, channel.collected_samples.items);
}
}
{
var view_iter = self.project.views.idIterator();
while (view_iter.next()) |id| {
@ -1108,6 +1122,8 @@ pub fn loadFile(self: *App, id: Id) !void {
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);
}
}
@ -1246,6 +1262,21 @@ pub fn getViewSamples(self: *App, id: Id) []const f64 {
return result;
}
pub fn getViewMinMaxCache(self: *App, id: Id) Graph.MinMaxCache {
const view = self.getView(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;
}
}
}
fn refreshViewAvailableXYRanges(self: *App, id: Id) void {
const view = self.getView(id) orelse return;

View File

@ -100,6 +100,8 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
ctx.view_controls.pushViewMove(view_id, view.available_x_range, view.available_y_range);
}
view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id);
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples);
if (view.graph_cache.texture) |texture| {
graph_box.texture = texture.texture;

View File

@ -44,16 +44,90 @@ pub const ViewOptions = struct {
}
};
pub const Cache = struct {
pub const MinMaxCache = struct {
const MinMaxPair = struct {
min: f64,
max: f64
};
const chunk_size = 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(chunk: []const f64) MinMaxPair {
assert(chunk.len > 0);
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, chunk_size, chunk_size);
while (iter.next()) |chunk| {
try self.min_max_pairs.append(allocator, 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, chunk_size);
const to_chunk = @divFloor(samples.len - 1, chunk_size);
for (from_chunk..(to_chunk+1)) |i| {
const chunk = samples[
(chunk_size*i)..(@min(chunk_size*(i+1), samples.len))
];
const min_max_pair = 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;
}
};
pub const RenderCache = struct {
const Key = struct {
options: ViewOptions,
drawn_x_range: RangeF64
};
min_max_cache: ?MinMaxCache = null,
texture: ?rl.RenderTexture2D = null,
key: ?Key = null,
pub fn clear(self: *Cache) void {
pub fn clear(self: *RenderCache) void {
if (self.texture) |texture| {
texture.unload();
self.texture = null;
@ -61,11 +135,11 @@ pub const Cache = struct {
self.key = null;
}
pub fn invalidate(self: *Cache) void {
pub fn invalidate(self: *RenderCache) void {
self.key = null;
}
pub fn draw(self: Cache, rect: rl.Rectangle) void {
pub fn draw(self: RenderCache, rect: rl.Rectangle) void {
if (self.texture) |texture| {
const source = rl.Rectangle{
.x = 0,
@ -177,7 +251,60 @@ fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples
}
}
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
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.lower > i_range.upper) {
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, MinMaxCache.chunk_size);
column_end = @divFloor(column_end, MinMaxCache.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_level: ?MinMaxCache) void {
const x_range = options.x_range;
if (x_range.lower >= @as(f64, @floatFromInt(samples.len))) return;
@ -185,13 +312,17 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f
const samples_per_column = x_range.size() / draw_rect.width;
if (samples_per_column >= 2) {
if (min_max_level != null and samples_per_column > 2*MinMaxCache.chunk_size) {
drawSamplesMinMax(draw_rect, options, min_max_level.?);
} else {
drawSamplesApproximate(draw_rect, options, samples);
}
} else {
drawSamplesExact(draw_rect, options, samples);
}
}
pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
pub fn drawCached(cache: *RenderCache, 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));
@ -218,7 +349,7 @@ pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, sample
const render_texture = cache.texture.?;
const cache_key = Cache.Key{
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)
};
@ -246,10 +377,10 @@ pub fn drawCached(cache: *Cache, render_size: Vec2, options: ViewOptions, sample
.width = render_size.x,
.height = render_size.y
};
drawSamples(draw_rect, options, samples);
drawSamples(draw_rect, options, samples, cache.min_max_cache);
}
pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
if (draw_rect.width < 0 or draw_rect.height < 0) {
return;
}
@ -259,6 +390,6 @@ pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, sampl
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
c.draw(draw_rect);
} else {
drawSamples(draw_rect, options, samples);
drawSamples(draw_rect, options, samples, null);
}
}

View File

@ -102,6 +102,10 @@ pub fn main() !void {
defer app.deinit();
if (builtin.mode == .Debug) {
_ = try app.addView(.{
.channel = try app.addChannel("Dev1/ai0")
});
_ = try app.addView(.{
.file = try app.addFile("./samples-5k.bin")
});

View File

@ -27,7 +27,7 @@ view_controls: ViewControlsSystem,
frequency_input: UI.TextInputStorage,
amplitude_input: UI.TextInputStorage,
protocol_error_message: ?[]const u8 = null,
protocol_graph_cache: Graph.Cache = .{},
protocol_graph_cache: Graph.RenderCache = .{},
preview_samples: std.ArrayListUnmanaged(f64) = .{},
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),