add cache for min/max calculations when drawing graphs
This commit is contained in:
parent
8d1cad16b3
commit
a2fc2befd1
33
src/app.zig
33
src/app.zig
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
151
src/graph.zig
151
src/graph.zig
@ -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);
|
||||
}
|
||||
}
|
@ -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")
|
||||
});
|
||||
|
@ -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),
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user