Compare commits
	
		
			25 Commits
		
	
	
		
			9a58c248f3
			...
			window
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3b13102abb | |||
| 7234ecab37 | |||
| ddf4599162 | |||
| 01f5410180 | |||
| 49706842af | |||
| 2798a90d83 | |||
| 518b868249 | |||
| 755115660b | |||
| 6709f8c551 | |||
| 1f3cdd9513 | |||
| 65af6aa499 | |||
| 024151a5c1 | |||
| e380af7056 | |||
| e654abfd1d | |||
| 3510a6cff8 | |||
| 3fb351e762 | |||
| a11b96b84e | |||
| c3be1c0a67 | |||
| fdf7399e52 | |||
| ed8155139a | |||
| 8112b1aab2 | |||
| c0e583d20d | |||
| 3f72367aaf | |||
| c27f487bf0 | |||
| ae3bb94036 | 
							
								
								
									
										9
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,8 @@ jobs: | ||||
|   build: | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [ubuntu-latest, windows-latest, macos-latest] | ||||
|         # os: [ubuntu-latest, windows-latest, macos-latest] | ||||
|         os: [ubuntu-latest, windows-latest] | ||||
|     runs-on: ${{matrix.os}} | ||||
|     steps: | ||||
|       - uses: goto-bus-stop/setup-zig@v2 | ||||
| @@ -37,9 +38,9 @@ jobs: | ||||
|             brew install sdl2 | ||||
|       - uses: actions/checkout@v3 | ||||
|         with: | ||||
|           submodules: true | ||||
|           submodules: recursive | ||||
|       - name: build  | ||||
|         run: zig build -Drelease-safe | ||||
|         run: zig build -Doptimize=ReleaseSafe -Dcpu=baseline | ||||
|       - name: upload | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
| @@ -50,7 +51,7 @@ jobs: | ||||
|     steps:  | ||||
|       - uses: actions/checkout@v3 | ||||
|         with: | ||||
|           submodules: true | ||||
|           submodules: recursive | ||||
|       - uses: goto-bus-stop/setup-zig@v2 | ||||
|         with: | ||||
|           version: master | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @@ -13,3 +13,6 @@ | ||||
| [submodule "lib/zig-toml"] | ||||
| 	path = lib/zig-toml | ||||
| 	url = https://github.com/aeronavery/zig-toml | ||||
| [submodule "lib/zba-gdbstub"] | ||||
| 	path = lib/zba-gdbstub | ||||
| 	url = https://git.musuka.dev/paoda/zba-gdbstub | ||||
|   | ||||
							
								
								
									
										8
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +0,0 @@ | ||||
| { | ||||
|     "recommendations": [ | ||||
|         "augusterame.zls-vscode", | ||||
|         "usernamehw.errorlens", | ||||
|         "vadimcn.vscode-lldb", | ||||
|         "dan-c-underwood.arm" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							| @@ -13,7 +13,7 @@ This is a simple (read: incomplete) for-fun long-term project. I hope to get "mo | ||||
| - [x] Affine Sprites | ||||
| - [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window)) | ||||
| - [ ] Audio Resampler (Having issues with SDL2's) | ||||
| - [ ] Immediate Mode GUI | ||||
| - [x] Immediate Mode GUI (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/imgui)) | ||||
| - [ ] Refactoring for easy-ish perf boosts | ||||
|  | ||||
| ## Usage | ||||
| @@ -77,7 +77,7 @@ arm7wrestler GBA Fixed | [destoer](https://github.com/destoer) | ||||
|  | ||||
| ## Compiling | ||||
|  | ||||
| Most recently built on Zig [v0.11.0-dev.987+a1d82352d](https://github.com/ziglang/zig/tree/a1d82352d) | ||||
| Most recently built on Zig [v0.11.0-dev.1580+a5b34a61a](https://github.com/ziglang/zig/tree/a5b34a61a) | ||||
|  | ||||
| ### Dependencies | ||||
|  | ||||
| @@ -91,17 +91,17 @@ zig-datetime | <https://github.com/frmdstryr/zig-datetime> | ||||
| `bitfields.zig` | [https://github.com/FlorenceOS/Florence](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig) | ||||
| `gl.zig` | <https://github.com/MasterQ32/zig-opengl> | ||||
|  | ||||
| Use `git submodule update --init` from the project root to pull the git submodules `SDL.zig`, `zig-clap`, `known-folders`, `zig-toml` and `zig-datetime` | ||||
| Use `git submodule update --init` from the project root to pull the git relevant git submodules | ||||
|  | ||||
| Be sure to provide SDL2 using: | ||||
|  | ||||
| - Linux: Your distro's package manager | ||||
| - MacOS: ¯\\\_(ツ)_/¯ | ||||
| - macOS: ¯\\\_(ツ)_/¯ (try [this formula](https://formulae.brew.sh/formula/sdl2)?) | ||||
| - Windows: [`vcpkg`](https://github.com/Microsoft/vcpkg) (install `sdl2:x64-windows`) | ||||
|  | ||||
| `SDL.zig` will provide a helpful compile error if the zig compiler is unable to find SDL2. | ||||
|  | ||||
| Once you've got all the dependencies, execute `zig build -Drelease-fast`. The executable is located at `zig-out/bin/`. | ||||
| Once you've got all the dependencies, execute `zig build -Doptimize=ReleaseSafe`. The executable is located at `zig-out/bin/`. | ||||
|  | ||||
| ## Controls | ||||
|  | ||||
|   | ||||
							
								
								
									
										49
									
								
								build.zig
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								build.zig
									
									
									
									
									
								
							| @@ -1,54 +1,55 @@ | ||||
| const std = @import("std"); | ||||
| const builtin = @import("builtin"); | ||||
|  | ||||
| const Sdk = @import("lib/SDL.zig/Sdk.zig"); | ||||
| const Gdbstub = @import("lib/zba-gdbstub/build.zig"); | ||||
|  | ||||
| pub fn build(b: *std.build.Builder) void { | ||||
|     // Minimum Zig Version | ||||
|     const min_ver = std.SemanticVersion.parse("0.11.0-dev.987+a1d82352d") catch return; // https://github.com/ziglang/zig/commit/19056cb68 | ||||
|     const min_ver = std.SemanticVersion.parse("0.11.0-dev.1580+a5b34a61a") catch return; // https://github.com/ziglang/zig/commit/a5b34a61a | ||||
|     if (builtin.zig_version.order(min_ver).compare(.lt)) { | ||||
|         std.log.err("{s}", .{b.fmt("Zig v{} does not meet the minimum version requirement. (Zig v{})", .{ builtin.zig_version, min_ver })}); | ||||
|         std.os.exit(1); | ||||
|     } | ||||
|  | ||||
|     // Standard target options allows the person running `zig build` to choose | ||||
|     // what target to build for. Here we do not override the defaults, which | ||||
|     // means any target is allowed, and the default is native. Other options | ||||
|     // for restricting supported target set are available. | ||||
|     const target = b.standardTargetOptions(.{}); | ||||
|     const optimize = b.standardOptimizeOption(.{}); | ||||
|  | ||||
|     // Standard release options allow the person running `zig build` to select | ||||
|     // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. | ||||
|     const mode = b.standardReleaseOptions(); | ||||
|     const exe = b.addExecutable(.{ | ||||
|         .name = "zba", | ||||
|         .root_source_file = .{ .path = "src/main.zig" }, | ||||
|         .target = target, | ||||
|         .optimize = optimize, | ||||
|     }); | ||||
|  | ||||
|     const exe = b.addExecutable("zba", "src/main.zig"); | ||||
|     exe.setMainPkgPath("."); // Necessary so that src/main.zig can embed example.toml | ||||
|     exe.setTarget(target); | ||||
|  | ||||
|     // Known Folders (%APPDATA%, XDG, etc.) | ||||
|     exe.addPackagePath("known_folders", "lib/known-folders/known-folders.zig"); | ||||
|     exe.addAnonymousModule("known_folders", .{ .source_file = .{ .path = "lib/known-folders/known-folders.zig" } }); | ||||
|  | ||||
|     // DateTime Library | ||||
|     exe.addPackagePath("datetime", "lib/zig-datetime/src/main.zig"); | ||||
|     exe.addAnonymousModule("datetime", .{ .source_file = .{ .path = "lib/zig-datetime/src/main.zig" } }); | ||||
|  | ||||
|     // Bitfield type from FlorenceOS: https://github.com/FlorenceOS/ | ||||
|     // exe.addPackage(.{ .name = "bitfield", .path = .{ .path = "lib/util/bitfield.zig" } }); | ||||
|     exe.addPackagePath("bitfield", "lib/util/bitfield.zig"); | ||||
|     exe.addAnonymousModule("bitfield", .{ .source_file = .{ .path = "lib/util/bitfield.zig" } }); | ||||
|  | ||||
|     // Argument Parsing Library | ||||
|     exe.addPackagePath("clap", "lib/zig-clap/clap.zig"); | ||||
|     exe.addAnonymousModule("clap", .{ .source_file = .{ .path = "lib/zig-clap/clap.zig" } }); | ||||
|  | ||||
|     // TOML Library | ||||
|     exe.addPackagePath("toml", "lib/zig-toml/src/toml.zig"); | ||||
|     exe.addAnonymousModule("toml", .{ .source_file = .{ .path = "lib/zig-toml/src/toml.zig" } }); | ||||
|  | ||||
|     // OpenGL 3.3 Bindings | ||||
|     exe.addPackagePath("gl", "lib/gl.zig"); | ||||
|     exe.addAnonymousModule("gl", .{ .source_file = .{ .path = "lib/gl.zig" } }); | ||||
|  | ||||
|     // gdbstub | ||||
|     Gdbstub.link(exe); | ||||
|  | ||||
|     // Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig | ||||
|     const sdk = Sdk.init(b); | ||||
|     const sdk = Sdk.init(b, null); | ||||
|     sdk.link(exe, .dynamic); | ||||
|     exe.addPackage(sdk.getNativePackage("sdl2")); | ||||
|     exe.addModule("sdl2", sdk.getNativeModule()); | ||||
|  | ||||
|     exe.setBuildMode(mode); | ||||
|     exe.install(); | ||||
|  | ||||
|     const run_cmd = exe.run(); | ||||
| @@ -60,9 +61,11 @@ pub fn build(b: *std.build.Builder) void { | ||||
|     const run_step = b.step("run", "Run the app"); | ||||
|     run_step.dependOn(&run_cmd.step); | ||||
|  | ||||
|     const exe_tests = b.addTest("src/main.zig"); | ||||
|     exe_tests.setTarget(target); | ||||
|     exe_tests.setBuildMode(mode); | ||||
|     const exe_tests = b.addTest(.{ | ||||
|         .root_source_file = .{ .path = "src/main.zig" }, | ||||
|         .target = target, | ||||
|         .optimize = optimize, | ||||
|     }); | ||||
|  | ||||
|     const test_step = b.step("test", "Run unit tests"); | ||||
|     test_step.dependOn(&exe_tests.step); | ||||
|   | ||||
 Submodule lib/SDL.zig updated: 2fbd4b2285...59c90e8ea2
									
								
							 Submodule lib/known-folders updated: 24845b0103...53fe3b676f
									
								
							
							
								
								
									
										1
									
								
								lib/zba-gdbstub
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								lib/zba-gdbstub
									
									
									
									
									
										Submodule
									
								
							 Submodule lib/zba-gdbstub added at acb59994fc
									
								
							 Submodule lib/zig-clap updated: 88edafd00e...861de651f3
									
								
							 Submodule lib/zig-datetime updated: 932d284521...bf0ae0c27c
									
								
							
							
								
								
									
										152
									
								
								src/core/Bus.zig
									
									
									
									
									
								
							
							
						
						
									
										152
									
								
								src/core/Bus.zig
									
									
									
									
									
								
							| @@ -105,7 +105,7 @@ pub fn deinit(self: *Self) void { | ||||
| fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void { | ||||
|     const vramMirror = @import("ppu/Vram.zig").mirror; | ||||
|  | ||||
|     for (table) |*ptr, i| { | ||||
|     for (table, 0..) |*ptr, i| { | ||||
|         const addr = @intCast(u32, page_size * i); | ||||
|  | ||||
|         ptr.* = switch (addr) { | ||||
| @@ -132,7 +132,7 @@ fn fillWriteTable(self: *Self, comptime T: type, table: *[table_len]?*const anyo | ||||
|     comptime std.debug.assert(T == u32 or T == u16 or T == u8); | ||||
|     const vramMirror = @import("ppu/Vram.zig").mirror; | ||||
|  | ||||
|     for (table) |*ptr, i| { | ||||
|     for (table, 0..) |*ptr, i| { | ||||
|         const addr = @intCast(u32, page_size * i); | ||||
|  | ||||
|         ptr.* = switch (addr) { | ||||
| @@ -198,55 +198,6 @@ fn fillReadTableExternal(self: *Self, addr: u32) ?*anyopaque { | ||||
|     return &self.pak.buf[masked_addr]; | ||||
| } | ||||
|  | ||||
| pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T { | ||||
|     const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits; | ||||
|     const page = unaligned_address >> bits; | ||||
|     const offset = unaligned_address & (page_size - 1); | ||||
|  | ||||
|     // We're doing some serious out-of-bounds open-bus reads | ||||
|     if (page >= table_len) return self.openBus(T, unaligned_address); | ||||
|  | ||||
|     if (self.read_table[page]) |some_ptr| { | ||||
|         // We have a pointer to a page, cast the pointer to it's underlying type | ||||
|         const Ptr = [*]const T; | ||||
|         const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr)); | ||||
|  | ||||
|         // Note: We don't check array length, since we force align the | ||||
|         // lower bits of the address as the GBA would | ||||
|         return ptr[forceAlign(T, offset) / @sizeOf(T)]; | ||||
|     } | ||||
|  | ||||
|     return self.dbgSlowRead(T, unaligned_address); | ||||
| } | ||||
|  | ||||
| fn dbgSlowRead(self: *const Self, comptime T: type, unaligned_address: u32) T { | ||||
|     const page = @truncate(u8, unaligned_address >> 24); | ||||
|     const address = forceAlign(T, unaligned_address); | ||||
|  | ||||
|     return switch (page) { | ||||
|         // General Internal Memory | ||||
|         0x00 => blk: { | ||||
|             if (address < Bios.size) | ||||
|                 break :blk self.bios.dbgRead(T, self.cpu.r[15], unaligned_address); | ||||
|  | ||||
|             break :blk self.openBus(T, address); | ||||
|         }, | ||||
|         0x02 => unreachable, // handled by fastmem | ||||
|         0x03 => unreachable, // handled by fastmem | ||||
|         0x04 => self.readIo(T, address), | ||||
|  | ||||
|         // Internal Display Memory | ||||
|         0x05 => unreachable, // handled by fastmem | ||||
|         0x06 => unreachable, // handled by fastmem | ||||
|         0x07 => unreachable, // handled by fastmem | ||||
|  | ||||
|         // External Memory (Game Pak) | ||||
|         0x08...0x0D => self.pak.dbgRead(T, address), | ||||
|         0x0E...0x0F => self.readBackup(T, unaligned_address), | ||||
|         else => self.openBus(T, address), | ||||
|     }; | ||||
| } | ||||
|  | ||||
| fn readIo(self: *const Self, comptime T: type, address: u32) T { | ||||
|     return io.read(self, T, address) orelse self.openBus(T, address); | ||||
| } | ||||
| @@ -336,6 +287,27 @@ pub fn read(self: *Self, comptime T: type, unaligned_address: u32) T { | ||||
|     return self.slowRead(T, unaligned_address); | ||||
| } | ||||
|  | ||||
| pub fn dbgRead(self: *const Self, comptime T: type, unaligned_address: u32) T { | ||||
|     const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits; | ||||
|     const page = unaligned_address >> bits; | ||||
|     const offset = unaligned_address & (page_size - 1); | ||||
|  | ||||
|     // We're doing some serious out-of-bounds open-bus reads | ||||
|     if (page >= table_len) return self.openBus(T, unaligned_address); | ||||
|  | ||||
|     if (self.read_table[page]) |some_ptr| { | ||||
|         // We have a pointer to a page, cast the pointer to it's underlying type | ||||
|         const Ptr = [*]const T; | ||||
|         const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr)); | ||||
|  | ||||
|         // Note: We don't check array length, since we force align the | ||||
|         // lower bits of the address as the GBA would | ||||
|         return ptr[forceAlign(T, offset) / @sizeOf(T)]; | ||||
|     } | ||||
|  | ||||
|     return self.dbgSlowRead(T, unaligned_address); | ||||
| } | ||||
|  | ||||
| fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T { | ||||
|     @setCold(true); | ||||
|  | ||||
| @@ -366,6 +338,34 @@ fn slowRead(self: *Self, comptime T: type, unaligned_address: u32) T { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| fn dbgSlowRead(self: *const Self, comptime T: type, unaligned_address: u32) T { | ||||
|     const page = @truncate(u8, unaligned_address >> 24); | ||||
|     const address = forceAlign(T, unaligned_address); | ||||
|  | ||||
|     return switch (page) { | ||||
|         // General Internal Memory | ||||
|         0x00 => blk: { | ||||
|             if (address < Bios.size) | ||||
|                 break :blk self.bios.dbgRead(T, self.cpu.r[15], unaligned_address); | ||||
|  | ||||
|             break :blk self.openBus(T, address); | ||||
|         }, | ||||
|         0x02 => unreachable, // handled by fastmem | ||||
|         0x03 => unreachable, // handled by fastmem | ||||
|         0x04 => self.readIo(T, address), | ||||
|  | ||||
|         // Internal Display Memory | ||||
|         0x05 => unreachable, // handled by fastmem | ||||
|         0x06 => unreachable, // handled by fastmem | ||||
|         0x07 => unreachable, // handled by fastmem | ||||
|  | ||||
|         // External Memory (Game Pak) | ||||
|         0x08...0x0D => self.pak.dbgRead(T, address), | ||||
|         0x0E...0x0F => self.readBackup(T, unaligned_address), | ||||
|         else => self.openBus(T, address), | ||||
|     }; | ||||
| } | ||||
|  | ||||
| fn readBackup(self: *const Self, comptime T: type, unaligned_address: u32) T { | ||||
|     const value = self.pak.backup.read(unaligned_address); | ||||
|  | ||||
| @@ -406,6 +406,31 @@ pub fn write(self: *Self, comptime T: type, unaligned_address: u32, value: T) vo | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Mostly Identical to `Bus.write`, slowmeme is handled by `Bus.dbgSlowWrite` | ||||
| pub fn dbgWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void { | ||||
|     const bits = @typeInfo(std.math.IntFittingRange(0, page_size - 1)).Int.bits; | ||||
|     const page = unaligned_address >> bits; | ||||
|     const offset = unaligned_address & (page_size - 1); | ||||
|  | ||||
|     // We're doing some serious out-of-bounds open-bus writes, they do nothing though | ||||
|     if (page >= table_len) return; | ||||
|  | ||||
|     if (self.write_tables[@boolToInt(T == u8)][page]) |some_ptr| { | ||||
|         // We have a pointer to a page, cast the pointer to it's underlying type | ||||
|         const Ptr = [*]T; | ||||
|         const ptr = @ptrCast(Ptr, @alignCast(@alignOf(std.meta.Child(Ptr)), some_ptr)); | ||||
|  | ||||
|         // Note: We don't check array length, since we force align the | ||||
|         // lower bits of the address as the GBA would | ||||
|         ptr[forceAlign(T, offset) / @sizeOf(T)] = value; | ||||
|     } else { | ||||
|         // we can return early if this is an 8-bit OAM write | ||||
|         if (T == u8 and @truncate(u8, unaligned_address >> 24) == 0x07) return; | ||||
|  | ||||
|         self.dbgSlowWrite(T, unaligned_address, value); | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void { | ||||
|     @setCold(true); | ||||
|  | ||||
| @@ -431,6 +456,31 @@ fn slowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) vo | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn dbgSlowWrite(self: *Self, comptime T: type, unaligned_address: u32, value: T) void { | ||||
|     @setCold(true); | ||||
|  | ||||
|     const page = @truncate(u8, unaligned_address >> 24); | ||||
|     const address = forceAlign(T, unaligned_address); | ||||
|  | ||||
|     switch (page) { | ||||
|         // General Internal Memory | ||||
|         0x00 => self.bios.write(T, address, value), | ||||
|         0x02 => unreachable, // completely handled by fastmem | ||||
|         0x03 => unreachable, // completely handled by fastmem | ||||
|         0x04 => return, // FIXME: Let debug writes mess with I/O | ||||
|  | ||||
|         // Internal Display Memory | ||||
|         0x05 => self.ppu.palette.write(T, address, value), | ||||
|         0x06 => self.ppu.vram.write(T, self.ppu.dispcnt, address, value), | ||||
|         0x07 => unreachable, // completely handled by fastmem | ||||
|  | ||||
|         // External Memory (Game Pak) | ||||
|         0x08...0x0D => return, // FIXME: Debug Write to Backup/GPIO w/out messing with state | ||||
|         0x0E...0x0F => return, // FIXME: Debug Write to Backup w/out messing with state | ||||
|         else => {}, | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fn rotateBy(comptime T: type, address: u32) u32 { | ||||
|     return switch (T) { | ||||
|         u32 => address & 3, | ||||
|   | ||||
| @@ -213,6 +213,7 @@ fn guessDevice(buf: []const u8) Gpio.Device.Kind { | ||||
|     // Try to Guess if ROM uses RTC | ||||
|     const needle = "RTC_V"; // I was told SIIRTC_V, though Pokemen Firered (USA) is a false negative | ||||
|  | ||||
|     // TODO: Use new for loop syntax? | ||||
|     var i: usize = 0; | ||||
|     while ((i + needle.len) < buf.len) : (i += 1) { | ||||
|         if (std.mem.eql(u8, needle, buf[i..(i + needle.len)])) return .Rtc; | ||||
|   | ||||
| @@ -137,6 +137,7 @@ pub const Backup = struct { | ||||
|         for (backup_kinds) |needle| { | ||||
|             const needle_len = needle.str.len; | ||||
|  | ||||
|             // TODO: Use new for loop syntax? | ||||
|             var i: usize = 0; | ||||
|             while ((i + needle_len) < rom.len) : (i += 1) { | ||||
|                 if (std.mem.eql(u8, needle.str, rom[i..][0..needle_len])) return needle.kind; | ||||
|   | ||||
| @@ -339,10 +339,7 @@ fn DmaController(comptime id: u2) type { | ||||
| } | ||||
|  | ||||
| pub fn onBlanking(bus: *Bus, comptime kind: DmaKind) void { | ||||
|     comptime var i: usize = 0; | ||||
|     inline while (i < 4) : (i += 1) { | ||||
|         bus.dma[i].poll(kind); | ||||
|     } | ||||
|     inline for (0..4) |i| bus.dma[i].poll(kind); | ||||
| } | ||||
|  | ||||
| const Adjustment = enum(u2) { | ||||
|   | ||||
| @@ -293,13 +293,13 @@ pub const Clock = struct { | ||||
|         self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule | ||||
|  | ||||
|         const now = DateTime.now(); | ||||
|         self.year = bcd(u8, @intCast(u8, now.date.year - 2000)); | ||||
|         self.month = bcd(u5, now.date.month); | ||||
|         self.day = bcd(u6, now.date.day); | ||||
|         self.weekday = bcd(u3, (now.date.weekday() + 1) % 7); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6 | ||||
|         self.hour = bcd(u6, now.time.hour); | ||||
|         self.minute = bcd(u7, now.time.minute); | ||||
|         self.second = bcd(u7, now.time.second); | ||||
|         self.year = bcd(@intCast(u8, now.date.year - 2000)); | ||||
|         self.month = @truncate(u5, bcd(now.date.month)); | ||||
|         self.day = @truncate(u6, bcd(now.date.day)); | ||||
|         self.weekday = @truncate(u3, bcd((now.date.weekday() + 1) % 7)); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6 | ||||
|         self.hour = @truncate(u6, bcd(now.time.hour)); | ||||
|         self.minute = @truncate(u7, bcd(now.time.minute)); | ||||
|         self.second = @truncate(u7, bcd(now.time.second)); | ||||
|     } | ||||
|  | ||||
|     fn step(self: *Self, value: Data) u4 { | ||||
| @@ -449,16 +449,8 @@ pub const Clock = struct { | ||||
|     } | ||||
| }; | ||||
|  | ||||
| fn bcd(comptime T: type, value: u8) T { | ||||
|     var input = value; | ||||
|     var ret: u8 = 0; | ||||
|     var shift: u3 = 0; | ||||
|  | ||||
|     while (input > 0) { | ||||
|         ret |= (input % 10) << (shift << 2); | ||||
|         shift += 1; | ||||
|         input /= 10; | ||||
|     } | ||||
|  | ||||
|     return @truncate(T, ret); | ||||
| /// Converts an 8-bit unsigned integer to its BCD representation. | ||||
| /// Note: Algorithm only works for values between 0 and 99 inclusive. | ||||
| fn bcd(value: u8) u8 { | ||||
|     return ((value / 10) << 4) + (value % 10); | ||||
| } | ||||
|   | ||||
| @@ -39,13 +39,12 @@ pub const arm = struct { | ||||
|     } | ||||
|  | ||||
|     fn populate() [0x1000]InstrFn { | ||||
|         return comptime { | ||||
|         comptime { | ||||
|             @setEvalBranchQuota(0xE000); | ||||
|             var ret = [_]InstrFn{und} ** 0x1000; | ||||
|             var table = [_]InstrFn{und} ** 0x1000; | ||||
|  | ||||
|             var i: usize = 0; | ||||
|             while (i < ret.len) : (i += 1) { | ||||
|                 ret[i] = switch (@as(u2, i >> 10)) { | ||||
|             for (&table, 0..) |*handler, i| { | ||||
|                 handler.* = switch (@as(u2, i >> 10)) { | ||||
|                     0b00 => if (i == 0x121) blk: { | ||||
|                         break :blk branchExchange; | ||||
|                     } else if (i & 0xFCF == 0x009) blk: { | ||||
| @@ -107,8 +106,8 @@ pub const arm = struct { | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return ret; | ||||
|         }; | ||||
|             return table; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| @@ -136,13 +135,12 @@ pub const thumb = struct { | ||||
|     } | ||||
|  | ||||
|     fn populate() [0x400]InstrFn { | ||||
|         return comptime { | ||||
|         comptime { | ||||
|             @setEvalBranchQuota(5025); // This is exact | ||||
|             var ret = [_]InstrFn{und} ** 0x400; | ||||
|             var table = [_]InstrFn{und} ** 0x400; | ||||
|  | ||||
|             var i: usize = 0; | ||||
|             while (i < ret.len) : (i += 1) { | ||||
|                 ret[i] = switch (@as(u3, i >> 7 & 0x7)) { | ||||
|             for (&table, 0..) |*handler, i| { | ||||
|                 handler.* = switch (@as(u3, i >> 7 & 0x7)) { | ||||
|                     0b000 => if (i >> 5 & 0x3 == 0b11) blk: { | ||||
|                         const I = i >> 4 & 1 == 1; | ||||
|                         const is_sub = i >> 3 & 1 == 1; | ||||
| @@ -230,8 +228,8 @@ pub const thumb = struct { | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return ret; | ||||
|         }; | ||||
|             return table; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| @@ -385,8 +383,7 @@ pub const Arm7tdmi = struct { | ||||
|         const now = getModeChecked(self, self.cpsr.mode.read()); | ||||
|  | ||||
|         // Bank R8 -> r12 | ||||
|         var i: usize = 0; | ||||
|         while (i < 5) : (i += 1) { | ||||
|         for (0..5) |i| { | ||||
|             self.bank.fiq[Bank.fiqIdx(i, now)] = self.r[8 + i]; | ||||
|         } | ||||
|  | ||||
| @@ -404,8 +401,7 @@ pub const Arm7tdmi = struct { | ||||
|         } | ||||
|  | ||||
|         // Grab R8 -> R12 | ||||
|         i = 0; | ||||
|         while (i < 5) : (i += 1) { | ||||
|         for (0..5) |i| { | ||||
|             self.r[8 + i] = self.bank.fiq[Bank.fiqIdx(i, next)]; | ||||
|         } | ||||
|  | ||||
| @@ -470,8 +466,7 @@ pub const Arm7tdmi = struct { | ||||
|     } | ||||
|  | ||||
|     pub fn stepDmaTransfer(self: *Self) bool { | ||||
|         comptime var i: usize = 0; | ||||
|         inline while (i < 4) : (i += 1) { | ||||
|         inline for (0..4) |i| { | ||||
|             if (self.bus.dma[i].in_progress) { | ||||
|                 self.bus.dma[i].step(self); | ||||
|                 return true; | ||||
|   | ||||
| @@ -11,9 +11,7 @@ pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, | ||||
|             const rn = opcode >> 16 & 0xF; | ||||
|             const rd = opcode >> 12 & 0xF; | ||||
|  | ||||
|             // rn is r15 and L is not set, the PC is 12 ahead | ||||
|             const base = cpu.r[rn] + if (!L and rn == 0xF) 4 else @as(u32, 0); | ||||
|  | ||||
|             const base = cpu.r[rn]; | ||||
|             const offset = if (I) shifter.immediate(false, cpu, opcode) else opcode & 0xFFF; | ||||
|  | ||||
|             const modified_base = if (U) base +% offset else base -% offset; | ||||
|   | ||||
| @@ -92,8 +92,7 @@ pub fn fmt15(comptime L: bool, comptime rb: u3) InstrFn { | ||||
| inline fn countRlist(opcode: u16) u32 { | ||||
|     var count: u32 = 0; | ||||
|  | ||||
|     comptime var i: u4 = 0; | ||||
|     inline while (i < 8) : (i += 1) { | ||||
|     inline for (0..8) |i| { | ||||
|         if (opcode >> (7 - i) & 1 == 1) count += 1; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -146,9 +146,8 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 { | ||||
|  | ||||
|     const step = 2 * std.time.ns_per_ms; // Granularity of 2ms | ||||
|     const times = sleep_for / step; | ||||
|     var i: usize = 0; | ||||
|  | ||||
|     while (i < times) : (i += 1) { | ||||
|     for (0..times) |_| { | ||||
|         std.time.sleep(step); | ||||
|  | ||||
|         // Upon wakeup, check to see if this particular sleep was longer than expected | ||||
| @@ -163,3 +162,59 @@ fn sleep(timer: *Timer, wake_time: u64) ?u64 { | ||||
| fn spinLoop(timer: *Timer, wake_time: u64) void { | ||||
|     while (true) if (timer.read() > wake_time) break; | ||||
| } | ||||
|  | ||||
| pub const EmuThing = struct { | ||||
|     const Self = @This(); | ||||
|     const Interface = @import("gdbstub").Emulator; | ||||
|     const Allocator = std.mem.Allocator; | ||||
|  | ||||
|     cpu: *Arm7tdmi, | ||||
|     scheduler: *Scheduler, | ||||
|  | ||||
|     pub fn init(cpu: *Arm7tdmi, scheduler: *Scheduler) Self { | ||||
|         return .{ .cpu = cpu, .scheduler = scheduler }; | ||||
|     } | ||||
|  | ||||
|     pub fn interface(self: *Self, allocator: Allocator) Interface { | ||||
|         return Interface.init(allocator, self); | ||||
|     } | ||||
|  | ||||
|     pub fn read(self: *const Self, addr: u32) u8 { | ||||
|         return self.cpu.bus.dbgRead(u8, addr); | ||||
|     } | ||||
|  | ||||
|     pub fn write(self: *Self, addr: u32, value: u8) void { | ||||
|         self.cpu.bus.dbgWrite(u8, addr, value); | ||||
|     } | ||||
|  | ||||
|     pub fn registers(self: *const Self) *[16]u32 { | ||||
|         return &self.cpu.r; | ||||
|     } | ||||
|  | ||||
|     pub fn cpsr(self: *const Self) u32 { | ||||
|         return self.cpu.cpsr.raw; | ||||
|     } | ||||
|  | ||||
|     pub fn step(self: *Self) void { | ||||
|         const cpu = self.cpu; | ||||
|         const sched = self.scheduler; | ||||
|  | ||||
|         // Is true when we have executed one (1) instruction | ||||
|         var did_step: bool = false; | ||||
|  | ||||
|         // TODO: How can I make it easier to keep this in lock-step with runFrame? | ||||
|         while (!did_step) { | ||||
|             if (!cpu.stepDmaTransfer()) { | ||||
|                 if (cpu.isHalted()) { | ||||
|                     // Fast-forward to next Event | ||||
|                     sched.tick = sched.queue.peek().?.tick; | ||||
|                 } else { | ||||
|                     cpu.step(); | ||||
|                     did_step = true; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (sched.tick >= sched.nextTimestamp()) sched.handleEvent(cpu); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -625,8 +625,7 @@ pub const Ppu = struct { | ||||
|                 const framebuf_base = width * @as(usize, scanline); | ||||
|                 if (obj_enable) self.fetchSprites(); | ||||
|  | ||||
|                 var layer: usize = 0; | ||||
|                 while (layer < 4) : (layer += 1) { | ||||
|                 for (0..4) |layer| { | ||||
|                     self.drawSprites(@truncate(u2, layer)); | ||||
|                     if (layer == self.bg[0].cnt.priority.read() and bg_enable & 1 == 1) self.drawBackground(0); | ||||
|                     if (layer == self.bg[1].cnt.priority.read() and bg_enable >> 1 & 1 == 1) self.drawBackground(1); | ||||
| @@ -640,8 +639,7 @@ pub const Ppu = struct { | ||||
|                 const framebuf_base = width * @as(usize, scanline); | ||||
|                 if (obj_enable) self.fetchSprites(); | ||||
|  | ||||
|                 var layer: usize = 0; | ||||
|                 while (layer < 4) : (layer += 1) { | ||||
|                 for (0..4) |layer| { | ||||
|                     self.drawSprites(@truncate(u2, layer)); | ||||
|                     if (layer == self.bg[0].cnt.priority.read() and bg_enable & 1 == 1) self.drawBackground(0); | ||||
|                     if (layer == self.bg[1].cnt.priority.read() and bg_enable >> 1 & 1 == 1) self.drawBackground(1); | ||||
| @@ -654,8 +652,7 @@ pub const Ppu = struct { | ||||
|                 const framebuf_base = width * @as(usize, scanline); | ||||
|                 if (obj_enable) self.fetchSprites(); | ||||
|  | ||||
|                 var layer: usize = 0; | ||||
|                 while (layer < 4) : (layer += 1) { | ||||
|                 for (0..4) |layer| { | ||||
|                     self.drawSprites(@truncate(u2, layer)); | ||||
|                     if (layer == self.bg[2].cnt.priority.read() and bg_enable >> 2 & 1 == 1) self.drawAffineBackground(2); | ||||
|                     if (layer == self.bg[3].cnt.priority.read() and bg_enable >> 3 & 1 == 1) self.drawAffineBackground(3); | ||||
| @@ -671,7 +668,7 @@ pub const Ppu = struct { | ||||
|                 const vram_buf = @ptrCast([*]const u16, @alignCast(@alignOf(u16), self.vram.buf)); | ||||
|                 const framebuf = @ptrCast([*]u32, @alignCast(@alignOf(u32), self.framebuf.get(.Emulator))); | ||||
|  | ||||
|                 for (vram_buf[vram_base .. vram_base + width]) |bgr555, i| { | ||||
|                 for (vram_buf[vram_base .. vram_base + width], 0..) |bgr555, i| { | ||||
|                     framebuf[framebuf_base + i] = rgba888(bgr555); | ||||
|                 } | ||||
|             }, | ||||
| @@ -685,7 +682,7 @@ pub const Ppu = struct { | ||||
|                 const pal_buf = @ptrCast([*]const u16, @alignCast(@alignOf(u16), self.palette.buf)); | ||||
|                 const framebuf = @ptrCast([*]u32, @alignCast(@alignOf(u32), self.framebuf.get(.Emulator))); | ||||
|  | ||||
|                 for (self.vram.buf[vram_base .. vram_base + width]) |pal_id, i| { | ||||
|                 for (self.vram.buf[vram_base .. vram_base + width], 0..) |pal_id, i| { | ||||
|                     framebuf[framebuf_base + i] = rgba888(pal_buf[pal_id]); | ||||
|                 } | ||||
|             }, | ||||
| @@ -701,8 +698,7 @@ pub const Ppu = struct { | ||||
|                 const vram_buf = @ptrCast([*]const u16, @alignCast(@alignOf(u16), self.vram.buf)); | ||||
|                 const framebuf = @ptrCast([*]u32, @alignCast(@alignOf(u32), self.framebuf.get(.Emulator))); | ||||
|  | ||||
|                 var i: usize = 0; | ||||
|                 while (i < width) : (i += 1) { | ||||
|                 for (0..width) |i| { | ||||
|                     const bgr555 = if (scanline < m5_height and i < m5_width) vram_buf[vram_base + i] else self.palette.backdrop(); | ||||
|                     framebuf[framebuf_base + i] = rgba888(bgr555); | ||||
|                 } | ||||
| @@ -718,7 +714,7 @@ pub const Ppu = struct { | ||||
|         // FIXME: @ptrCast between slices changing the length isn't implemented yet | ||||
|         const framebuf = @ptrCast([*]u32, @alignCast(@alignOf(u32), self.framebuf.get(.Emulator))); | ||||
|  | ||||
|         for (self.scanline.top()) |maybe_top, i| { | ||||
|         for (self.scanline.top(), 0..) |maybe_top, i| { | ||||
|             const maybe_btm = self.scanline.btm()[i]; | ||||
|  | ||||
|             const bgr555 = self.getBgr555(maybe_top, maybe_btm); | ||||
| @@ -843,7 +839,12 @@ pub const Ppu = struct { | ||||
|                 const is_top_layer = (top_layer >> layer) & 1 == 1; | ||||
|  | ||||
|                 if (is_top_layer) { | ||||
|                     self.scanline.btm()[i] = Scanline.Pixel.from(.Background, bgr555); // this is intentional | ||||
|                     const pixel = self.scanline.btm()[i]; | ||||
|  | ||||
|                     // FIXME: Can't I do this check ealier? Test Amazing Mirror File Select, bld_demo.gba | ||||
|                     if (!pixel.isSet()) | ||||
|                         self.scanline.btm()[i] = Scanline.Pixel.from(.Background, bgr555); // this is intentional | ||||
|  | ||||
|                     return; | ||||
|                 } | ||||
|             }, | ||||
| @@ -985,8 +986,7 @@ pub const Ppu = struct { | ||||
|             cpu.handleInterrupt(); | ||||
|         } | ||||
|  | ||||
|         // See if HBlank DMA is present and not enabled | ||||
|  | ||||
|         // If we're not also in VBlank, attempt to run any pending DMA Reqs | ||||
|         if (!self.dispstat.vblank.read()) | ||||
|             dma.onBlanking(cpu.bus, .HBlank); | ||||
|  | ||||
|   | ||||
| @@ -73,7 +73,7 @@ pub const Scheduler = struct { | ||||
|  | ||||
|     /// Removes the **first** scheduled event of type `needle` | ||||
|     pub fn removeScheduledEvent(self: *Self, needle: EventKind) void { | ||||
|         for (self.queue.items) |event, i| { | ||||
|         for (self.queue.items, 0..) |event, i| { | ||||
|             if (std.meta.eql(event.kind, needle)) { | ||||
|  | ||||
|                 // invalidates the slice we're iterating over | ||||
|   | ||||
							
								
								
									
										46
									
								
								src/main.zig
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								src/main.zig
									
									
									
									
									
								
							| @@ -4,14 +4,17 @@ const known_folders = @import("known_folders"); | ||||
| const clap = @import("clap"); | ||||
|  | ||||
| const config = @import("config.zig"); | ||||
| const emu = @import("core/emu.zig"); | ||||
|  | ||||
| const Gui = @import("platform.zig").Gui; | ||||
| const Bus = @import("core/Bus.zig"); | ||||
| const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; | ||||
| const Scheduler = @import("core/scheduler.zig").Scheduler; | ||||
| const FilePaths = @import("util.zig").FilePaths; | ||||
|  | ||||
| const FpsTracker = @import("util.zig").FpsTracker; | ||||
| const Allocator = std.mem.Allocator; | ||||
| const Atomic = std.atomic.Atomic; | ||||
|  | ||||
| const log = std.log.scoped(.Cli); | ||||
| const width = @import("core/ppu.zig").width; | ||||
| const height = @import("core/ppu.zig").height; | ||||
| @@ -22,6 +25,7 @@ const params = clap.parseParamsComptime( | ||||
|     \\-h, --help            Display this help and exit. | ||||
|     \\-s, --skip            Skip BIOS. | ||||
|     \\-b, --bios <str>      Optional path to a GBA BIOS ROM. | ||||
|     \\ --gdb                Run ZBA from the context of a GDB Server | ||||
|     \\<str>                 Path to the GBA GamePak ROM. | ||||
|     \\ | ||||
| ); | ||||
| @@ -87,13 +91,49 @@ pub fn main() void { | ||||
|         cpu.fastBoot(); | ||||
|     } | ||||
|  | ||||
|     var quit = Atomic(bool).init(false); | ||||
|     var gui = Gui.init(&bus.pak.title, &bus.apu, width, height) catch |e| exitln("failed to init gui: {}", .{e}); | ||||
|     defer gui.deinit(); | ||||
|  | ||||
|     gui.run(&cpu, &scheduler) catch |e| exitln("failed to run gui thread: {}", .{e}); | ||||
|     if (result.args.gdb) { | ||||
|         const Server = @import("gdbstub").Server; | ||||
|         const EmuThing = @import("core/emu.zig").EmuThing; | ||||
|  | ||||
|         var wrapper = EmuThing.init(&cpu, &scheduler); | ||||
|         var emulator = wrapper.interface(allocator); | ||||
|         defer emulator.deinit(); | ||||
|  | ||||
|         log.info("Ready to connect", .{}); | ||||
|  | ||||
|         var server = Server.init(emulator) catch |e| exitln("failed to init gdb server: {}", .{e}); | ||||
|         defer server.deinit(allocator); | ||||
|  | ||||
|         log.info("Starting GDB Server Thread", .{}); | ||||
|  | ||||
|         const thread = std.Thread.spawn(.{}, Server.run, .{ &server, allocator, &quit }) catch |e| exitln("gdb server thread crashed: {}", .{e}); | ||||
|         defer thread.join(); | ||||
|  | ||||
|         gui.run(.{ | ||||
|             .cpu = &cpu, | ||||
|             .scheduler = &scheduler, | ||||
|             .quit = &quit, | ||||
|         }) catch |e| exitln("main thread panicked: {}", .{e}); | ||||
|     } else { | ||||
|         var tracker = FpsTracker.init(); | ||||
|  | ||||
|         const thread = std.Thread.spawn(.{}, emu.run, .{ &quit, &scheduler, &cpu, &tracker }) catch |e| exitln("emu thread panicked: {}", .{e}); | ||||
|         defer thread.join(); | ||||
|  | ||||
|         gui.run(.{ | ||||
|             .cpu = &cpu, | ||||
|             .scheduler = &scheduler, | ||||
|             .tracker = &tracker, | ||||
|             .quit = &quit, | ||||
|         }) catch |e| exitln("main thread panicked: {}", .{e}); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths { | ||||
| fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths { | ||||
|     const rom_path = romPath(result); | ||||
|     log.info("ROM path: {s}", .{rom_path}); | ||||
|  | ||||
|   | ||||
| @@ -154,9 +154,17 @@ pub const Gui = struct { | ||||
|         return tex_id; | ||||
|     } | ||||
|  | ||||
|     pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void { | ||||
|         var quit = std.atomic.Atomic(bool).init(false); | ||||
|         var tracker = FpsTracker.init(); | ||||
|     const RunOptions = struct { | ||||
|         quit: *std.atomic.Atomic(bool), | ||||
|         tracker: ?*FpsTracker = null, | ||||
|         cpu: *Arm7tdmi, | ||||
|         scheduler: *Scheduler, | ||||
|     }; | ||||
|  | ||||
|     pub fn run(self: *Self, opt: RunOptions) !void { | ||||
|         const cpu = opt.cpu; | ||||
|         const tracker = opt.tracker; | ||||
|         const quit = opt.quit; | ||||
|  | ||||
|         var buffer_ids = Self.generateBuffers(); | ||||
|         defer { | ||||
| @@ -169,13 +177,15 @@ pub const Gui = struct { | ||||
|         const tex_id = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer)); | ||||
|         defer gl.deleteTextures(1, &tex_id); | ||||
|  | ||||
|         const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, scheduler, cpu, &tracker }); | ||||
|         defer thread.join(); | ||||
|  | ||||
|         var title_buf: [0x100]u8 = undefined; | ||||
|  | ||||
|         emu_loop: while (true) { | ||||
|             var event: SDL.SDL_Event = undefined; | ||||
|  | ||||
|             // This might be true if the emu is running via a gdbstub server | ||||
|             // and the gdb stub exits first | ||||
|             if (quit.load(.Monotonic)) break :emu_loop; | ||||
|  | ||||
|             while (SDL.SDL_PollEvent(&event) != 0) { | ||||
|                 switch (event.type) { | ||||
|                     SDL.SDL_QUIT => break :emu_loop, | ||||
| @@ -238,8 +248,10 @@ pub const Gui = struct { | ||||
|             gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null); | ||||
|             SDL.SDL_GL_SwapWindow(self.window); | ||||
|  | ||||
|             const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, tracker.value() }) catch unreachable; | ||||
|             SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); | ||||
|             if (tracker) |t| { | ||||
|                 const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, t.value() }) catch unreachable; | ||||
|                 SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         quit.store(true, .Monotonic); // Terminate Emulator Thread | ||||
|   | ||||
		Reference in New Issue
	
	Block a user