From bd54cba8a038857f2351423974e69ecc662ba514 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Tue, 22 Mar 2022 14:41:18 -0300 Subject: [PATCH] feat: implement SRAM saving and loading --- .gitmodules | 3 ++ build.zig | 3 ++ lib/known-folders | 1 + src/Bus.zig | 6 ++-- src/bus/GamePak.zig | 8 +++--- src/bus/backup.zig | 68 +++++++++++++++++++++++++++++++++++++++++++-- src/emu.zig | 4 +-- src/main.zig | 48 ++++++++++++++++++++++---------- src/util.zig | 47 +++++++++++++++++++++++++++++++ 9 files changed, 161 insertions(+), 27 deletions(-) create mode 160000 lib/known-folders diff --git a/.gitmodules b/.gitmodules index e266212..9dda538 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/zig-clap"] path = lib/zig-clap url = https://github.com/Hejsil/zig-clap +[submodule "lib/known-folders"] + path = lib/known-folders + url = https://github.com/ziglibs/known-folders diff --git a/build.zig b/build.zig index c8c275d..5cdd799 100644 --- a/build.zig +++ b/build.zig @@ -13,6 +13,9 @@ pub fn build(b: *std.build.Builder) void { const mode = b.standardReleaseOptions(); const exe = b.addExecutable("zba", "src/main.zig"); + + // Known Folders (%APPDATA%, XDG, etc.) + exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig"); // Bitfield type from FlorenceOS: https://github.com/FlorenceOS/ // exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } }); diff --git a/lib/known-folders b/lib/known-folders new file mode 160000 index 0000000..9db1b99 --- /dev/null +++ b/lib/known-folders @@ -0,0 +1 @@ +Subproject commit 9db1b99219c767d5e24994b1525273fe4031e464 diff --git a/src/Bus.zig b/src/Bus.zig index 2ba4c8f..72add6d 100644 --- a/src/Bus.zig +++ b/src/Bus.zig @@ -29,10 +29,10 @@ ewram: Ewram, io: Io, -pub fn init(alloc: Allocator, sched: *Scheduler, rom_path: []const u8, maybe_bios: ?[]const u8) !Self { +pub fn init(alloc: Allocator, sched: *Scheduler, rom_path: []const u8, bios_path: ?[]const u8, save_path: ?[]const u8) !Self { return Self{ - .pak = try GamePak.init(alloc, rom_path), - .bios = try Bios.init(alloc, maybe_bios), + .pak = try GamePak.init(alloc, rom_path, save_path), + .bios = try Bios.init(alloc, bios_path), .ppu = try Ppu.init(alloc, sched), .apu = Apu.init(), .iwram = try Iwram.init(alloc), diff --git a/src/bus/GamePak.zig b/src/bus/GamePak.zig index 7dbdc74..24383ae 100644 --- a/src/bus/GamePak.zig +++ b/src/bus/GamePak.zig @@ -10,8 +10,8 @@ buf: []u8, alloc: Allocator, backup: Backup, -pub fn init(alloc: Allocator, path: []const u8) !Self { - const file = try std.fs.cwd().openFile(path, .{}); +pub fn init(alloc: Allocator, rom_path: []const u8, save_path: ?[]const u8) !Self { + const file = try std.fs.cwd().openFile(rom_path, .{}); defer file.close(); const len = try file.getEndPos(); @@ -24,7 +24,7 @@ pub fn init(alloc: Allocator, path: []const u8) !Self { .buf = buf, .alloc = alloc, .title = title, - .backup = try Backup.init(alloc, kind), + .backup = try Backup.init(alloc, kind, title, save_path), }; pak.parseHeader(); @@ -40,7 +40,7 @@ fn parseHeader(self: *const Self) void { log.info("Title: {s}", .{title}); if (version != 0) log.info("Version: {}", .{version}); log.info("Game Code: {s}", .{code}); - if (lookupMaker(maker)) |c| log.info("Maker Code: {s}", .{c}) else log.info("Maker: {s}", .{maker}); + if (lookupMaker(maker)) |c| log.info("Maker: {s}", .{c}) else log.info("Maker Code: {s}", .{maker}); } fn parseTitle(buf: []u8) [12]u8 { diff --git a/src/bus/backup.zig b/src/bus/backup.zig index 80eac79..929f8eb 100644 --- a/src/bus/backup.zig +++ b/src/bus/backup.zig @@ -1,8 +1,9 @@ const std = @import("std"); - const Allocator = std.mem.Allocator; const log = std.log.scoped(.Backup); +const correctTitle = @import("../util.zig").correctTitle; + const backup_kinds = [5]Needle{ .{ .str = "EEPROM_V", .kind = .Eeprom }, .{ .str = "SRAM_V", .kind = .Sram }, @@ -18,7 +19,10 @@ pub const Backup = struct { alloc: Allocator, kind: BackupKind, - pub fn init(alloc: Allocator, kind: BackupKind) !Self { + title: [12]u8, + save_path: ?[]const u8, + + pub fn init(alloc: Allocator, kind: BackupKind, title: [12]u8, path: ?[]const u8) !Self { const buf_len: usize = switch (kind) { .Sram => 0x8000, // 32K .Flash => 0x10000, // 64K @@ -29,11 +33,16 @@ pub const Backup = struct { const buf = try alloc.alloc(u8, buf_len); std.mem.set(u8, buf, 0); - return Self{ + var backup = Self{ .buf = buf, .alloc = alloc, .kind = kind, + .title = title, + .save_path = path, }; + if (backup.save_path) |p| backup.loadSaveFromDisk(p) catch |e| log.err("Failed to load save: {}", .{e}); + + return backup; } pub fn guessKind(rom: []const u8) ?BackupKind { @@ -52,9 +61,58 @@ pub const Backup = struct { } pub fn deinit(self: Self) void { + if (self.save_path) |path| self.writeSaveToDisk(path) catch |e| log.err("Failed to save {}", .{e}); + self.alloc.free(self.buf); } + fn loadSaveFromDisk(self: *Self, path: []const u8) !void { + const file_path = try self.getSaveFilePath(path); + defer self.alloc.free(file_path); + + const file: std.fs.File = try std.fs.openFileAbsolute(file_path, .{}); + + const len = try file.getEndPos(); + const file_buf = try file.readToEndAlloc(self.alloc, len); + defer self.alloc.free(file_buf); + + switch (self.kind) { + .Sram => { + std.mem.copy(u8, self.buf, file_buf); + log.info("Loaded Save from {s}", .{file_path}); + }, + else => return SaveError.UnsupportedBackupKind, + } + } + + fn getSaveFilePath(self: *const Self, path: []const u8) ![]const u8 { + const filename = try self.getSaveFilename(); + defer self.alloc.free(filename); + + return try std.fs.path.join(self.alloc, &[_][]const u8{ path, filename }); + } + + fn getSaveFilename(self: *const Self) ![]const u8 { + const title = correctTitle(self.title); + return try std.mem.concat(self.alloc, u8, &[_][]const u8{ title, ".sav" }); + } + + fn writeSaveToDisk(self: Self, path: []const u8) !void { + const file_path = try self.getSaveFilePath(path); + defer self.alloc.free(file_path); + + const file = try std.fs.createFileAbsolute(file_path, .{}); + defer file.close(); + + switch (self.kind) { + .Sram => { + try file.writeAll(self.buf); + log.info("Dumped SRAM to {s}", .{file_path}); + }, + else => return SaveError.UnsupportedBackupKind, + } + } + pub fn get8(self: *const Self, idx: usize) u8 { // TODO: Implement Flash and EEPROM switch (self.kind) { @@ -96,3 +154,7 @@ const Needle = struct { }; } }; + +const SaveError = error{ + UnsupportedBackupKind, +}; diff --git a/src/emu.zig b/src/emu.zig index b5b5b61..2ff3990 100644 --- a/src/emu.zig +++ b/src/emu.zig @@ -11,7 +11,7 @@ const Atomic = std.atomic.Atomic; // 228 Lines which consist of 308 dots (which are 4 cycles long) const cycles_per_frame: u64 = 228 * (308 * 4); //280896 -const clock_rate: u64 = 1 << 24; // 16.78MHz +const clock_rate: u64 = 1 << 24; // 16.78MHz // TODO: Don't truncate this, be more accurate w/ timing // 59.6046447754ns (truncated to just 59ns) @@ -19,7 +19,7 @@ const clock_period: u64 = std.time.ns_per_s / clock_rate; const frame_period = (clock_period * cycles_per_frame); // 59.7275005696Hz -pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) / +pub const frame_rate = @intToFloat(f64, std.time.ns_per_s) / ((@intToFloat(f64, std.time.ns_per_s) / @intToFloat(f64, clock_rate)) * @intToFloat(f64, cycles_per_frame)); const log = std.log.scoped(.Emulation); diff --git a/src/main.zig b/src/main.zig index 8a723f6..32ce529 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); const SDL = @import("sdl2"); const clap = @import("clap"); +const known_folders = @import("known_folders"); const emu = @import("emu.zig"); const Bus = @import("Bus.zig"); @@ -21,6 +22,9 @@ const expected_rate = @import("emu.zig").frame_rate; pub const enable_logging: bool = false; const is_binary: bool = false; +const log = std.log.scoped(.GUI); + +const correctTitle = @import("util.zig").correctTitle; pub fn main() anyerror!void { // Allocator for Emulator + CLI @@ -40,7 +44,7 @@ pub fn main() anyerror!void { if (args.flag("--help")) return clap.help(std.io.getStdErr().writer(), ¶ms); - const maybe_bios: ?[]const u8 = if (args.option("--bios")) |p| p else null; + const bios_path: ?[]const u8 = if (args.option("--bios")) |p| p else null; const positionals = args.positionals(); const stderr = std.io.getStdErr(); @@ -58,11 +62,16 @@ pub fn main() anyerror!void { }, }; + // Determine Save Directory + const save_path = try setupSavePath(alloc); + defer if (save_path) |path| alloc.free(path); + log.info("Save Path: {s}", .{save_path}); + // Initialize Emulator var scheduler = Scheduler.init(alloc); defer scheduler.deinit(); - var bus = try Bus.init(alloc, &scheduler, rom_path, maybe_bios); + var bus = try Bus.init(alloc, &scheduler, rom_path, bios_path, save_path); defer bus.deinit(); var cpu = Arm7tdmi.init(&scheduler, &bus); @@ -88,12 +97,13 @@ pub fn main() anyerror!void { if (status < 0) sdlPanic(); defer SDL.SDL_Quit(); + const title = correctTitle(bus.pak.title); + var title_buf: [0x20]u8 = std.mem.zeroes([0x20]u8); - var title = try std.fmt.bufPrint(&title_buf, "ZBA | {s}", .{bus.pak.title}); - correctTitleSlice(&title); + const window_title = try std.fmt.bufPrint(&title_buf, "ZBA | {s}", .{title}); var window = SDL.SDL_CreateWindow( - title.ptr, + window_title.ptr, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, gba_width * window_scale, @@ -163,7 +173,7 @@ pub fn main() anyerror!void { SDL.SDL_RenderPresent(renderer); const actual = emu_rate.calc(); - const dyn_title = std.fmt.bufPrint(&dyn_title_buf, "{s} [Emu: {d:0>3.2}fps, {d:0>3.2}%] ", .{ title, actual, actual * 100 / expected_rate }) catch unreachable; + const dyn_title = std.fmt.bufPrint(&dyn_title_buf, "{s} [Emu: {d:0>3.2}fps, {d:0>3.2}%] ", .{ window_title, actual, actual * 100 / expected_rate }) catch unreachable; SDL.SDL_SetWindowTitle(window, dyn_title.ptr); } @@ -180,14 +190,22 @@ const CliError = error{ UnneededOptions, }; -/// The slice considers some null values to be a part of the string -/// so change the length of the slice so that isn't the case -// FIXME: This is awful and bad -fn correctTitleSlice(title: *[]u8) void { - for (title.*) |char, i| { - if (char == 0) { - title.len = i; - break; - } +// FIXME: Superfluous allocations? +fn setupSavePath(alloc: std.mem.Allocator) !?[]const u8 { + const save_subpath = try std.fs.path.join(alloc, &[_][]const u8{ "zba", "save" }); + defer alloc.free(save_subpath); + + const maybe_data_path = try known_folders.getPath(alloc, .data); + defer if (maybe_data_path) |path| alloc.free(path); + + const save_path = if (maybe_data_path) |base| try std.fs.path.join(alloc, &[_][]const u8{ base, save_subpath }) else null; + + if (save_path) |_| { + // If we've determined what our save path should be, ensure the prereq directories + // are present so that we can successfully write to the path when necessary + const maybe_data_dir = try known_folders.open(alloc, .data, .{}); + if (maybe_data_dir) |data_dir| try data_dir.makePath(save_subpath); } + + return save_path; } diff --git a/src/util.zig b/src/util.zig index 1f3e260..f8e4bcb 100644 --- a/src/util.zig +++ b/src/util.zig @@ -53,3 +53,50 @@ pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 { return result; } + +/// The Title from the GBA Cartridge may be null padded to a maximum +/// length of 12 bytes. +/// +/// This function returns a slice of everything just before the first +/// `\0` +pub fn correctTitle(title: [12]u8) []const u8 { + var len = title.len; + for (title) |char, i| { + if (char == 0) { + len = i; + break; + } + } + + return title[0..len]; +} + +/// Copies a Title and returns either an identical or similar +/// array consisting of ASCII that won't make any file system angry +/// +/// Currently Unused but I assume there's some ROM out there that will require this +pub fn safeTitle(title: [12]u8) [12]u8 { + var result: [12]u8 = title; + + for (result) |*char| { + if (char.* == '-') char.* = '_'; + if (char.* == 0) break; + } + + return result; +} + +pub fn fixTitle(alloc: std.mem.Allocator, title: [12]u8) ![]u8 { + var len: usize = 12; + for (title) |char, i| { + if (char == 0) { + len = i; + break; + } + } + + const buf = try alloc.alloc(u8, len); + std.mem.copy(u8, buf, title[0..len]); + + return buf; +}