diff --git a/build.zig.zon b/build.zig.zon index ee8bfab..7a16772 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,13 +2,20 @@ .name = "Baigiamasis projektas", .version = "0.1.0", - .dependencies = .{ .@"raylib-zig" = .{ .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" }, .@"known-folders" = .{ - .url = "git+https://github.com/ziglibs/known-folders.git#1cceeb70e77dec941a4178160ff6c8d05a74de6f", - .hash = "12205f5e7505c96573f6fc5144592ec38942fb0a326d692f9cddc0c7dd38f9028f29", - }, .ini = .{ - .url = "https://github.com/ziglibs/ini/archive/e18d36665905c1e7ba0c1ce3e8780076b33e3002.tar.gz", - .hash = "1220b0979ea9891fa4aeb85748fc42bc4b24039d9c99a4d65d893fb1c83e921efad8", - }, .@"profiler.zig" = .{ + .dependencies = .{ + .@"raylib-zig" = .{ + .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", + .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" + }, + .@"known-folders" = .{ + .url = "git+https://github.com/ziglibs/known-folders.git#1cceeb70e77dec941a4178160ff6c8d05a74de6f", + .hash = "12205f5e7505c96573f6fc5144592ec38942fb0a326d692f9cddc0c7dd38f9028f29", + }, + .ini = .{ + .url = "https://github.com/ziglibs/ini/archive/e18d36665905c1e7ba0c1ce3e8780076b33e3002.tar.gz", + .hash = "1220b0979ea9891fa4aeb85748fc42bc4b24039d9c99a4d65d893fb1c83e921efad8", + }, + .@"profiler.zig" = .{ .url = "git+https://github.com/lassade/profiler.zig.git#d066d066c36c4eebd494babf15c1cdbd2d512b12", .hash = "122097461acc2064f5f89b85d76d2a02232579864b17604617a333789c892f2d262f", }, diff --git a/src/app.zig b/src/app.zig index 8bb2c6e..4886561 100644 --- a/src/app.zig +++ b/src/app.zig @@ -28,6 +28,10 @@ pub const Id = packed struct { return @bitCast(self); } + pub fn fromInt(self: u32) Id { + return @bitCast(self); + } + pub fn eql(self: Id, other: Id) bool { return @as(u32, @bitCast(self)) == @as(u32, @bitCast(other)); } @@ -78,6 +82,16 @@ fn GenerationalArray(Item: type) type { used: UsedBitSet = UsedBitSet.initFull(), items: [Id.max_items]GenerationalItem = undefined, + pub fn insertUndefinedAt(self: *Self, id: Id) !void { + if (!self.used.isSet(id.index)) { + return error.NotEmpty; + } + + self.used.unset(id.index); + + self.items[id.index].generation = id.generation; + } + pub fn insertUndefined(self: *Self) !Id { const index: Id.Index = @intCast(self.used.findFirstSet() orelse return error.OutOfMemory); @@ -96,11 +110,11 @@ fn GenerationalArray(Item: type) type { } pub fn remove(self: *Self, id: Id) void { - if (self.used.isSet(id.generation)) { + if (self.used.isSet(id.index)) { return; } - self.used.set(id.generation); + self.used.set(id.index); self.items[id.index].generation += 1; } @@ -113,7 +127,7 @@ fn GenerationalArray(Item: type) type { } pub fn has(self: *Self, id: Id) bool { - if (self.used.isSet(id.generation)) { + if (self.used.isSet(id.index)) { return false; } @@ -152,9 +166,9 @@ pub const Channel = struct { // Persistent name: Name = .{}, - device: Device = .{}, // Runtime + device: Device = .{}, allowed_sample_values: ?RangeF64 = null, allowed_sample_rates: ?RangeF64 = null, collected_samples: std.ArrayListUnmanaged(f64) = .{}, @@ -263,6 +277,10 @@ pub const View = struct { }; pub const Project = struct { + const file_endian = std.builtin.Endian.big; + const file_format_version: u8 = 0; + + save_location: ?[]u8 = null, sample_rate: ?f64 = null, channels: GenerationalArray(Channel) = .{}, @@ -307,6 +325,215 @@ pub const Project = struct { while (channel_iter.next()) |channel| { channel.deinit(allocator); } + + if (self.save_location) |str| { + allocator.free(str); + self.save_location = null; + } + } + + // ------------------- Serialization ------------------ // + + pub fn initFromFile(allocator: Allocator, save_location: []const u8) !Project { + var self = Project{}; + + const f = try std.fs.cwd().openFile(save_location, .{}); + defer f.close(); + + const reader = f.reader(); + + const version = try readInt(reader, u8); + if (version != file_format_version) { + return error.VersionMismatch; + } + + self.sample_rate = try readFloat(reader, f64); + if (self.sample_rate == 0) { + self.sample_rate = null; + } + + { // Channels + const channel_count = try readInt(reader, u32); + for (0..channel_count) |_| { + const id = try readId(reader); + + try self.channels.insertUndefinedAt(id); + + const channel_name = try readString(reader, allocator); + defer allocator.free(channel_name); + + const channel = self.channels.get(id).?; + channel.* = Channel{ + .name = try utils.initBoundedStringZ(Channel.Name, channel_name) + }; + } + } + + { // Files + const file_count = try readInt(reader, u32); + for (0..file_count) |_| { + const id = try readId(reader); + + try self.files.insertUndefinedAt(id); + + const path = try readString(reader, allocator); + errdefer allocator.free(path); + + const file = self.files.get(id).?; + file.* = File{ + .path = path + }; + } + } + + { // Views + const view_count = try readInt(reader, u32); + for (0..view_count) |_| { + const id = try readId(reader); + + try self.views.insertUndefinedAt(id); + + const reference_tag = try readInt(reader, u8); + var reference: View.Reference = undefined; + if (reference_tag == @intFromEnum(View.Reference.file)) { + reference = .{ + .file = try readId(reader) + }; + } else if (reference_tag == @intFromEnum(View.Reference.channel)) { + reference = .{ + .channel = try readId(reader) + }; + } else { + return error.InvalidReferenceTag; + } + + const view = self.views.get(id).?; + view.* = View{ + .reference = reference, + }; + + view.graph_opts.x_range = try readRangeF64(reader); + view.graph_opts.y_range = try readRangeF64(reader); + } + } + + self.save_location = try allocator.dupe(u8, save_location); + errdefer allocator.free(self.save_location); + + return self; + } + + pub fn save(self: *Project) !void { + const save_location = self.save_location orelse return error.NoSaveLocation; + + const f = try std.fs.cwd().createFile(save_location, .{}); + defer f.close(); + + const writer = f.writer(); + + try writeInt(writer, u8, file_format_version); + try writeFloat(writer, f64, self.sample_rate orelse 0); + + { // Channels + try writeInt(writer, u32, @intCast(self.channels.count())); + var channel_iter = self.channels.idIterator(); + while (channel_iter.next()) |channel_id| { + const channel = self.channels.get(channel_id).?; + const channel_name = utils.getBoundedStringZ(&channel.name); + + try writeId(writer, channel_id); + try writeString(writer, channel_name); + } + } + + { // Files + try writeInt(writer, u32, @intCast(self.files.count())); + var file_iter = self.files.idIterator(); + while (file_iter.next()) |file_id| { + const file = self.files.get(file_id).?; + + try writeId(writer, file_id); + try writeString(writer, file.path); + } + } + + { // Views + try writeInt(writer, u32, @intCast(self.views.count())); + var view_iter = self.views.idIterator(); + while (view_iter.next()) |view_id| { + const view = self.views.get(view_id).?; + + try writeId(writer, view_id); + try writeInt(writer, u8, @intFromEnum(view.reference)); + switch (view.reference) { + .channel => |channel_id| { + try writeInt(writer, u32, channel_id.asInt()); + }, + .file => |file_id| { + try writeInt(writer, u32, file_id.asInt()); + } + } + + try writeRangeF64(writer, view.graph_opts.x_range); + try writeRangeF64(writer, view.graph_opts.y_range); + } + } + } + + fn writeRangeF64(writer: anytype, range: RangeF64) !void { + try writeFloat(writer, f64, range.lower); + try writeFloat(writer, f64, range.upper); + } + + fn readRangeF64(writer: anytype) !RangeF64 { + var range: RangeF64 = undefined; + + range.lower = try readFloat(writer, f64); + range.upper = try readFloat(writer, f64); + + return range; + } + + fn readInt(reader: anytype, T: type) !T { + return try reader.readInt(T, file_endian); + } + + fn writeInt(writer: anytype, T: type, value: T) !void { + try writer.writeInt(T, value, file_endian); + } + + fn readId(reader: anytype) !Id { + const id_u32 = try readInt(reader, u32); + return Id.fromInt(id_u32); + } + + fn writeId(writer: anytype, value: Id) !void { + try writer.writeInt(u32, value.asInt(), file_endian); + } + + fn writeFloat(writer: anytype, T: type, value: T) !void { + const bytes = std.mem.asBytes(&value); + try writer.writeAll(bytes); + } + + fn readFloat(reader: anytype, T: type) !T { + var buff: [@sizeOf(T)]u8 = undefined; + try reader.readNoEof(&buff); + return std.mem.bytesToValue(T, &buff); + } + + fn writeString(writer: anytype, text: []const u8) !void { + try writeInt(writer, u32, @intCast(text.len)); + try writer.writeAll(text); + } + + 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. + const str_len = try readInt(reader, u32); + const buff = try allocator.alloc(u8, str_len); + errdefer allocator.free(buff); + try reader.readNoEof(buff); + return buff; } }; @@ -314,6 +541,7 @@ pub const Command = union(enum) { start_collection, stop_collection, save_project, + load_project, stop_output: Id, // Channel id start_output: Id, // Channel id add_file_from_picker @@ -390,11 +618,14 @@ pub fn init(self: *App, allocator: Allocator) !void { } pub fn deinit(self: *App) void { - self.stopCollection(); + self.deinitProject(); + + self.should_close = true; + self.collection_condition.signal(); + self.collection_thread.join(); self.ui.deinit(); self.main_screen.deinit(); - self.project.deinit(self.allocator); if (self.ni_daq) |*ni_daq| { ni_daq.deinit(self.allocator); @@ -405,9 +636,50 @@ pub fn deinit(self: *App) void { } } -pub fn loadProject(self: *App, project_file: []const u8) !void { - _ = self; - _ = project_file; +pub fn deinitProject(self: *App) void { + self.stopCollection(); + self.project.deinit(self.allocator); +} + +pub fn loadProject(self: *App) !void { + const save_location = self.project.save_location orelse return error.MissingSaveLocation; + + log.info("Load project from: {s}", .{save_location}); + + const loaded = try Project.initFromFile(self.allocator, save_location); + errdefer loaded.deinit(self.allocator); + + self.deinitProject(); + self.project = loaded; + + var file_iter = self.project.files.idIterator(); + while (file_iter.next()) |file_id| { + self.loadFile(file_id) catch |e| { + log.err("Failed to load file: {}", .{ e }); + }; + } + + var channel_iter = self.project.channels.idIterator(); + while (channel_iter.next()) |channel_id| { + self.loadChannel(channel_id) catch |e| { + log.err("Failed to load channel: {}", .{ e }); + }; + } + + var view_iter = self.project.views.idIterator(); + while (view_iter.next()) |view_id| { + self.loadView(view_id) catch |e| { + log.err("Failed to load view: {}", .{ e }); + }; + } +} + +pub fn saveProject(self: *App) !void { + const save_location = self.project.save_location orelse return error.MissingSaveLocation; + + log.info("Save project to: {s}", .{save_location}); + + try self.project.save(); } pub fn tick(self: *App) !void { @@ -415,6 +687,15 @@ pub fn tick(self: *App) !void { self.command_queue.len = 0; ui.pullOsEvents(); + + if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_s)) { + self.pushCommand(.save_project); + } + + if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_l)) { + self.pushCommand(.load_project); + } + { self.collection_samples_mutex.lock(); defer self.collection_samples_mutex.unlock(); @@ -447,10 +728,6 @@ pub fn tick(self: *App) !void { } } - if (ui.isCtrlDown() and ui.isKeyboardPressed(.key_s)) { - self.pushCommand(.save_project); - } - ui.begin(); defer ui.end(); @@ -473,7 +750,15 @@ pub fn tick(self: *App) !void { self.stopCollection(); }, .save_project => { - // TODO: + self.saveProject() catch |e| { + log.err("Failed to save project: {}", .{e}); + }; + }, + .load_project => { + self.loadProject() catch |e| { + log.err("Failed to load project: {}", .{e}); + utils.dumpErrorTrace(); + }; }, .add_file_from_picker => { self.addFileFromPicker() catch |e| { diff --git a/src/main.zig b/src/main.zig index a74ac9e..16607d7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -102,10 +102,6 @@ pub fn main() !void { defer app.deinit(); if (builtin.mode == .Debug) { - // const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); - // defer allocator.free(cwd_path); - // try app.loadProject(cwd_path); - _ = try app.addView(.{ .channel = try app.addChannel("Dev1/ai0") }); @@ -116,6 +112,14 @@ pub fn main() !void { .file = try app.addFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin") }); + var cwd_realpath_buff: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const cwd_realpath = try std.fs.cwd().realpath(".", &cwd_realpath_buff); + + const save_location = try std.fs.path.join(allocator, &.{ cwd_realpath, "project.proj" }); + errdefer allocator.free(allocator); + + app.project.save_location = save_location; + // try app.appendChannelFromDevice("Dev1/ai0"); // try app.appendChannelFromDevice("Dev3/ao0"); // try app.appendChannelFromFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin"); diff --git a/src/utils.zig b/src/utils.zig index 3037b4c..9e98718 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -76,4 +76,10 @@ pub fn initBoundedStringZ(comptime BoundedString: type, text: []const u8) !Bound pub fn getBoundedStringZ(bounded_array: anytype) [:0]const u8 { return bounded_array.buffer[0..(bounded_array.len-1) :0]; +} + +pub inline fn dumpErrorTrace() void { + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } } \ No newline at end of file