feat: Initial Implementation of DMA Audio
This commit is contained in:
parent
c100d64fcb
commit
76b4d56ca6
26
src/Bus.zig
26
src/Bus.zig
|
@ -1,5 +1,6 @@
|
|||
const std = @import("std");
|
||||
|
||||
const AudioDeviceId = @import("sdl2").SDL_AudioDeviceID;
|
||||
const Bios = @import("bus/Bios.zig");
|
||||
const Ewram = @import("bus/Ewram.zig");
|
||||
const GamePak = @import("bus/GamePak.zig");
|
||||
|
@ -10,6 +11,7 @@ const Apu = @import("apu.zig").Apu;
|
|||
const DmaControllers = @import("bus/dma.zig").DmaControllers;
|
||||
const Timers = @import("bus/timer.zig").Timers;
|
||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||
const FilePaths = @import("util.zig").FilePaths;
|
||||
|
||||
const io = @import("bus/io.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
@ -33,12 +35,12 @@ io: Io,
|
|||
|
||||
sched: *Scheduler,
|
||||
|
||||
pub fn init(alloc: Allocator, sched: *Scheduler, rom_path: []const u8, bios_path: ?[]const u8, save_path: ?[]const u8) !Self {
|
||||
pub fn init(alloc: Allocator, sched: *Scheduler, dev: AudioDeviceId, paths: FilePaths) !Self {
|
||||
return Self{
|
||||
.pak = try GamePak.init(alloc, rom_path, save_path),
|
||||
.bios = try Bios.init(alloc, bios_path),
|
||||
.pak = try GamePak.init(alloc, paths.rom, paths.save),
|
||||
.bios = try Bios.init(alloc, paths.bios),
|
||||
.ppu = try Ppu.init(alloc, sched),
|
||||
.apu = Apu.init(),
|
||||
.apu = Apu.init(dev),
|
||||
.iwram = try Iwram.init(alloc),
|
||||
.ewram = try Ewram.init(alloc),
|
||||
.dma = DmaControllers.init(),
|
||||
|
@ -56,6 +58,22 @@ pub fn deinit(self: Self) void {
|
|||
self.ppu.deinit();
|
||||
}
|
||||
|
||||
pub fn handleDMATransfers(self: *Self) void {
|
||||
while (self.isDmaRunning()) {
|
||||
if (self.dma._1.step(self)) continue;
|
||||
if (self.dma._0.step(self)) continue;
|
||||
if (self.dma._2.step(self)) continue;
|
||||
if (self.dma._3.step(self)) continue;
|
||||
}
|
||||
}
|
||||
|
||||
fn isDmaRunning(self: *const Self) bool {
|
||||
return self.dma._0.active or
|
||||
self.dma._1.active or
|
||||
self.dma._2.active or
|
||||
self.dma._3.active;
|
||||
}
|
||||
|
||||
pub fn read(self: *const Self, comptime T: type, address: u32) T {
|
||||
const page = @truncate(u8, address >> 24);
|
||||
const align_addr = alignAddress(T, address);
|
||||
|
|
117
src/apu.zig
117
src/apu.zig
|
@ -1,8 +1,13 @@
|
|||
const std = @import("std");
|
||||
const SDL = @import("sdl2");
|
||||
const io = @import("bus/io.zig");
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
|
||||
const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
||||
const AudioDeviceId = SDL.SDL_AudioDeviceID;
|
||||
|
||||
const io = @import("bus/io.zig");
|
||||
const intToBytes = @import("util.zig").intToBytes;
|
||||
const log = std.log.scoped(.APU);
|
||||
|
||||
pub const Apu = struct {
|
||||
const Self = @This();
|
||||
|
@ -11,30 +16,45 @@ pub const Apu = struct {
|
|||
ch2: Tone,
|
||||
ch3: Wave,
|
||||
ch4: Noise,
|
||||
chA: DmaSound,
|
||||
chB: DmaSound,
|
||||
chA: DmaSound(.A),
|
||||
chB: DmaSound(.B),
|
||||
|
||||
bias: io.SoundBias,
|
||||
ch_vol_cnt: io.ChannelVolumeControl,
|
||||
dma_cnt: io.DmaSoundControl,
|
||||
cnt: io.SoundControl,
|
||||
|
||||
pub fn init() Self {
|
||||
dev: AudioDeviceId,
|
||||
|
||||
pub fn init(dev: AudioDeviceId) Self {
|
||||
return .{
|
||||
.ch1 = ToneSweep.init(),
|
||||
.ch2 = Tone.init(),
|
||||
.ch3 = Wave.init(),
|
||||
.ch4 = Noise.init(),
|
||||
.chA = DmaSound.init(),
|
||||
.chB = DmaSound.init(),
|
||||
.chA = DmaSound(.A).init(),
|
||||
.chB = DmaSound(.B).init(),
|
||||
|
||||
.ch_vol_cnt = .{ .raw = 0 },
|
||||
.dma_cnt = .{ .raw = 0 },
|
||||
.cnt = .{ .raw = 0 },
|
||||
.bias = .{ .raw = 0x0200 },
|
||||
|
||||
.dev = dev,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setDmaCnt(self: *Self, value: u16) void {
|
||||
const new: io.DmaSoundControl = .{ .raw = value };
|
||||
|
||||
// Reinitializing instead of resetting is fine because
|
||||
// the FIFOs I'm using are stack allocated and 0x20 bytes big
|
||||
if (new.sa_reset.read()) self.chA.fifo = SoundFifo.init();
|
||||
if (new.sb_reset.read()) self.chB.fifo = SoundFifo.init();
|
||||
|
||||
self.dma_cnt = new;
|
||||
}
|
||||
|
||||
pub fn setSoundCntX(self: *Self, value: bool) void {
|
||||
self.cnt.apu_enable.write(value);
|
||||
}
|
||||
|
@ -50,6 +70,21 @@ pub const Apu = struct {
|
|||
pub fn setBiasHigh(self: *Self, byte: u8) void {
|
||||
self.bias.raw = (@as(u16, byte) << 8) | (self.bias.raw & 0xFF);
|
||||
}
|
||||
|
||||
pub fn handleTimerOverflow(self: *Self, kind: DmaSoundKind, cpu: *Arm7tdmi) void {
|
||||
if (!self.cnt.apu_enable.read()) return;
|
||||
|
||||
const samples = switch (kind) {
|
||||
.A => blk: {
|
||||
break :blk self.chA.handleTimerOverflow(cpu, self.dma_cnt);
|
||||
},
|
||||
.B => blk: {
|
||||
break :blk self.chB.handleTimerOverflow(cpu, self.dma_cnt);
|
||||
},
|
||||
};
|
||||
|
||||
_ = SDL.SDL_QueueAudio(self.dev, &samples, 2);
|
||||
}
|
||||
};
|
||||
|
||||
const ToneSweep = struct {
|
||||
|
@ -162,16 +197,66 @@ const Noise = struct {
|
|||
};
|
||||
}
|
||||
};
|
||||
const DmaSound = struct {
|
||||
const Self = @This();
|
||||
|
||||
a: SoundFifo,
|
||||
b: SoundFifo,
|
||||
pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
fn init() Self {
|
||||
return .{
|
||||
.a = SoundFifo.init(),
|
||||
.b = SoundFifo.init(),
|
||||
};
|
||||
}
|
||||
fifo: SoundFifo,
|
||||
|
||||
kind: DmaSoundKind,
|
||||
|
||||
fn init() Self {
|
||||
return .{ .fifo = SoundFifo.init(), .kind = kind };
|
||||
}
|
||||
|
||||
pub fn push(self: *Self, value: u32) void {
|
||||
self.fifo.write(&intToBytes(u32, value)) catch {};
|
||||
}
|
||||
|
||||
pub fn pop(self: *Self) u8 {
|
||||
return self.fifo.readItem() orelse 0;
|
||||
}
|
||||
|
||||
pub fn len(self: *const Self) usize {
|
||||
return self.fifo.readableLength();
|
||||
}
|
||||
|
||||
pub fn handleTimerOverflow(self: *Self, cpu: *Arm7tdmi, cnt: io.DmaSoundControl) [2]u8 {
|
||||
const sample = self.pop();
|
||||
|
||||
var left: u8 = 0;
|
||||
var right: u8 = 0;
|
||||
var fifo_addr: u32 = undefined;
|
||||
|
||||
switch (kind) {
|
||||
.A => {
|
||||
const vol = @boolToInt(!cnt.sa_vol.read()); // if unset, vol is 50%
|
||||
if (cnt.sa_left_enable.read()) left = sample >> vol;
|
||||
if (cnt.sa_right_enable.read()) right = sample >> vol;
|
||||
|
||||
fifo_addr = 0x0400_00A0;
|
||||
},
|
||||
.B => {
|
||||
const vol = @boolToInt(!cnt.sb_vol.read()); // if unset, vol is 50%
|
||||
if (cnt.sb_left_enable.read()) left = sample >> vol;
|
||||
if (cnt.sb_right_enable.read()) right = sample >> vol;
|
||||
|
||||
fifo_addr = 0x0400_00A4;
|
||||
},
|
||||
}
|
||||
|
||||
if (self.len() <= 15) {
|
||||
cpu.bus.dma._1.enableSoundDma(fifo_addr);
|
||||
cpu.bus.dma._2.enableSoundDma(fifo_addr);
|
||||
}
|
||||
|
||||
return .{ left, right };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const DmaSoundKind = enum {
|
||||
A,
|
||||
B,
|
||||
};
|
||||
|
|
|
@ -53,10 +53,13 @@ fn DmaController(comptime id: u2) type {
|
|||
/// Internal. Word Count
|
||||
_word_count: if (id == 3) u16 else u14,
|
||||
|
||||
// Internal. FIFO Word Count
|
||||
_fifo_word_count: u8,
|
||||
|
||||
/// Some DMA Transfers are enabled during Hblank / VBlank and / or
|
||||
/// have delays. Thefore bit 15 of DMACNT isn't actually something
|
||||
/// we can use to control when we do or do not execute a step in a DMA Transfer
|
||||
enabled: bool,
|
||||
active: bool,
|
||||
|
||||
pub fn init() Self {
|
||||
return .{
|
||||
|
@ -70,7 +73,8 @@ fn DmaController(comptime id: u2) type {
|
|||
._sad = 0,
|
||||
._dad = 0,
|
||||
._word_count = 0,
|
||||
.enabled = false,
|
||||
._fifo_word_count = 4,
|
||||
.active = false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -96,7 +100,7 @@ fn DmaController(comptime id: u2) type {
|
|||
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
||||
|
||||
// Only a Start Timing of 00 has a DMA Transfer immediately begin
|
||||
self.enabled = new.start_timing.read() == 0b00;
|
||||
self.active = new.start_timing.read() == 0b00;
|
||||
}
|
||||
|
||||
self.cnt.raw = halfword;
|
||||
|
@ -108,27 +112,50 @@ fn DmaController(comptime id: u2) type {
|
|||
}
|
||||
|
||||
pub inline fn check(self: *Self, bus: *Bus) bool {
|
||||
if (!self.enabled) return false; // FIXME: Check CNT register?
|
||||
if (!self.active) return false; // FIXME: Check CNT register?
|
||||
|
||||
self.step(bus);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn step(self: *Self, bus: *Bus) void {
|
||||
@setCold(true);
|
||||
pub fn step(self: *Self, bus: *Bus) bool {
|
||||
if (!self.active) return false;
|
||||
|
||||
const sad_adj = std.meta.intToEnum(Adjustment, self.cnt.sad_adj.read()) catch unreachable;
|
||||
const dad_adj = std.meta.intToEnum(Adjustment, self.cnt.dad_adj.read()) catch unreachable;
|
||||
const is_fifo = (self.id == 1 or self.id == 2) and self.cnt.start_timing.read() == 0b11;
|
||||
|
||||
var offset: u32 = 0;
|
||||
if (self.cnt.transfer_type.read()) {
|
||||
offset = @sizeOf(u32); // 32-bit Transfer
|
||||
const word = bus.read(u32, self._sad);
|
||||
bus.write(u32, self._dad, word);
|
||||
// // if (is_fifo) {
|
||||
// // const offset = @sizeOf(u32);
|
||||
// // bus.write(u32, self._dad, bus.read(u32, self._sad));
|
||||
|
||||
// // // TODO: Deduplicate
|
||||
// // switch (sad_adj) {
|
||||
// // .Increment => self._sad +%= offset,
|
||||
// // .Decrement => self._sad -%= offset,
|
||||
// // .Fixed => {},
|
||||
|
||||
// // // TODO: Figure out correct behaviour on Illegal Source Addr Control Type
|
||||
// // .IncrementReload => std.debug.panic("panic(DmaTransfer): {} is an illegal src addr adjustment type", .{sad_adj}),
|
||||
// // }
|
||||
|
||||
// // self._fifo_word_count -= 1;
|
||||
|
||||
// // if (self._fifo_word_count == 0) {
|
||||
// // self._fifo_word_count = 4;
|
||||
// // self.active = false;
|
||||
// // }
|
||||
|
||||
// // return true;
|
||||
// // }
|
||||
|
||||
const transfer_type = self.cnt.transfer_type.read() or is_fifo;
|
||||
const offset: u32 = if (transfer_type) @sizeOf(u32) else @sizeOf(u16);
|
||||
|
||||
if (transfer_type) {
|
||||
bus.write(u32, self._dad, bus.read(u32, self._sad));
|
||||
} else {
|
||||
offset = @sizeOf(u16); // 16-bit Transfer
|
||||
const halfword = bus.read(u16, self._sad);
|
||||
bus.write(u16, self._dad, halfword);
|
||||
bus.write(u16, self._dad, bus.read(u16, self._sad));
|
||||
}
|
||||
|
||||
switch (sad_adj) {
|
||||
|
@ -140,10 +167,12 @@ fn DmaController(comptime id: u2) type {
|
|||
.IncrementReload => std.debug.panic("panic(DmaTransfer): {} is an illegal src addr adjustment type", .{sad_adj}),
|
||||
}
|
||||
|
||||
switch (dad_adj) {
|
||||
.Increment, .IncrementReload => self._dad +%= offset,
|
||||
.Decrement => self._dad -%= offset,
|
||||
.Fixed => {},
|
||||
if (!is_fifo) {
|
||||
switch (dad_adj) {
|
||||
.Increment, .IncrementReload => self._dad +%= offset,
|
||||
.Decrement => self._dad -%= offset,
|
||||
.Fixed => {},
|
||||
}
|
||||
}
|
||||
|
||||
self._word_count -= 1;
|
||||
|
@ -165,8 +194,10 @@ fn DmaController(comptime id: u2) type {
|
|||
// We want to disable our internal enabled flag regardless of repeat
|
||||
// because we only want to step A DMA that repeats during it's specific
|
||||
// timing window
|
||||
self.enabled = false;
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn isBlocking(self: *const Self) bool {
|
||||
|
@ -175,21 +206,31 @@ fn DmaController(comptime id: u2) type {
|
|||
}
|
||||
|
||||
pub fn pollBlankingDma(self: *Self, comptime kind: DmaKind) void {
|
||||
if (self.enabled) return;
|
||||
if (self.active) return;
|
||||
|
||||
switch (kind) {
|
||||
.HBlank => self.enabled = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b10,
|
||||
.VBlank => self.enabled = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b01,
|
||||
.HBlank => self.active = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b10,
|
||||
.VBlank => self.active = self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b01,
|
||||
.Immediate, .Special => {},
|
||||
}
|
||||
|
||||
if (self.cnt.repeat.read() and self.enabled) {
|
||||
if (self.cnt.repeat.read() and self.active) {
|
||||
self._word_count = if (self.word_count == 0) std.math.maxInt(@TypeOf(self._word_count)) else self.word_count;
|
||||
|
||||
const dad_adj = std.meta.intToEnum(Adjustment, self.cnt.dad_adj.read()) catch unreachable;
|
||||
if (dad_adj == .IncrementReload) self._dad = self.dad;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enableSoundDma(self: *Self, fifo_addr: u32) void {
|
||||
comptime std.debug.assert(id == 1 or id == 2);
|
||||
|
||||
if (self.cnt.enabled.read() and self.cnt.start_timing.read() == 0b11 and self.dad == fifo_addr) {
|
||||
self.active = true;
|
||||
self._word_count = 4;
|
||||
self.cnt.repeat.set();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -143,8 +143,8 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||
0x0400_001C => bus.ppu.setBgOffsets(3, value),
|
||||
|
||||
// Sound
|
||||
0x0400_00A0 => log.warn("Wrote 0x{X:0>8} to FIFO_A", .{value}),
|
||||
0x0400_00A4 => log.warn("Wrote 0x{X:0>8} to FIFO_B", .{value}),
|
||||
0x0400_00A0 => bus.apu.chA.push(value),
|
||||
0x0400_00A4 => bus.apu.chB.push(value),
|
||||
|
||||
// DMA Transfers
|
||||
0x0400_00B0 => bus.dma._0.writeSad(value),
|
||||
|
@ -208,7 +208,7 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||
|
||||
// Sound
|
||||
0x0400_0080 => bus.apu.ch_vol_cnt.raw = value,
|
||||
0x0400_0082 => bus.apu.dma_cnt.raw = value,
|
||||
0x0400_0082 => bus.apu.setDmaCnt(value),
|
||||
0x0400_0084 => bus.apu.setSoundCntX(value >> 7 & 1 == 1),
|
||||
0x0400_0088 => bus.apu.bias.raw = value,
|
||||
|
||||
|
|
|
@ -103,6 +103,17 @@ fn Timer(comptime id: u2) type {
|
|||
cpu.handleInterrupt();
|
||||
}
|
||||
|
||||
// DMA Sound Things
|
||||
if (id == 0 or id == 1) {
|
||||
const apu = &cpu.bus.apu;
|
||||
|
||||
const a_tim = @boolToInt(apu.dma_cnt.sa_timer.read());
|
||||
const b_tim = @boolToInt(apu.dma_cnt.sb_timer.read());
|
||||
|
||||
if (a_tim == id) apu.handleTimerOverflow(.A, cpu);
|
||||
if (b_tim == id) apu.handleTimerOverflow(.B, cpu);
|
||||
}
|
||||
|
||||
// Perform Cascade Behaviour
|
||||
switch (id) {
|
||||
0 => if (tim._1.cnt.cascade.read()) {
|
||||
|
|
18
src/cpu.zig
18
src/cpu.zig
|
@ -246,15 +246,6 @@ pub const Arm7tdmi = struct {
|
|||
}
|
||||
|
||||
pub fn step(self: *Self) void {
|
||||
// If we're processing a DMA (not Sound or Blanking) the CPU is disabled
|
||||
if (self.handleDMATransfers()) return;
|
||||
|
||||
// If we're halted, the cpu is disabled
|
||||
if (self.bus.io.haltcnt == .Halt) {
|
||||
self.sched.tick += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.cpsr.t.read()) {
|
||||
const opcode = self.thumbFetch();
|
||||
if (enable_logging) if (self.log_file) |file| self.debug_log(file, opcode);
|
||||
|
@ -296,15 +287,6 @@ pub const Arm7tdmi = struct {
|
|||
}
|
||||
}
|
||||
|
||||
fn handleDMATransfers(self: *Self) bool {
|
||||
if (self.bus.dma._0.check(self.bus)) return self.bus.dma._0.isBlocking();
|
||||
if (self.bus.dma._1.check(self.bus)) return self.bus.dma._1.isBlocking();
|
||||
if (self.bus.dma._2.check(self.bus)) return self.bus.dma._2.isBlocking();
|
||||
if (self.bus.dma._3.check(self.bus)) return self.bus.dma._3.isBlocking();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn thumbFetch(self: *Self) u16 {
|
||||
defer self.r[15] += 2;
|
||||
return self.bus.read(u16, self.r[15]);
|
||||
|
|
|
@ -46,7 +46,9 @@ pub fn runFrame(sched: *Scheduler, cpu: *Arm7tdmi, bus: *Bus) void {
|
|||
const frame_end = sched.tick + cycles_per_frame;
|
||||
|
||||
while (sched.tick < frame_end) {
|
||||
cpu.step();
|
||||
if (bus.io.haltcnt == .Halt) sched.tick += 1;
|
||||
if (bus.io.haltcnt == .Execute) cpu.step();
|
||||
bus.handleDMATransfers();
|
||||
|
||||
while (sched.tick >= sched.nextTimestamp()) {
|
||||
sched.handleEvent(cpu, bus);
|
||||
|
|
29
src/main.zig
29
src/main.zig
|
@ -66,11 +66,33 @@ pub fn main() anyerror!void {
|
|||
defer if (save_path) |path| alloc.free(path);
|
||||
log.info("Save Path: {s}", .{save_path});
|
||||
|
||||
// Initialize SDL
|
||||
const status = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
||||
defer SDL.SDL_Quit();
|
||||
if (status < 0) sdlPanic();
|
||||
|
||||
// Initialize SDL Audio
|
||||
var have: SDL.SDL_AudioSpec = undefined;
|
||||
var want = std.mem.zeroes(SDL.SDL_AudioSpec);
|
||||
want.freq = 32768;
|
||||
want.format = SDL.AUDIO_S8;
|
||||
want.channels = 2;
|
||||
want.samples = 0x200;
|
||||
want.callback = null;
|
||||
|
||||
const audio_dev = SDL.SDL_OpenAudioDevice(null, 0, &want, &have, 0);
|
||||
defer SDL.SDL_CloseAudioDevice(audio_dev);
|
||||
if (audio_dev == 0) sdlPanic();
|
||||
|
||||
// Start Playback on the Audio evice
|
||||
SDL.SDL_PauseAudioDevice(audio_dev, 0);
|
||||
|
||||
// Initialize Emulator
|
||||
var scheduler = Scheduler.init(alloc);
|
||||
defer scheduler.deinit();
|
||||
|
||||
var bus = try Bus.init(alloc, &scheduler, rom_path, bios_path, save_path);
|
||||
const paths = .{ .bios = bios_path, .rom = rom_path, .save = save_path };
|
||||
var bus = try Bus.init(alloc, &scheduler, audio_dev, paths);
|
||||
defer bus.deinit();
|
||||
|
||||
var cpu = Arm7tdmi.init(&scheduler, &bus);
|
||||
|
@ -91,11 +113,6 @@ pub fn main() anyerror!void {
|
|||
const emu_thread = try Thread.spawn(.{}, emu.run, .{ .LimitedFPS, &quit, &emu_rate, &scheduler, &cpu, &bus });
|
||||
defer emu_thread.join();
|
||||
|
||||
// Initialize SDL
|
||||
const status = SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO | SDL.SDL_INIT_GAMECONTROLLER);
|
||||
if (status < 0) sdlPanic();
|
||||
defer SDL.SDL_Quit();
|
||||
|
||||
const title = correctTitle(bus.pak.title);
|
||||
|
||||
var title_buf: [0x20]u8 = std.mem.zeroes([0x20]u8);
|
||||
|
|
|
@ -101,3 +101,9 @@ pub fn fixTitle(alloc: std.mem.Allocator, title: [12]u8) ![]u8 {
|
|||
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub const FilePaths = struct {
|
||||
rom: []const u8,
|
||||
bios: ?[]const u8,
|
||||
save: ?[]const u8,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue