From 9a8aaba1ab9a1a3a41e07f76283c5849bd1bc3ee Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sun, 18 Sep 2022 05:54:44 -0300 Subject: [PATCH] chore: improve util and Gui API --- src/Gui.zig | 194 ---------------------------------------- src/core/bus/backup.zig | 4 +- src/core/util.zig | 60 ++++++++++--- src/main.zig | 7 +- src/platform.zig | 190 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 242 insertions(+), 213 deletions(-) delete mode 100644 src/Gui.zig create mode 100644 src/platform.zig diff --git a/src/Gui.zig b/src/Gui.zig deleted file mode 100644 index 1d0be00..0000000 --- a/src/Gui.zig +++ /dev/null @@ -1,194 +0,0 @@ -const std = @import("std"); -const SDL = @import("sdl2"); -const Self = @This(); - -const Apu = @import("core/apu.zig").Apu; -const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; -const Scheduler = @import("core/scheduler.zig").Scheduler; -const FpsTracker = @import("core/util.zig").FpsTracker; - -const pitch = @import("core/ppu.zig").framebuf_pitch; -const scale = @import("core/emu.zig").win_scale; - -const emu = @import("core/emu.zig"); -const log = std.log.scoped(.GUI); - -const default_title: []const u8 = "ZBA"; - -window: *SDL.SDL_Window, -base_title: [12]u8, -renderer: *SDL.SDL_Renderer, -texture: *SDL.SDL_Texture, -audio: ?Audio, - -pub fn init(title: [12]u8, width: i32, height: i32) Self { - 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 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), - SDL.SDL_WINDOW_SHOWN, - ) orelse panic(); - - const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic(); - - const texture = SDL.SDL_CreateTexture( - renderer, - SDL.SDL_PIXELFORMAT_RGBA8888, - SDL.SDL_TEXTUREACCESS_STREAMING, - @as(c_int, width), - @as(c_int, height), - ) orelse panic(); - - return Self{ - .window = window, - .base_title = title, - .renderer = renderer, - .texture = texture, - .audio = null, - }; -} - -pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void { - var quit = std.atomic.Atomic(bool).init(false); - var frame_rate = FpsTracker.init(); - - const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, cpu }); - defer thread.join(); - - var title_buf: [0x100]u8 = [_]u8{0} ** 0x100; - - emu_loop: while (true) { - var event: SDL.SDL_Event = undefined; - while (SDL.SDL_PollEvent(&event) != 0) { - switch (event.type) { - SDL.SDL_QUIT => break :emu_loop, - SDL.SDL_KEYDOWN => { - const io = &cpu.bus.io; - const key_code = event.key.keysym.sym; - - switch (key_code) { - SDL.SDLK_UP => io.keyinput.up.unset(), - SDL.SDLK_DOWN => io.keyinput.down.unset(), - SDL.SDLK_LEFT => io.keyinput.left.unset(), - SDL.SDLK_RIGHT => io.keyinput.right.unset(), - SDL.SDLK_x => io.keyinput.a.unset(), - SDL.SDLK_z => io.keyinput.b.unset(), - SDL.SDLK_a => io.keyinput.shoulder_l.unset(), - SDL.SDLK_s => io.keyinput.shoulder_r.unset(), - SDL.SDLK_RETURN => io.keyinput.start.unset(), - SDL.SDLK_RSHIFT => io.keyinput.select.unset(), - else => {}, - } - }, - SDL.SDL_KEYUP => { - const io = &cpu.bus.io; - const key_code = event.key.keysym.sym; - - switch (key_code) { - SDL.SDLK_UP => io.keyinput.up.set(), - SDL.SDLK_DOWN => io.keyinput.down.set(), - SDL.SDLK_LEFT => io.keyinput.left.set(), - SDL.SDLK_RIGHT => io.keyinput.right.set(), - SDL.SDLK_x => io.keyinput.a.set(), - SDL.SDLK_z => io.keyinput.b.set(), - SDL.SDLK_a => io.keyinput.shoulder_l.set(), - SDL.SDLK_s => io.keyinput.shoulder_r.set(), - SDL.SDLK_RETURN => io.keyinput.start.set(), - SDL.SDLK_RSHIFT => io.keyinput.select.set(), - SDL.SDLK_i => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}), - SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }), - SDL.SDLK_k => { - // Dump IWRAM to file - log.info("PC: 0x{X:0>8}", .{cpu.r[15]}); - log.info("LR: 0x{X:0>8}", .{cpu.r[14]}); - // const iwram_file = try std.fs.cwd().createFile("iwram.bin", .{}); - // defer iwram_file.close(); - - // try iwram_file.writeAll(cpu.bus.iwram.buf); - }, - else => {}, - } - }, - else => {}, - } - } - - // Emulator has an internal Double Buffer - const framebuf = cpu.bus.ppu.framebuf.get(.Renderer); - _ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch); - _ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null); - SDL.SDL_RenderPresent(self.renderer); - - const title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.base_title, frame_rate.value() }) catch unreachable; - SDL.SDL_SetWindowTitle(self.window, title.ptr); - } - - quit.store(true, .SeqCst); // Terminate Emulator Thread -} - -pub fn initAudio(self: *Self, apu: *Apu) void { - self.audio = Audio.init(apu); - self.audio.?.play(); -} - -pub fn deinit(self: *Self) void { - if (self.audio) |*aud| aud.deinit(); - SDL.SDL_DestroyTexture(self.texture); - SDL.SDL_DestroyRenderer(self.renderer); - SDL.SDL_DestroyWindow(self.window); - SDL.SDL_Quit(); - self.* = undefined; -} - -const Audio = struct { - const This = @This(); - const sample_rate = @import("core/apu.zig").host_sample_rate; - - device: SDL.SDL_AudioDeviceID, - - fn init(apu: *Apu) This { - var have: SDL.SDL_AudioSpec = undefined; - var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec); - want.freq = sample_rate; - want.format = SDL.AUDIO_U16; - want.channels = 2; - want.samples = 0x100; - want.callback = This.callback; - want.userdata = apu; - - const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0); - if (device == 0) panic(); - - return .{ - .device = device, - }; - } - - fn deinit(self: *This) void { - SDL.SDL_CloseAudioDevice(self.device); - self.* = undefined; - } - - pub fn play(self: *This) void { - SDL.SDL_PauseAudioDevice(self.device, 0); - } - - export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void { - const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata)); - _ = SDL.SDL_AudioStreamGet(apu.stream, stream, len); - - // If we don't write anything, play silence otherwise garbage will be played - // FIXME: I don't think this hack to remove DC Offset is acceptable :thinking: - // if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40); - } -}; - -fn panic() noreturn { - const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error"; - @panic(std.mem.sliceTo(str, 0)); -} diff --git a/src/core/bus/backup.zig b/src/core/bus/backup.zig index f89a0cc..73a2580 100644 --- a/src/core/bus/backup.zig +++ b/src/core/bus/backup.zig @@ -3,7 +3,7 @@ const Allocator = std.mem.Allocator; const log = std.log.scoped(.Backup); const escape = @import("../util.zig").escape; -const asStringSlice = @import("../util.zig").asStringSlice; +const span = @import("../util.zig").span; const backup_kinds = [5]Needle{ .{ .str = "EEPROM_V", .kind = .Eeprom }, @@ -128,7 +128,7 @@ pub const Backup = struct { } fn getSaveFilename(self: *const Self, allocator: Allocator) ![]const u8 { - const title_str = asStringSlice(&escape(self.title)); + const title_str = span(&escape(self.title)); const name = if (title_str.len != 0) title_str else "untitled"; return try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".sav" }); diff --git a/src/core/util.zig b/src/core/util.zig index 33c5efe..15224e6 100644 --- a/src/core/util.zig +++ b/src/core/util.zig @@ -66,21 +66,55 @@ 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. +/// The Title from the GBA Cartridge is an Uppercase ASCII string which is +/// null-padded to 12 bytes /// -/// This function returns a slice of everything just before the first -/// `\0` -pub fn asStringSlice(title: *const [12]u8) []const u8 { - var len = title.len; - for (title) |char, i| { - if (char == 0) { - len = i; - break; - } - } +/// This function returns a slice of the ASCII string without the null terminator(s) +/// (essentially, a proper Zig/Rust/Any modern language String) +pub fn span(title: *const [12]u8) []const u8 { + const end = std.mem.indexOfScalar(u8, title, "\x00"[0]); + return title[0 .. end orelse title.len]; +} - return title[0..len]; +test "span" { + var example: *const [12]u8 = "POKEMON_EMER"; + try std.testing.expectEqualSlices(u8, "POKEMON_EMER", span(example)); + + example = "POKEMON_EME\x00"; + try std.testing.expectEqualSlices(u8, "POKEMON_EME", span(example)); + + example = "POKEMON_EM\x00\x00"; + try std.testing.expectEqualSlices(u8, "POKEMON_EM", span(example)); + + example = "POKEMON_E\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "POKEMON_E", span(example)); + + example = "POKEMON_\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "POKEMON_", span(example)); + + example = "POKEMON\x00\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "POKEMON", span(example)); + + example = "POKEMO\x00\x00\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "POKEMO", span(example)); + + example = "POKEM\x00\x00\x00\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "POKEM", span(example)); + + example = "POKE\x00\x00\x00\x00\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "POKE", span(example)); + + example = "POK\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "POK", span(example)); + + example = "PO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "PO", span(example)); + + example = "P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "P", span(example)); + + example = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + try std.testing.expectEqualSlices(u8, "", span(example)); } /// Copies a Title and returns either an identical or similar diff --git a/src/main.zig b/src/main.zig index 100eed8..2b69c24 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,14 +4,14 @@ const builtin = @import("builtin"); const known_folders = @import("known_folders"); const clap = @import("clap"); -const Gui = @import("Gui.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("core/util.zig").FilePaths; const Allocator = std.mem.Allocator; -const log = std.log.scoped(.CLI); +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; @@ -54,8 +54,7 @@ pub fn main() anyerror!void { try bus.init(allocator, &scheduler, &cpu, paths); defer bus.deinit(); - var gui = Gui.init(bus.pak.title, width, height); - gui.initAudio(&bus.apu); + var gui = Gui.init(&bus.pak.title, &bus.apu, width, height); defer gui.deinit(); try gui.run(&cpu, &scheduler); diff --git a/src/platform.zig b/src/platform.zig new file mode 100644 index 0000000..89d1640 --- /dev/null +++ b/src/platform.zig @@ -0,0 +1,190 @@ +const std = @import("std"); +const SDL = @import("sdl2"); +const emu = @import("core/emu.zig"); + +const Apu = @import("core/apu.zig").Apu; +const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; +const Scheduler = @import("core/scheduler.zig").Scheduler; +const FpsTracker = @import("core/util.zig").FpsTracker; + +const span = @import("core/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 { + const Self = @This(); + const log = std.log.scoped(.Gui); + + window: *SDL.SDL_Window, + title: []const u8, + renderer: *SDL.SDL_Renderer, + texture: *SDL.SDL_Texture, + audio: Audio, + + pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) Self { + 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 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), + SDL.SDL_WINDOW_SHOWN, + ) orelse panic(); + + const renderer = SDL.SDL_CreateRenderer(window, -1, SDL.SDL_RENDERER_ACCELERATED | SDL.SDL_RENDERER_PRESENTVSYNC) orelse panic(); + + const texture = SDL.SDL_CreateTexture( + renderer, + SDL.SDL_PIXELFORMAT_RGBA8888, + SDL.SDL_TEXTUREACCESS_STREAMING, + @as(c_int, width), + @as(c_int, height), + ) orelse panic(); + + return Self{ + .window = window, + .title = span(title), + .renderer = renderer, + .texture = texture, + .audio = Audio.init(apu), + }; + } + + pub fn run(self: *Self, cpu: *Arm7tdmi, scheduler: *Scheduler) !void { + var quit = std.atomic.Atomic(bool).init(false); + var frame_rate = FpsTracker.init(); + + const thread = try std.Thread.spawn(.{}, emu.run, .{ &quit, &frame_rate, scheduler, cpu }); + defer thread.join(); + + var title_buf: [0x100]u8 = [_]u8{0} ** 0x100; + + emu_loop: while (true) { + var event: SDL.SDL_Event = undefined; + while (SDL.SDL_PollEvent(&event) != 0) { + switch (event.type) { + SDL.SDL_QUIT => break :emu_loop, + SDL.SDL_KEYDOWN => { + const io = &cpu.bus.io; + const key_code = event.key.keysym.sym; + + switch (key_code) { + SDL.SDLK_UP => io.keyinput.up.unset(), + SDL.SDLK_DOWN => io.keyinput.down.unset(), + SDL.SDLK_LEFT => io.keyinput.left.unset(), + SDL.SDLK_RIGHT => io.keyinput.right.unset(), + SDL.SDLK_x => io.keyinput.a.unset(), + SDL.SDLK_z => io.keyinput.b.unset(), + SDL.SDLK_a => io.keyinput.shoulder_l.unset(), + SDL.SDLK_s => io.keyinput.shoulder_r.unset(), + SDL.SDLK_RETURN => io.keyinput.start.unset(), + SDL.SDLK_RSHIFT => io.keyinput.select.unset(), + else => {}, + } + }, + SDL.SDL_KEYUP => { + const io = &cpu.bus.io; + const key_code = event.key.keysym.sym; + + switch (key_code) { + SDL.SDLK_UP => io.keyinput.up.set(), + SDL.SDLK_DOWN => io.keyinput.down.set(), + SDL.SDLK_LEFT => io.keyinput.left.set(), + SDL.SDLK_RIGHT => io.keyinput.right.set(), + SDL.SDLK_x => io.keyinput.a.set(), + SDL.SDLK_z => io.keyinput.b.set(), + SDL.SDLK_a => io.keyinput.shoulder_l.set(), + SDL.SDLK_s => io.keyinput.shoulder_r.set(), + SDL.SDLK_RETURN => io.keyinput.start.set(), + SDL.SDLK_RSHIFT => io.keyinput.select.set(), + SDL.SDLK_i => log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}), + SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }), + SDL.SDLK_k => { + // Dump IWRAM to file + log.info("PC: 0x{X:0>8}", .{cpu.r[15]}); + log.info("LR: 0x{X:0>8}", .{cpu.r[14]}); + // const iwram_file = try std.fs.cwd().createFile("iwram.bin", .{}); + // defer iwram_file.close(); + + // try iwram_file.writeAll(cpu.bus.iwram.buf); + }, + else => {}, + } + }, + else => {}, + } + } + + // Emulator has an internal Double Buffer + const framebuf = cpu.bus.ppu.framebuf.get(.Renderer); + _ = SDL.SDL_UpdateTexture(self.texture, null, framebuf.ptr, pitch); + _ = SDL.SDL_RenderCopy(self.renderer, self.texture, null, null); + SDL.SDL_RenderPresent(self.renderer); + + const dyn_title = std.fmt.bufPrint(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, frame_rate.value() }) catch unreachable; + SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); + } + + quit.store(true, .SeqCst); // Terminate Emulator Thread + } + + pub fn deinit(self: *Self) void { + self.audio.deinit(); + SDL.SDL_DestroyTexture(self.texture); + SDL.SDL_DestroyRenderer(self.renderer); + SDL.SDL_DestroyWindow(self.window); + SDL.SDL_Quit(); + self.* = undefined; + } +}; + +const Audio = struct { + const Self = @This(); + const log = std.log.scoped(.PlatformAudio); + const sample_rate = @import("core/apu.zig").host_sample_rate; + + device: SDL.SDL_AudioDeviceID, + + fn init(apu: *Apu) Self { + var have: SDL.SDL_AudioSpec = undefined; + var want: SDL.SDL_AudioSpec = std.mem.zeroes(SDL.SDL_AudioSpec); + want.freq = sample_rate; + want.format = SDL.AUDIO_U16; + want.channels = 2; + want.samples = 0x100; + want.callback = Self.callback; + want.userdata = apu; + + const device = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0); + if (device == 0) panic(); + + SDL.SDL_PauseAudioDevice(device, 0); // Unpause Audio + + return .{ .device = device }; + } + + fn deinit(self: *Self) void { + SDL.SDL_CloseAudioDevice(self.device); + self.* = undefined; + } + + export fn callback(userdata: ?*anyopaque, stream: [*c]u8, len: c_int) void { + const apu = @ptrCast(*Apu, @alignCast(@alignOf(*Apu), userdata)); + _ = SDL.SDL_AudioStreamGet(apu.stream, stream, len); + + // If we don't write anything, play silence otherwise garbage will be played + // FIXME: I don't think this hack to remove DC Offset is acceptable :thinking: + // if (written == 0) std.mem.set(u8, stream[0..@intCast(usize, len)], 0x40); + } +}; + +fn panic() noreturn { + const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error"; + @panic(std.mem.sliceTo(str, 0)); +}