const builtin = @import("builtin"); const std = @import("std"); const rl = @import("raylib"); const srcery = @import("./srcery.zig"); const remap = @import("./utils.zig").remap; const assert = std.debug.assert; const Vec2 = rl.Vector2; const Rect = rl.Rectangle; const clamp = std.math.clamp; const disable_caching = false; comptime { // Just making sure that release build has caching enabled if (builtin.mode != .Debug) { assert(disable_caching == false); } } pub const ViewOptions = struct { from: f32, // inclusive to: f32, // inclusive min_value: f64, max_value: f64, left_aligned: bool = true, color: rl.Color = srcery.red, pub fn mapSampleIndexToX(self: ViewOptions, to_x: f64, to_width: f64, index: f64) f64 { return remap( f64, self.from, self.to, to_x, to_x + to_width, index ); } pub fn mapSampleXToIndex(self: ViewOptions, from_x: f64, from_width: f64, x: f64) f64 { return remap( f64, from_x, from_x + from_width, self.from, self.to, x ); } pub fn mapSampleValueToY(self: ViewOptions, to_y: f64, to_height: f64, sample: f64) f64 { return remap( f64, self.min_value, self.max_value, to_y + to_height, to_y, sample ); } pub fn mapSampleYToValue(self: ViewOptions, to_y: f64, to_height: f64, y: f64) f64 { return remap( f64, to_y + to_height, to_y, self.min_value, self.max_value, y ); } pub fn mapSampleVec2(self: ViewOptions, draw_rect: rl.Rectangle, index: f64, sample: f64) Vec2 { return .{ .x = @floatCast(self.mapSampleIndexToX(draw_rect.x, draw_rect.width, index)), .y = @floatCast(self.mapSampleValueToY(draw_rect.y, draw_rect.height, sample)) }; } }; pub const ViewOptionsWithRect = struct { view: ViewOptions, rect: Rect, pub fn mapSampleIndexToX(self: ViewOptionsWithRect, index: f64) f64 { return self.view.mapSampleIndexToX(self.rect.x, self.rect.width, index); } }; pub const Cache = struct { texture: ?rl.RenderTexture2D = null, options: ?ViewOptions = null, pub fn deinit(self: *Cache) void { if (self.texture) |texture| { texture.unload(); self.texture = null; } self.options = null; } pub fn invalidate(self: *Cache) void { self.options = null; } pub fn draw(self: Cache, rect: rl.Rectangle) void { if (self.texture) |texture| { const source = rl.Rectangle{ .x = 0, .y = 0, .width = @floatFromInt(texture.texture.width), .height = @floatFromInt(texture.texture.height) }; rl.drawTexturePro( texture.texture, source, rect, rl.Vector2.zero(), 0, rl.Color.white ); } } }; fn clampIndex(value: f32, size: usize) f32 { const size_f32: f32 = @floatFromInt(size); return clamp(value, 0, size_f32); } fn clampIndexUsize(value: f32, size: usize) usize { const size_f32: f32 = @floatFromInt(size); return @intFromFloat(clamp(value, 0, size_f32)); } fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { assert(options.left_aligned); // TODO: assert(options.to >= options.from); if (options.from > @as(f32, @floatFromInt(samples.len))) return; if (options.to < 0) return; const sample_count = options.to - options.from; const samples_per_column = sample_count / draw_rect.width; const samples_threshold = 2; if (samples_per_column >= samples_threshold) { var i = clampIndex(options.from, samples.len); while (i < clampIndex(options.to, samples.len)) : (i += samples_per_column) { const from_index = clampIndexUsize(i, samples.len); const to_index = clampIndexUsize(i+samples_per_column, samples.len); const column_samples = samples[from_index..to_index]; if (column_samples.len == 0) continue; 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.mapSampleIndexToX(draw_rect.x, draw_rect.width, @floatFromInt(from_index)); const y_min = options.mapSampleValueToY(draw_rect.y, draw_rect.height, column_min); const y_max = options.mapSampleValueToY(draw_rect.y, draw_rect.height, 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 ); } } } else { rl.beginScissorMode( @intFromFloat(draw_rect.x), @intFromFloat(draw_rect.y), @intFromFloat(draw_rect.width), @intFromFloat(draw_rect.height), ); defer rl.endScissorMode(); const from_index = clampIndexUsize(@floor(options.from), samples.len); const to_index = clampIndexUsize(@ceil(options.to) + 1, samples.len); if (to_index - from_index > 0) { for (from_index..(to_index-1)) |i| { const from_point = options.mapSampleVec2(draw_rect, @floatFromInt(i), samples[i]); const to_point = options.mapSampleVec2(draw_rect, @floatFromInt(i + 1), samples[i + 1]); rl.drawLineV(from_point, to_point, options.color); } } } } pub fn drawCached(cache: *Cache, 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)); if (render_width <= 0 or render_height <= 0) { return; } // Unload render texture if rendering width or height changed if (cache.texture) |render_texture| { const texure = render_texture.texture; if (texure.width != render_width or texure.height != render_height) { render_texture.unload(); cache.texture = null; cache.options = null; } } if (cache.texture == null) { const texture = rl.loadRenderTexture(render_width, render_height); // TODO: Maybe fallback to just drawing without caching, if GPU doesn't have enough memory? assert(rl.isRenderTextureReady(texture)); cache.texture = texture; } const render_texture = cache.texture.?; if (cache.options != null and std.meta.eql(cache.options.?, options)) { // Cached graph hasn't changed, no need to redraw. return; } cache.options = options; render_texture.begin(); defer render_texture.end(); rl.gl.rlPushMatrix(); defer rl.gl.rlPopMatrix(); rl.clearBackground(rl.Color.black.alpha(0)); rl.gl.rlTranslatef(0, render_size.y, 0); rl.gl.rlScalef(1, -1, 1); const draw_rect = rl.Rectangle{ .x = 0, .y = 0, .width = render_size.x, .height = render_size.y }; drawSamples(draw_rect, options, samples); } pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) 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); c.draw(draw_rect); } else { drawSamples(draw_rect, options, samples); } }