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 = .{},
|
device: Device = .{},
|
||||||
allowed_sample_values: ?RangeF64 = null,
|
allowed_sample_values: ?RangeF64 = null,
|
||||||
allowed_sample_rates: ?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) = .{},
|
collected_samples: std.ArrayListUnmanaged(f64) = .{},
|
||||||
write_pattern: std.ArrayListUnmanaged(f64) = .{},
|
write_pattern: std.ArrayListUnmanaged(f64) = .{},
|
||||||
output_task: ?NIDaq.Task = null,
|
output_task: ?NIDaq.Task = null,
|
||||||
|
graph_min_max_cache: Graph.MinMaxCache = .{},
|
||||||
|
|
||||||
pub fn deinit(self: *Channel, allocator: Allocator) void {
|
pub fn deinit(self: *Channel, allocator: Allocator) void {
|
||||||
self.clear(allocator);
|
self.clear(allocator);
|
||||||
@ -186,6 +188,8 @@ pub const Channel = struct {
|
|||||||
task.clear();
|
task.clear();
|
||||||
self.output_task = null;
|
self.output_task = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.graph_min_max_cache.deinit(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(self: *Channel, allocator: Allocator) void {
|
pub fn clear(self: *Channel, allocator: Allocator) void {
|
||||||
@ -221,11 +225,13 @@ pub const File = struct {
|
|||||||
|
|
||||||
// Runtime
|
// Runtime
|
||||||
samples: ?[]f64 = null,
|
samples: ?[]f64 = null,
|
||||||
|
graph_min_max_cache: Graph.MinMaxCache = .{},
|
||||||
min_sample: f64 = 0,
|
min_sample: f64 = 0,
|
||||||
max_sample: f64 = 0,
|
max_sample: f64 = 0,
|
||||||
|
|
||||||
pub fn deinit(self: *File, allocator: Allocator) void {
|
pub fn deinit(self: *File, allocator: Allocator) void {
|
||||||
self.clear(allocator);
|
self.clear(allocator);
|
||||||
|
self.graph_min_max_cache.deinit(allocator);
|
||||||
allocator.free(self.path);
|
allocator.free(self.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +259,7 @@ pub const View = struct {
|
|||||||
sync_controls: bool = false,
|
sync_controls: bool = false,
|
||||||
|
|
||||||
// Runtime
|
// Runtime
|
||||||
graph_cache: Graph.Cache = .{},
|
graph_cache: Graph.RenderCache = .{},
|
||||||
available_x_range: RangeF64 = RangeF64.init(0, 0),
|
available_x_range: RangeF64 = RangeF64.init(0, 0),
|
||||||
available_y_range: RangeF64 = RangeF64.init(0, 0),
|
available_y_range: RangeF64 = RangeF64.init(0, 0),
|
||||||
unit: ?NIDaq.Unit = .Voltage,
|
unit: ?NIDaq.Unit = .Voltage,
|
||||||
@ -756,6 +762,14 @@ pub fn tick(self: *App) !void {
|
|||||||
self.collection_samples_mutex.lock();
|
self.collection_samples_mutex.lock();
|
||||||
defer self.collection_samples_mutex.unlock();
|
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();
|
var view_iter = self.project.views.idIterator();
|
||||||
while (view_iter.next()) |id| {
|
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.min_sample = @min(file.min_sample, sample);
|
||||||
file.max_sample = @max(file.max_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;
|
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 {
|
fn refreshViewAvailableXYRanges(self: *App, id: Id) void {
|
||||||
const view = self.getView(id) orelse return;
|
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);
|
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);
|
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples);
|
||||||
if (view.graph_cache.texture) |texture| {
|
if (view.graph_cache.texture) |texture| {
|
||||||
graph_box.texture = texture.texture;
|
graph_box.texture = texture.texture;
|
||||||
|
153
src/graph.zig
153
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 {
|
const Key = struct {
|
||||||
options: ViewOptions,
|
options: ViewOptions,
|
||||||
drawn_x_range: RangeF64
|
drawn_x_range: RangeF64
|
||||||
};
|
};
|
||||||
|
|
||||||
|
min_max_cache: ?MinMaxCache = null,
|
||||||
texture: ?rl.RenderTexture2D = null,
|
texture: ?rl.RenderTexture2D = null,
|
||||||
key: ?Key = null,
|
key: ?Key = null,
|
||||||
|
|
||||||
pub fn clear(self: *Cache) void {
|
pub fn clear(self: *RenderCache) void {
|
||||||
if (self.texture) |texture| {
|
if (self.texture) |texture| {
|
||||||
texture.unload();
|
texture.unload();
|
||||||
self.texture = null;
|
self.texture = null;
|
||||||
@ -61,11 +135,11 @@ pub const Cache = struct {
|
|||||||
self.key = null;
|
self.key = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalidate(self: *Cache) void {
|
pub fn invalidate(self: *RenderCache) void {
|
||||||
self.key = null;
|
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| {
|
if (self.texture) |texture| {
|
||||||
const source = rl.Rectangle{
|
const source = rl.Rectangle{
|
||||||
.x = 0,
|
.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;
|
const x_range = options.x_range;
|
||||||
|
|
||||||
if (x_range.lower >= @as(f64, @floatFromInt(samples.len))) return;
|
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;
|
const samples_per_column = x_range.size() / draw_rect.width;
|
||||||
if (samples_per_column >= 2) {
|
if (samples_per_column >= 2) {
|
||||||
drawSamplesApproximate(draw_rect, options, samples);
|
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 {
|
} else {
|
||||||
drawSamplesExact(draw_rect, options, samples);
|
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_width: i32 = @intFromFloat(@ceil(render_size.x));
|
||||||
const render_height: i32 = @intFromFloat(@ceil(render_size.y));
|
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 render_texture = cache.texture.?;
|
||||||
|
|
||||||
const cache_key = Cache.Key{
|
const cache_key = RenderCache.Key{
|
||||||
.options = options,
|
.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(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,
|
.width = render_size.x,
|
||||||
.height = render_size.y
|
.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) {
|
if (draw_rect.width < 0 or draw_rect.height < 0) {
|
||||||
return;
|
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);
|
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
|
||||||
c.draw(draw_rect);
|
c.draw(draw_rect);
|
||||||
} else {
|
} else {
|
||||||
drawSamples(draw_rect, options, samples);
|
drawSamples(draw_rect, options, samples, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -102,6 +102,10 @@ pub fn main() !void {
|
|||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
|
|
||||||
if (builtin.mode == .Debug) {
|
if (builtin.mode == .Debug) {
|
||||||
|
_ = try app.addView(.{
|
||||||
|
.channel = try app.addChannel("Dev1/ai0")
|
||||||
|
});
|
||||||
|
|
||||||
_ = try app.addView(.{
|
_ = try app.addView(.{
|
||||||
.file = try app.addFile("./samples-5k.bin")
|
.file = try app.addFile("./samples-5k.bin")
|
||||||
});
|
});
|
||||||
|
@ -27,7 +27,7 @@ view_controls: ViewControlsSystem,
|
|||||||
frequency_input: UI.TextInputStorage,
|
frequency_input: UI.TextInputStorage,
|
||||||
amplitude_input: UI.TextInputStorage,
|
amplitude_input: UI.TextInputStorage,
|
||||||
protocol_error_message: ?[]const u8 = null,
|
protocol_error_message: ?[]const u8 = null,
|
||||||
protocol_graph_cache: Graph.Cache = .{},
|
protocol_graph_cache: Graph.RenderCache = .{},
|
||||||
preview_samples: std.ArrayListUnmanaged(f64) = .{},
|
preview_samples: std.ArrayListUnmanaged(f64) = .{},
|
||||||
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user