diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..e7d1f92 --- /dev/null +++ b/src/config.zig @@ -0,0 +1,74 @@ +const std = @import("std"); +const toml = @import("toml"); + +const Allocator = std.mem.Allocator; + +var state: Config = .{}; + +const Config = struct { + host: Host = .{}, + guest: Guest = .{}, + debug: Debug = .{}, + + /// Settings related to the Computer the Emulator is being run on + const Host = struct { + /// Using Nearest-Neighbor, multiply the resolution of the GBA Window + win_scale: i64 = 3, + /// Enable Vsync + /// + /// Note: This does not affect whether Emulation is synced to 59Hz + vsync: bool = true, + }; + + // Settings realted to the emulation itself + const Guest = struct { + /// Whether Emulation thread to sync to Audio Callbacks + audio_sync: bool = true, + /// Whether Emulation thread should sync to 59Hz + video_sync: bool = true, + /// Whether RTC I/O should always be enabled + force_rtc: bool = false, + }; + + /// Settings related to debugging ZBA + const Debug = struct { + /// Enable CPU Trace logs + cpu_trace: bool = false, + /// If false and ZBA is build in debug mode, ZBA will panic on unhandled I/O + unhandled_io: bool = true, + }; +}; + +pub fn config() *const Config { + return &state; +} + +/// Reads a config file and then loads it into the global state +pub fn load(allocator: Allocator, config_path: []const u8) !void { + var config_file = try std.fs.cwd().openFile(config_path, .{}); + defer config_file.close(); + + const contents = try config_file.readToEndAlloc(allocator, try config_file.getEndPos()); + defer allocator.free(contents); + + const table = try toml.parseContents(allocator, contents, null); + defer table.deinit(); + + // TODO: Report unknown config options + + if (table.keys.get("Host")) |host| { + if (host.Table.keys.get("win_scale")) |scale| state.host.win_scale = scale.Integer; + if (host.Table.keys.get("vsync")) |vsync| state.host.vsync = vsync.Boolean; + } + + if (table.keys.get("Guest")) |guest| { + if (guest.Table.keys.get("audio_sync")) |sync| state.guest.audio_sync = sync.Boolean; + if (guest.Table.keys.get("video_sync")) |sync| state.guest.video_sync = sync.Boolean; + if (guest.Table.keys.get("force_rtc")) |forced| state.guest.force_rtc = forced.Boolean; + } + + if (table.keys.get("Debug")) |debug| { + if (debug.Table.keys.get("cpu_trace")) |trace| state.debug.cpu_trace = trace.Boolean; + if (debug.Table.keys.get("unhandled_io")) |unhandled| state.debug.unhandled_io = unhandled.Boolean; + } +} diff --git a/src/core/bus/GamePak.zig b/src/core/bus/GamePak.zig index fdc98be..6b8ee54 100644 --- a/src/core/bus/GamePak.zig +++ b/src/core/bus/GamePak.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const config = @import("../../config.zig"); + const Bit = @import("bitfield").Bit; const Bitfield = @import("bitfield").Bitfield; const DateTime = @import("datetime").datetime.Datetime; @@ -8,7 +10,6 @@ const Backup = @import("backup.zig").Backup; const Gpio = @import("gpio.zig").Gpio; const Allocator = std.mem.Allocator; -const force_rtc = @import("../emu.zig").force_rtc; const log = std.log.scoped(.GamePak); const Self = @This(); @@ -190,7 +191,7 @@ pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_pat const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos()); const title = file_buf[0xA0..0xAC].*; const kind = Backup.guess(file_buf); - const device = if (force_rtc) .Rtc else guessDevice(file_buf); + const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf); logHeader(file_buf, &title); diff --git a/src/core/cpu.zig b/src/core/cpu.zig index 3154be2..80d0a1b 100644 --- a/src/core/cpu.zig +++ b/src/core/cpu.zig @@ -236,7 +236,6 @@ pub const thumb = struct { } }; -const cpu_logging = @import("emu.zig").cpu_logging; const log = std.log.scoped(.Arm7Tdmi); pub const Arm7tdmi = struct { @@ -428,12 +427,12 @@ pub const Arm7tdmi = struct { pub fn step(self: *Self) void { if (self.cpsr.t.read()) { const opcode = self.fetch(u16); - if (cpu_logging) self.logger.?.mgbaLog(self, opcode); + if (self.logger) |*trace| trace.mgbaLog(self, opcode); thumb.lut[thumb.idx(opcode)](self, self.bus, opcode); } else { const opcode = self.fetch(u32); - if (cpu_logging) self.logger.?.mgbaLog(self, opcode); + if (self.logger) |*trace| trace.mgbaLog(self, opcode); if (checkCond(self.cpsr, @truncate(u4, opcode >> 28))) { arm.lut[arm.idx(opcode)](self, self.bus, opcode); diff --git a/src/core/emu.zig b/src/core/emu.zig index 81ca1fc..3bd516b 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -1,5 +1,6 @@ const std = @import("std"); const SDL = @import("sdl2"); +const config = @import("../config.zig"); const Bus = @import("Bus.zig"); const Scheduler = @import("scheduler.zig").Scheduler; @@ -12,14 +13,6 @@ const Thread = std.Thread; const Atomic = std.atomic.Atomic; const Allocator = std.mem.Allocator; -// TODO: Move these to a TOML File -const sync_audio = false; // Enable Audio Sync -const sync_video: RunKind = .LimitedFPS; // Configure Video Sync -pub const win_scale = 4; // 1x, 2x, 3x, etc. Window Scaling -pub const cpu_logging = false; // Enable detailed CPU logging -pub const allow_unhandled_io = true; // Only relevant in Debug Builds -pub const force_rtc = false; - // 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 @@ -40,18 +33,20 @@ const RunKind = enum { UnlimitedFPS, Limited, LimitedFPS, - LimitedBusy, }; pub fn run(quit: *Atomic(bool), fps: *FpsTracker, sched: *Scheduler, cpu: *Arm7tdmi) void { - if (sync_audio) log.info("Audio sync enabled", .{}); + if (config.config().guest.audio_sync) log.info("Audio sync enabled", .{}); - switch (sync_video) { - .Unlimited => runUnsynchronized(quit, sched, cpu, null), - .Limited => runSynchronized(quit, sched, cpu, null), - .UnlimitedFPS => runUnsynchronized(quit, sched, cpu, fps), - .LimitedFPS => runSynchronized(quit, sched, cpu, fps), - .LimitedBusy => runBusyLoop(quit, sched, cpu), + // TODO: Support the other modes? + const video_sync: RunKind = if (config.config().guest.video_sync) .LimitedFPS else .UnlimitedFPS; + const audio_sync = config.config().guest.audio_sync; + + switch (video_sync) { + .Unlimited => runUnsynchronized(audio_sync, quit, sched, cpu, null), + .Limited => runSynchronized(audio_sync, quit, sched, cpu, null), + .UnlimitedFPS => runUnsynchronized(audio_sync, quit, sched, cpu, fps), + .LimitedFPS => runSynchronized(audio_sync, quit, sched, cpu, fps), } } @@ -72,7 +67,7 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi) void { } } -fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void { +fn syncToAudio(audio_sync: bool, stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void { const sample_size = 2 * @sizeOf(u16); const max_buf_size: c_int = 0x400; @@ -85,11 +80,11 @@ fn syncToAudio(stream: *SDL.SDL_AudioStream, is_buffer_full: *bool) void { while (true) { still_full = SDL.SDL_AudioStreamAvailable(stream) > sample_size * max_buf_size >> 1; - if (!sync_audio or !still_full) break; + if (!audio_sync or !still_full) break; } } -pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void { +pub fn runUnsynchronized(audio_sync: bool, quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void { log.info("Emulation thread w/out video sync", .{}); if (fps) |tracker| { @@ -97,19 +92,19 @@ pub fn runUnsynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, while (!quit.load(.SeqCst)) { runFrame(sched, cpu); - syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); + syncToAudio(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); tracker.tick(); } } else { while (!quit.load(.SeqCst)) { runFrame(sched, cpu); - syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); + syncToAudio(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); } } } -pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void { +pub fn runSynchronized(audio_sync: bool, quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, fps: ?*FpsTracker) void { log.info("Emulation thread w/ video sync", .{}); var timer = Timer.start() catch std.debug.panic("Failed to initialize std.timer.Timer", .{}); var wake_time: u64 = frame_period; @@ -125,8 +120,8 @@ pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, f // If we happen to also be syncing to audio, we choose to spin on // the amount of time needed for audio to catch up rather than // our expected wake-up time - syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); - if (!sync_audio) spinLoop(&timer, wake_time); + syncToAudio(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); + if (!audio_sync) spinLoop(&timer, wake_time); wake_time = new_wake_time; tracker.tick(); @@ -137,8 +132,8 @@ pub fn runSynchronized(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi, f const new_wake_time = blockOnVideo(&timer, wake_time); // see above comment - syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); - if (!sync_audio) spinLoop(&timer, wake_time); + syncToAudio(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); + if (!audio_sync) spinLoop(&timer, wake_time); wake_time = new_wake_time; } } @@ -153,22 +148,6 @@ inline fn blockOnVideo(timer: *Timer, wake_time: u64) u64 { return if (maybe_recalc_wake_time) |recalc| recalc else wake_time + frame_period; } -pub fn runBusyLoop(quit: *Atomic(bool), sched: *Scheduler, cpu: *Arm7tdmi) void { - log.info("Emulation thread with video sync using busy loop", .{}); - var timer = Timer.start() catch unreachable; - var wake_time: u64 = frame_period; - - while (!quit.load(.SeqCst)) { - runFrame(sched, cpu); - spinLoop(&timer, wake_time); - - syncToAudio(cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); - - // Update to the new wake time - wake_time += frame_period; - } -} - fn sleep(timer: *Timer, wake_time: u64) ?u64 { // const step = std.time.ns_per_ms * 10; // 10ms const timestamp = timer.read(); diff --git a/src/main.zig b/src/main.zig index 44c0052..a2c5ac0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,7 +2,8 @@ const std = @import("std"); const builtin = @import("builtin"); const known_folders = @import("known_folders"); const clap = @import("clap"); -const toml = @import("toml"); + +const config = @import("config.zig"); const Gui = @import("platform.zig").Gui; const Bus = @import("core/Bus.zig"); @@ -14,11 +15,8 @@ const Allocator = std.mem.Allocator; const log = std.log.scoped(.Cli); const width = @import("core/ppu.zig").width; const height = @import("core/ppu.zig").height; -const cpu_logging = @import("core/emu.zig").cpu_logging; pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level; -// TODO: Reimpl Logging - // CLI Arguments + Help Text const params = clap.parseParamsComptime( \\-h, --help Display this help and exit. @@ -43,10 +41,7 @@ pub fn main() anyerror!void { const save_path = try savePath(allocator, data_path); defer allocator.free(save_path); - const config = try loadConfig(allocator, config_path); - _ = config; - - log.debug("got past loadConfig()", .{}); + try config.load(allocator, config_path); // Handle CLI Input const result = try clap.parse(clap.Help, ¶ms, clap.parsers.default, .{}); @@ -55,7 +50,8 @@ pub fn main() anyerror!void { const paths = try handleArguments(allocator, data_path, &result); defer if (paths.save) |path| allocator.free(path); - const log_file: ?std.fs.File = if (cpu_logging) try std.fs.cwd().createFile("zba.log", .{}) else null; + const cpu_trace = config.config().debug.cpu_trace; + const log_file: ?std.fs.File = if (cpu_trace) try std.fs.cwd().createFile("zba.log", .{}) else null; defer if (log_file) |file| file.close(); // TODO: Take Emulator Init Code out of main.zig @@ -92,23 +88,6 @@ pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *con }; } -const Config = struct { - // TODO: Add Config Fields - // TODO: Move to it's own file? config.zig? -}; - -// FIXME: TOML Library Leaks memory here -// Either I should improve the TOML library, or try some of the even less used ones :thinking -fn loadConfig(allocator: Allocator, config_path: []const u8) !Config { - // _ = allocator; - // _ = config_path; - - const table = try toml.parseFile(allocator, config_path, null); - table.deinit(); - - return .{}; -} - fn configFilePath(allocator: Allocator, data_path: []const u8) ![]const u8 { const path = try std.fs.path.join(allocator, &[_][]const u8{ data_path, "zba", "config.toml" }); diff --git a/src/platform.zig b/src/platform.zig index 319aea3..5f9d663 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -1,6 +1,7 @@ const std = @import("std"); const SDL = @import("sdl2"); const emu = @import("core/emu.zig"); +const config = @import("config.zig"); const Apu = @import("core/apu.zig").Apu; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; @@ -10,8 +11,6 @@ const FpsTracker = @import("util.zig").FpsTracker; const span = @import("util.zig").span; const pitch = @import("core/ppu.zig").framebuf_pitch; -const scale = @import("core/emu.zig").win_scale; - const default_title: []const u8 = "ZBA"; pub const Gui = struct { @@ -28,16 +27,19 @@ pub const Gui = struct { const ret = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER); if (ret < 0) panic(); + const win_scale = @intCast(c_int, config.config().host.win_scale); + const window = SDL.SDL_CreateWindow( default_title.ptr, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, - @as(c_int, width * scale), - @as(c_int, height * scale), + @as(c_int, width * win_scale), + @as(c_int, height * win_scale), SDL.SDL_WINDOW_SHOWN, ) orelse panic(); - const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic(); + const renderer_flags = SDL.SDL_RENDERER_ACCELERATED | if (config.config().host.vsync) SDL.SDL_RENDERER_PRESENTVSYNC else 0; + const renderer = SDL.SDL_CreateRenderer(window, -1, @bitCast(u32, renderer_flags)) orelse panic(); const texture = SDL.SDL_CreateTexture( renderer, diff --git a/src/util.zig b/src/util.zig index 848828f..2275451 100644 --- a/src/util.zig +++ b/src/util.zig @@ -1,10 +1,10 @@ const std = @import("std"); const builtin = @import("builtin"); +const config = @import("config.zig"); + const Log2Int = std.math.Log2Int; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; -const allow_unhandled_io = @import("core/emu.zig").allow_unhandled_io; - // Sign-Extend value of type `T` to type `U` pub fn sext(comptime T: type, comptime U: type, value: T) T { // U must have less bits than T @@ -144,8 +144,10 @@ pub const io = struct { } pub fn undef(comptime T: type, log: anytype, comptime format: []const u8, args: anytype) ?T { + const unhandled_io = config.config().debug.unhandled_io; + log.warn(format, args); - if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{}); + if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{}); return null; } @@ -153,8 +155,10 @@ pub const io = struct { pub const write = struct { pub fn undef(log: anytype, comptime format: []const u8, args: anytype) void { + const unhandled_io = config.config().debug.unhandled_io; + log.warn(format, args); - if (builtin.mode == .Debug and !allow_unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{}); + if (builtin.mode == .Debug and !unhandled_io) std.debug.panic("TODO: Implement I/O Register", .{}); } }; };