Compare commits
9 Commits
8b1fd64c16
...
965c27a188
Author | SHA1 | Date |
---|---|---|
Rekai Nyangadzayi Musuka | 965c27a188 | |
Rekai Nyangadzayi Musuka | 612474cb34 | |
Rekai Nyangadzayi Musuka | 95ca8a52d8 | |
Rekai Nyangadzayi Musuka | 3dedf4661f | |
Rekai Nyangadzayi Musuka | 3466bf6c0a | |
Rekai Nyangadzayi Musuka | fbe3de0eb3 | |
Rekai Nyangadzayi Musuka | 4af144fca2 | |
Rekai Nyangadzayi Musuka | 9a8aaba1ab | |
Rekai Nyangadzayi Musuka | fa3b9c21b9 |
194
src/Gui.zig
194
src/Gui.zig
|
@ -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));
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
const std = @import("std");
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const DateTime = @import("datetime").datetime.Datetime;
|
||||
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
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;
|
||||
|
@ -239,463 +240,3 @@ test "OOB Access" {
|
|||
std.debug.assert(pak.get(4) == 0x02); // 0x0002
|
||||
std.debug.assert(pak.get(5) == 0x00);
|
||||
}
|
||||
|
||||
/// GPIO Register Implementation
|
||||
const Gpio = struct {
|
||||
const This = @This();
|
||||
|
||||
data: u4,
|
||||
direction: u4,
|
||||
cnt: u1,
|
||||
|
||||
device: Device,
|
||||
|
||||
const Device = struct {
|
||||
ptr: ?*anyopaque,
|
||||
kind: Kind, // TODO: Make comptime known?
|
||||
|
||||
const Kind = enum { Rtc, None };
|
||||
|
||||
fn step(self: *Device, value: u4) u4 {
|
||||
return switch (self.kind) {
|
||||
.Rtc => blk: {
|
||||
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
|
||||
break :blk clock.step(Clock.Data{ .raw = value });
|
||||
},
|
||||
.None => value,
|
||||
};
|
||||
}
|
||||
|
||||
fn init(kind: Kind, ptr: ?*anyopaque) Device {
|
||||
return .{ .kind = kind, .ptr = ptr };
|
||||
}
|
||||
};
|
||||
|
||||
const Register = enum {
|
||||
Data,
|
||||
Direction,
|
||||
Control,
|
||||
};
|
||||
|
||||
fn init(allocator: Allocator, cpu: *Arm7tdmi, kind: Device.Kind) !*This {
|
||||
log.info("Device: {}", .{kind});
|
||||
|
||||
const self = try allocator.create(This);
|
||||
self.* = .{
|
||||
.data = 0b0000,
|
||||
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
|
||||
.cnt = 0b0,
|
||||
|
||||
.device = switch (kind) {
|
||||
.Rtc => blk: {
|
||||
const clock = try allocator.create(Clock);
|
||||
clock.init(cpu, self);
|
||||
|
||||
break :blk Device{ .kind = kind, .ptr = clock };
|
||||
},
|
||||
.None => Device{ .kind = kind, .ptr = null },
|
||||
},
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn deinit(self: *This, allocator: Allocator) void {
|
||||
switch (self.device.kind) {
|
||||
.Rtc => {
|
||||
allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?)));
|
||||
},
|
||||
.None => {},
|
||||
}
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
fn write(self: *This, comptime reg: Register, value: if (reg == .Control) u1 else u4) void {
|
||||
switch (reg) {
|
||||
.Data => {
|
||||
const masked_value = value & self.direction;
|
||||
|
||||
// The value which is actually stored in the GPIO register
|
||||
// might be modified by the device implementing the GPIO interface e.g. RTC reads
|
||||
self.data = self.device.step(masked_value);
|
||||
},
|
||||
.Direction => self.direction = value,
|
||||
.Control => self.cnt = value,
|
||||
}
|
||||
}
|
||||
|
||||
fn read(self: *const This, comptime reg: Register) if (reg == .Control) u1 else u4 {
|
||||
if (self.cnt == 0) return 0;
|
||||
|
||||
return switch (reg) {
|
||||
.Data => self.data & ~self.direction,
|
||||
.Direction => self.direction,
|
||||
.Control => self.cnt,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// GBA Real Time Clock
|
||||
pub const Clock = struct {
|
||||
const This = @This();
|
||||
|
||||
writer: Writer,
|
||||
reader: Reader,
|
||||
state: State,
|
||||
cnt: Control,
|
||||
|
||||
year: u8,
|
||||
month: u5,
|
||||
day: u6,
|
||||
weekday: u3,
|
||||
hour: u6,
|
||||
minute: u7,
|
||||
second: u7,
|
||||
|
||||
cpu: *Arm7tdmi,
|
||||
gpio: *const Gpio,
|
||||
|
||||
const Register = enum {
|
||||
Control,
|
||||
DateTime,
|
||||
Time,
|
||||
};
|
||||
|
||||
const State = union(enum) {
|
||||
Idle,
|
||||
Command,
|
||||
Write: Register,
|
||||
Read: Register,
|
||||
};
|
||||
|
||||
const Reader = struct {
|
||||
i: u4,
|
||||
count: u8,
|
||||
|
||||
/// Reads a bit from RTC registers. Which bit it reads is dependent on
|
||||
///
|
||||
/// 1. The RTC State Machine, whitch tells us which register we're accessing
|
||||
/// 2. A `count`, which keeps track of which byte is currently being read
|
||||
/// 3. An index, which keeps track of which bit of the byte determined by `count` is being read
|
||||
fn read(self: *Reader, clock: *const Clock, register: Register) u1 {
|
||||
const idx = @intCast(u3, self.i);
|
||||
defer self.i += 1;
|
||||
|
||||
// FIXME: What do I do about the unused bits?
|
||||
return switch (register) {
|
||||
.Control => @truncate(u1, switch (self.count) {
|
||||
0 => clock.cnt.raw >> idx,
|
||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
||||
}),
|
||||
.DateTime => @truncate(u1, switch (self.count) {
|
||||
// Date
|
||||
0 => clock.year >> idx,
|
||||
1 => @as(u8, clock.month) >> idx,
|
||||
2 => @as(u8, clock.day) >> idx,
|
||||
3 => @as(u8, clock.weekday) >> idx,
|
||||
|
||||
// Time
|
||||
4 => @as(u8, clock.hour) >> idx,
|
||||
5 => @as(u8, clock.minute) >> idx,
|
||||
6 => @as(u8, clock.second) >> idx,
|
||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }),
|
||||
}),
|
||||
.Time => @truncate(u1, switch (self.count) {
|
||||
0 => @as(u8, clock.hour) >> idx,
|
||||
1 => @as(u8, clock.minute) >> idx,
|
||||
2 => @as(u8, clock.second) >> idx,
|
||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 3 bytes)", .{ self.count, register }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/// Is true when a Reader has read a u8's worth of bits
|
||||
fn finished(self: *const Reader) bool {
|
||||
return self.i >= 8;
|
||||
}
|
||||
|
||||
/// Resets the index used to shift bits out of RTC registers
|
||||
/// and `count`, which is used to keep track of which byte we're reading
|
||||
/// is incremeneted
|
||||
fn lap(self: *Reader) void {
|
||||
self.i = 0;
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
/// Resets the state of a `Reader` in preparation for a future
|
||||
/// read command
|
||||
fn reset(self: *Reader) void {
|
||||
self.i = 0;
|
||||
self.count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const Writer = struct {
|
||||
buf: u8,
|
||||
i: u4,
|
||||
|
||||
/// The Number of bytes written since last reset
|
||||
count: u8,
|
||||
|
||||
/// Append a bit to the internal bit buffer (aka an integer)
|
||||
fn push(self: *Writer, value: u1) void {
|
||||
const idx = @intCast(u3, self.i);
|
||||
self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx;
|
||||
self.i += 1;
|
||||
}
|
||||
|
||||
/// Takes the contents of the internal buffer and writes it to an RTC register
|
||||
/// Where it writes to is dependent on:
|
||||
///
|
||||
/// 1. The RTC State Machine, whitch tells us which register we're accessing
|
||||
/// 2. A `count`, which keeps track of which byte is currently being read
|
||||
fn write(self: *const Writer, clock: *Clock, register: Register) void {
|
||||
// FIXME: What do do about unused bits?
|
||||
switch (register) {
|
||||
.Control => switch (self.count) {
|
||||
0 => clock.cnt.raw = (clock.cnt.raw & 0x80) | (self.buf & 0x7F), // Bit 7 read-only
|
||||
else => std.debug.panic("Tried to write to byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
||||
},
|
||||
.DateTime, .Time => log.debug("RTC: Ignoring {} write", .{register}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is true when 8 bits have been shifted into the internal buffer
|
||||
fn finished(self: *const Writer) bool {
|
||||
return self.i >= 8;
|
||||
}
|
||||
|
||||
/// Resets the internal buffer
|
||||
/// resets the index used to shift bits into the internal buffer
|
||||
/// increments `count` (which keeps track of byte offsets) by one
|
||||
fn lap(self: *Writer) void {
|
||||
self.buf = 0;
|
||||
self.i = 0;
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
/// Resets `Writer` to a clean state in preparation for a future write command
|
||||
fn reset(self: *Writer) void {
|
||||
self.buf = 0;
|
||||
self.i = 0;
|
||||
self.count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const Data = extern union {
|
||||
sck: Bit(u8, 0),
|
||||
sio: Bit(u8, 1),
|
||||
cs: Bit(u8, 2),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
const Control = extern union {
|
||||
/// Unknown, value should be preserved though
|
||||
unk: Bit(u8, 1),
|
||||
/// Per-minute IRQ
|
||||
/// If set, fire a Gamepak IRQ every 30s,
|
||||
irq: Bit(u8, 3),
|
||||
/// 12/24 Hour Bit
|
||||
/// If set, 12h mode
|
||||
/// If cleared, 24h mode
|
||||
mode: Bit(u8, 6),
|
||||
/// Read-Only, bit cleared on read
|
||||
/// If is set, means that there has been a failure / time has been lost
|
||||
off: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
fn init(ptr: *This, cpu: *Arm7tdmi, gpio: *const Gpio) void {
|
||||
ptr.* = .{
|
||||
.writer = .{ .buf = 0, .i = 0, .count = 0 },
|
||||
.reader = .{ .i = 0, .count = 0 },
|
||||
.state = .Idle,
|
||||
.cnt = .{ .raw = 0 },
|
||||
.year = 0x01,
|
||||
.month = 0x6,
|
||||
.day = 0x13,
|
||||
.weekday = 0x3,
|
||||
.hour = 0x23,
|
||||
.minute = 0x59,
|
||||
.second = 0x59,
|
||||
.cpu = cpu,
|
||||
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
|
||||
};
|
||||
|
||||
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
|
||||
}
|
||||
|
||||
pub fn updateTime(self: *This, late: u64) void {
|
||||
self.cpu.sched.push(.RealTimeClock, (1 << 24) -| late); // Reschedule
|
||||
|
||||
const now = DateTime.now();
|
||||
self.year = toBcd(u8, @intCast(u8, now.date.year - 2000));
|
||||
self.month = toBcd(u5, now.date.month);
|
||||
self.day = toBcd(u6, now.date.day);
|
||||
self.weekday = toBcd(u3, (now.date.weekday() + 1) % 7); // API is Monday = 0, Sunday = 6. We want Sunday = 0, Saturday = 6
|
||||
self.hour = toBcd(u6, now.time.hour);
|
||||
self.minute = toBcd(u7, now.time.minute);
|
||||
self.second = toBcd(u7, now.time.second);
|
||||
}
|
||||
|
||||
fn step(self: *This, value: Data) u4 {
|
||||
const cache: Data = .{ .raw = self.gpio.data };
|
||||
|
||||
return switch (self.state) {
|
||||
.Idle => blk: {
|
||||
// FIXME: Maybe check incoming value to see if SCK is also high?
|
||||
if (cache.sck.read()) {
|
||||
if (!cache.cs.read() and value.cs.read()) {
|
||||
log.debug("RTC: Entering Command Mode", .{});
|
||||
self.state = .Command;
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
},
|
||||
.Command => blk: {
|
||||
if (!value.cs.read()) log.err("RTC: Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||
|
||||
// If SCK rises, sample SIO
|
||||
if (!cache.sck.read() and value.sck.read()) {
|
||||
self.writer.push(@boolToInt(value.sio.read()));
|
||||
|
||||
if (self.writer.finished()) {
|
||||
self.state = self.processCommand(self.writer.buf);
|
||||
self.writer.reset();
|
||||
|
||||
log.debug("RTC: Switching to {}", .{self.state});
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
},
|
||||
.Write => |register| blk: {
|
||||
if (!value.cs.read()) log.err("RTC: Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||
|
||||
// If SCK rises, sample SIO
|
||||
if (!cache.sck.read() and value.sck.read()) {
|
||||
self.writer.push(@boolToInt(value.sio.read()));
|
||||
|
||||
const register_width: u32 = switch (register) {
|
||||
.Control => 1,
|
||||
.DateTime => 7,
|
||||
.Time => 3,
|
||||
};
|
||||
|
||||
if (self.writer.finished()) {
|
||||
self.writer.write(self, register); // write inner buffer to RTC register
|
||||
self.writer.lap();
|
||||
|
||||
if (self.writer.count == register_width) {
|
||||
self.writer.reset();
|
||||
self.state = .Idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
},
|
||||
.Read => |register| blk: {
|
||||
if (!value.cs.read()) log.err("RTC: Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||
var ret = value;
|
||||
|
||||
// if SCK rises, sample SIO
|
||||
if (!cache.sck.read() and value.sck.read()) {
|
||||
ret.sio.write(self.reader.read(self, register) == 0b1);
|
||||
|
||||
const register_width: u32 = switch (register) {
|
||||
.Control => 1,
|
||||
.DateTime => 7,
|
||||
.Time => 3,
|
||||
};
|
||||
|
||||
if (self.reader.finished()) {
|
||||
self.reader.lap();
|
||||
|
||||
if (self.reader.count == register_width) {
|
||||
self.reader.reset();
|
||||
self.state = .Idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, ret.raw);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn reset(self: *This) void {
|
||||
// mGBA and NBA only zero the control register. We will do the same
|
||||
log.debug("RTC: Reset (control register was zeroed)", .{});
|
||||
|
||||
self.cnt.raw = 0;
|
||||
}
|
||||
|
||||
fn irq(self: *This) void {
|
||||
// TODO: Confirm that this is the right behaviour
|
||||
log.debug("RTC: Force GamePak IRQ", .{});
|
||||
|
||||
self.cpu.bus.io.irq.game_pak.set();
|
||||
self.cpu.handleInterrupt();
|
||||
}
|
||||
|
||||
fn processCommand(self: *This, raw_command: u8) State {
|
||||
const command = blk: {
|
||||
// If High Nybble is 0x6, no need to switch the endianness
|
||||
if (raw_command >> 4 & 0xF == 0x6) break :blk raw_command;
|
||||
|
||||
// Turns out reversing the order of bits isn't trivial at all
|
||||
// https://stackoverflow.com/questions/2602823/in-c-c-whats-the-simplest-way-to-reverse-the-order-of-bits-in-a-byte
|
||||
var ret = raw_command;
|
||||
ret = (ret & 0xF0) >> 4 | (ret & 0x0F) << 4;
|
||||
ret = (ret & 0xCC) >> 2 | (ret & 0x33) << 2;
|
||||
ret = (ret & 0xAA) >> 1 | (ret & 0x55) << 1;
|
||||
|
||||
break :blk ret;
|
||||
};
|
||||
log.debug("RTC: Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command });
|
||||
|
||||
const is_write = command & 1 == 0;
|
||||
const rtc_register = @truncate(u3, command >> 1 & 0x7);
|
||||
|
||||
if (is_write) {
|
||||
return switch (rtc_register) {
|
||||
0 => blk: {
|
||||
self.reset();
|
||||
break :blk .Idle;
|
||||
},
|
||||
1 => .{ .Write = .Control },
|
||||
2 => .{ .Write = .DateTime },
|
||||
3 => .{ .Write = .Time },
|
||||
6 => blk: {
|
||||
self.irq();
|
||||
break :blk .Idle;
|
||||
},
|
||||
4, 5, 7 => .Idle,
|
||||
};
|
||||
} else {
|
||||
return switch (rtc_register) {
|
||||
1 => .{ .Read = .Control },
|
||||
2 => .{ .Read = .DateTime },
|
||||
3 => .{ .Read = .Time },
|
||||
0, 4, 5, 6, 7 => .Idle, // Do Nothing
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn toBcd(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);
|
||||
}
|
||||
|
|
|
@ -3,11 +3,12 @@ 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{
|
||||
const backup_kinds = [6]Needle{
|
||||
.{ .str = "EEPROM_V", .kind = .Eeprom },
|
||||
.{ .str = "SRAM_V", .kind = .Sram },
|
||||
.{ .str = "SRAM_F_V", .kind = .Sram },
|
||||
.{ .str = "FLASH_V", .kind = .Flash },
|
||||
.{ .str = "FLASH512_V", .kind = .Flash },
|
||||
.{ .str = "FLASH1M_V", .kind = .Flash1M },
|
||||
|
@ -128,7 +129,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" });
|
||||
|
|
|
@ -0,0 +1,463 @@
|
|||
const std = @import("std");
|
||||
const Bit = @import("bitfield").Bit;
|
||||
const Bitfield = @import("bitfield").Bitfield;
|
||||
const DateTime = @import("datetime").datetime.Datetime;
|
||||
|
||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// GPIO Register Implementation
|
||||
pub const Gpio = struct {
|
||||
const Self = @This();
|
||||
const log = std.log.scoped(.Gpio);
|
||||
|
||||
data: u4,
|
||||
direction: u4,
|
||||
cnt: u1,
|
||||
|
||||
device: Device,
|
||||
|
||||
const Register = enum { Data, Direction, Control };
|
||||
|
||||
pub const Device = struct {
|
||||
ptr: ?*anyopaque,
|
||||
kind: Kind, // TODO: Make comptime known?
|
||||
|
||||
pub const Kind = enum { Rtc, None };
|
||||
|
||||
fn step(self: *Device, value: u4) u4 {
|
||||
return switch (self.kind) {
|
||||
.Rtc => blk: {
|
||||
const clock = @ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.ptr.?));
|
||||
break :blk clock.step(Clock.Data{ .raw = value });
|
||||
},
|
||||
.None => value,
|
||||
};
|
||||
}
|
||||
|
||||
fn init(kind: Kind, ptr: ?*anyopaque) Device {
|
||||
return .{ .kind = kind, .ptr = ptr };
|
||||
}
|
||||
};
|
||||
|
||||
pub fn write(self: *Self, comptime reg: Register, value: if (reg == .Control) u1 else u4) void {
|
||||
switch (reg) {
|
||||
.Data => {
|
||||
const masked_value = value & self.direction;
|
||||
|
||||
// The value which is actually stored in the GPIO register
|
||||
// might be modified by the device implementing the GPIO interface e.g. RTC reads
|
||||
self.data = self.device.step(masked_value);
|
||||
},
|
||||
.Direction => self.direction = value,
|
||||
.Control => self.cnt = value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(self: *const Self, comptime reg: Register) if (reg == .Control) u1 else u4 {
|
||||
if (self.cnt == 0) return 0;
|
||||
|
||||
return switch (reg) {
|
||||
.Data => self.data & ~self.direction,
|
||||
.Direction => self.direction,
|
||||
.Control => self.cnt,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, kind: Device.Kind) !*Self {
|
||||
log.info("Device: {}", .{kind});
|
||||
|
||||
const self = try allocator.create(Self);
|
||||
self.* = .{
|
||||
.data = 0b0000,
|
||||
.direction = 0b1111, // TODO: What is GPIO DIrection set to by default?
|
||||
.cnt = 0b0,
|
||||
|
||||
.device = switch (kind) {
|
||||
.Rtc => blk: {
|
||||
const clock = try allocator.create(Clock);
|
||||
clock.init(cpu, self);
|
||||
|
||||
break :blk Device{ .kind = kind, .ptr = clock };
|
||||
},
|
||||
.None => Device{ .kind = kind, .ptr = null },
|
||||
},
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||
switch (self.device.kind) {
|
||||
.Rtc => allocator.destroy(@ptrCast(*Clock, @alignCast(@alignOf(*Clock), self.device.ptr.?))),
|
||||
.None => {},
|
||||
}
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/// GBA Real Time Clock
|
||||
pub const Clock = struct {
|
||||
const Self = @This();
|
||||
const log = std.log.scoped(.Rtc);
|
||||
|
||||
writer: Writer,
|
||||
reader: Reader,
|
||||
state: State,
|
||||
cnt: Control,
|
||||
|
||||
year: u8,
|
||||
month: u5,
|
||||
day: u6,
|
||||
weekday: u3,
|
||||
hour: u6,
|
||||
minute: u7,
|
||||
second: u7,
|
||||
|
||||
cpu: *Arm7tdmi,
|
||||
gpio: *const Gpio,
|
||||
|
||||
const Register = enum {
|
||||
Control,
|
||||
DateTime,
|
||||
Time,
|
||||
};
|
||||
|
||||
const State = union(enum) {
|
||||
Idle,
|
||||
Command,
|
||||
Write: Register,
|
||||
Read: Register,
|
||||
};
|
||||
|
||||
const Reader = struct {
|
||||
i: u4,
|
||||
count: u8,
|
||||
|
||||
/// Reads a bit from RTC registers. Which bit it reads is dependent on
|
||||
///
|
||||
/// 1. The RTC State Machine, whitch tells us which register we're accessing
|
||||
/// 2. A `count`, which keeps track of which byte is currently being read
|
||||
/// 3. An index, which keeps track of which bit of the byte determined by `count` is being read
|
||||
fn read(self: *Reader, clock: *const Clock, register: Register) u1 {
|
||||
const idx = @intCast(u3, self.i);
|
||||
defer self.i += 1;
|
||||
|
||||
// FIXME: What do I do about the unused bits?
|
||||
return switch (register) {
|
||||
.Control => @truncate(u1, switch (self.count) {
|
||||
0 => clock.cnt.raw >> idx,
|
||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
||||
}),
|
||||
.DateTime => @truncate(u1, switch (self.count) {
|
||||
// Date
|
||||
0 => clock.year >> idx,
|
||||
1 => @as(u8, clock.month) >> idx,
|
||||
2 => @as(u8, clock.day) >> idx,
|
||||
3 => @as(u8, clock.weekday) >> idx,
|
||||
|
||||
// Time
|
||||
4 => @as(u8, clock.hour) >> idx,
|
||||
5 => @as(u8, clock.minute) >> idx,
|
||||
6 => @as(u8, clock.second) >> idx,
|
||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 7 bytes)", .{ self.count, register }),
|
||||
}),
|
||||
.Time => @truncate(u1, switch (self.count) {
|
||||
0 => @as(u8, clock.hour) >> idx,
|
||||
1 => @as(u8, clock.minute) >> idx,
|
||||
2 => @as(u8, clock.second) >> idx,
|
||||
else => std.debug.panic("Tried to read from byte #{} of {} (hint: there's only 3 bytes)", .{ self.count, register }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/// Is true when a Reader has read a u8's worth of bits
|
||||
fn finished(self: *const Reader) bool {
|
||||
return self.i >= 8;
|
||||
}
|
||||
|
||||
/// Resets the index used to shift bits out of RTC registers
|
||||
/// and `count`, which is used to keep track of which byte we're reading
|
||||
/// is incremeneted
|
||||
fn lap(self: *Reader) void {
|
||||
self.i = 0;
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
/// Resets the state of a `Reader` in preparation for a future
|
||||
/// read command
|
||||
fn reset(self: *Reader) void {
|
||||
self.i = 0;
|
||||
self.count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const Writer = struct {
|
||||
buf: u8,
|
||||
i: u4,
|
||||
|
||||
/// The Number of bytes written since last reset
|
||||
count: u8,
|
||||
|
||||
/// Append a bit to the internal bit buffer (aka an integer)
|
||||
fn push(self: *Writer, value: u1) void {
|
||||
const idx = @intCast(u3, self.i);
|
||||
self.buf = (self.buf & ~(@as(u8, 1) << idx)) | @as(u8, value) << idx;
|
||||
self.i += 1;
|
||||
}
|
||||
|
||||
/// Takes the contents of the internal buffer and writes it to an RTC register
|
||||
/// Where it writes to is dependent on:
|
||||
///
|
||||
/// 1. The RTC State Machine, whitch tells us which register we're accessing
|
||||
/// 2. A `count`, which keeps track of which byte is currently being read
|
||||
fn write(self: *const Writer, clock: *Clock, register: Register) void {
|
||||
// FIXME: What do do about unused bits?
|
||||
switch (register) {
|
||||
.Control => switch (self.count) {
|
||||
0 => clock.cnt.raw = (clock.cnt.raw & 0x80) | (self.buf & 0x7F), // Bit 7 read-only
|
||||
else => std.debug.panic("Tried to write to byte #{} of {} (hint: there's only 1 byte)", .{ self.count, register }),
|
||||
},
|
||||
.DateTime, .Time => log.debug("Ignoring {} write", .{register}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is true when 8 bits have been shifted into the internal buffer
|
||||
fn finished(self: *const Writer) bool {
|
||||
return self.i >= 8;
|
||||
}
|
||||
|
||||
/// Resets the internal buffer
|
||||
/// resets the index used to shift bits into the internal buffer
|
||||
/// increments `count` (which keeps track of byte offsets) by one
|
||||
fn lap(self: *Writer) void {
|
||||
self.buf = 0;
|
||||
self.i = 0;
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
/// Resets `Writer` to a clean state in preparation for a future write command
|
||||
fn reset(self: *Writer) void {
|
||||
self.buf = 0;
|
||||
self.i = 0;
|
||||
self.count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const Data = extern union {
|
||||
sck: Bit(u8, 0),
|
||||
sio: Bit(u8, 1),
|
||||
cs: Bit(u8, 2),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
const Control = extern union {
|
||||
/// Unknown, value should be preserved though
|
||||
unk: Bit(u8, 1),
|
||||
/// Per-minute IRQ
|
||||
/// If set, fire a Gamepak IRQ every 30s,
|
||||
irq: Bit(u8, 3),
|
||||
/// 12/24 Hour Bit
|
||||
/// If set, 12h mode
|
||||
/// If cleared, 24h mode
|
||||
mode: Bit(u8, 6),
|
||||
/// Read-Only, bit cleared on read
|
||||
/// If is set, means that there has been a failure / time has been lost
|
||||
off: Bit(u8, 7),
|
||||
raw: u8,
|
||||
};
|
||||
|
||||
fn init(ptr: *Self, cpu: *Arm7tdmi, gpio: *const Gpio) void {
|
||||
ptr.* = .{
|
||||
.writer = .{ .buf = 0, .i = 0, .count = 0 },
|
||||
.reader = .{ .i = 0, .count = 0 },
|
||||
.state = .Idle,
|
||||
.cnt = .{ .raw = 0 },
|
||||
.year = 0x01,
|
||||
.month = 0x6,
|
||||
.day = 0x13,
|
||||
.weekday = 0x3,
|
||||
.hour = 0x23,
|
||||
.minute = 0x59,
|
||||
.second = 0x59,
|
||||
.cpu = cpu,
|
||||
.gpio = gpio, // Can't use Arm7tdmi ptr b/c not initialized yet
|
||||
};
|
||||
|
||||
cpu.sched.push(.RealTimeClock, 1 << 24); // Every Second
|
||||
}
|
||||
|
||||
pub fn updateTime(self: *Self, late: u64) void {
|
||||
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);
|
||||
}
|
||||
|
||||
fn step(self: *Self, value: Data) u4 {
|
||||
const cache: Data = .{ .raw = self.gpio.data };
|
||||
|
||||
return switch (self.state) {
|
||||
.Idle => blk: {
|
||||
// FIXME: Maybe check incoming value to see if SCK is also high?
|
||||
if (cache.sck.read()) {
|
||||
if (!cache.cs.read() and value.cs.read()) {
|
||||
log.debug("Entering Command Mode", .{});
|
||||
self.state = .Command;
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
},
|
||||
.Command => blk: {
|
||||
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||
|
||||
// If SCK rises, sample SIO
|
||||
if (!cache.sck.read() and value.sck.read()) {
|
||||
self.writer.push(@boolToInt(value.sio.read()));
|
||||
|
||||
if (self.writer.finished()) {
|
||||
self.state = self.processCommand(self.writer.buf);
|
||||
self.writer.reset();
|
||||
|
||||
log.debug("Switching to {}", .{self.state});
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
},
|
||||
.Write => |register| blk: {
|
||||
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||
|
||||
// If SCK rises, sample SIO
|
||||
if (!cache.sck.read() and value.sck.read()) {
|
||||
self.writer.push(@boolToInt(value.sio.read()));
|
||||
|
||||
const register_width: u32 = switch (register) {
|
||||
.Control => 1,
|
||||
.DateTime => 7,
|
||||
.Time => 3,
|
||||
};
|
||||
|
||||
if (self.writer.finished()) {
|
||||
self.writer.write(self, register); // write inner buffer to RTC register
|
||||
self.writer.lap();
|
||||
|
||||
if (self.writer.count == register_width) {
|
||||
self.writer.reset();
|
||||
self.state = .Idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, value.raw);
|
||||
},
|
||||
.Read => |register| blk: {
|
||||
if (!value.cs.read()) log.err("Expected CS to be set during {}, however CS was cleared", .{self.state});
|
||||
var ret = value;
|
||||
|
||||
// if SCK rises, sample SIO
|
||||
if (!cache.sck.read() and value.sck.read()) {
|
||||
ret.sio.write(self.reader.read(self, register) == 0b1);
|
||||
|
||||
const register_width: u32 = switch (register) {
|
||||
.Control => 1,
|
||||
.DateTime => 7,
|
||||
.Time => 3,
|
||||
};
|
||||
|
||||
if (self.reader.finished()) {
|
||||
self.reader.lap();
|
||||
|
||||
if (self.reader.count == register_width) {
|
||||
self.reader.reset();
|
||||
self.state = .Idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break :blk @truncate(u4, ret.raw);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn reset(self: *Self) void {
|
||||
// mGBA and NBA only zero the control register. We will do the same
|
||||
log.debug("Reset (control register was zeroed)", .{});
|
||||
|
||||
self.cnt.raw = 0;
|
||||
}
|
||||
|
||||
fn irq(self: *Self) void {
|
||||
// TODO: Confirm that this is the right behaviour
|
||||
log.debug("Force GamePak IRQ", .{});
|
||||
|
||||
self.cpu.bus.io.irq.game_pak.set();
|
||||
self.cpu.handleInterrupt();
|
||||
}
|
||||
|
||||
fn processCommand(self: *Self, raw_command: u8) State {
|
||||
const command = blk: {
|
||||
// If High Nybble is 0x6, no need to switch the endianness
|
||||
if (raw_command >> 4 & 0xF == 0x6) break :blk raw_command;
|
||||
|
||||
// Turns out reversing the order of bits isn't trivial at all
|
||||
// https://stackoverflow.com/questions/2602823/in-c-c-whats-the-simplest-way-to-reverse-the-order-of-bits-in-a-byte
|
||||
var ret = raw_command;
|
||||
ret = (ret & 0xF0) >> 4 | (ret & 0x0F) << 4;
|
||||
ret = (ret & 0xCC) >> 2 | (ret & 0x33) << 2;
|
||||
ret = (ret & 0xAA) >> 1 | (ret & 0x55) << 1;
|
||||
|
||||
break :blk ret;
|
||||
};
|
||||
log.debug("Handling Command 0x{X:0>2} [0b{b:0>8}]", .{ command, command });
|
||||
|
||||
const is_write = command & 1 == 0;
|
||||
const rtc_register = @truncate(u3, command >> 1 & 0x7);
|
||||
|
||||
if (is_write) {
|
||||
return switch (rtc_register) {
|
||||
0 => blk: {
|
||||
self.reset();
|
||||
break :blk .Idle;
|
||||
},
|
||||
1 => .{ .Write = .Control },
|
||||
2 => .{ .Write = .DateTime },
|
||||
3 => .{ .Write = .Time },
|
||||
6 => blk: {
|
||||
self.irq();
|
||||
break :blk .Idle;
|
||||
},
|
||||
4, 5, 7 => .Idle,
|
||||
};
|
||||
} else {
|
||||
return switch (rtc_register) {
|
||||
1 => .{ .Read = .Control },
|
||||
2 => .{ .Read = .DateTime },
|
||||
3 => .{ .Read = .Time },
|
||||
0, 4, 5, 6, 7 => .Idle, // Do Nothing
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
|
@ -302,6 +302,14 @@ pub fn write(bus: *Bus, comptime T: type, address: u32, value: T) void {
|
|||
0x0400_0009 => bus.ppu.bg[0].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[0].cnt.raw & 0xFF),
|
||||
0x0400_000A => bus.ppu.bg[1].cnt.raw = (bus.ppu.bg[1].cnt.raw & 0xFF00) | value,
|
||||
0x0400_000B => bus.ppu.bg[1].cnt.raw = (@as(u16, value) << 8) | (bus.ppu.bg[1].cnt.raw & 0xFF),
|
||||
0x0400_0040 => bus.ppu.win.h[0].set(.Lo, value),
|
||||
0x0400_0041 => bus.ppu.win.h[0].set(.Hi, value),
|
||||
0x0400_0042 => bus.ppu.win.h[1].set(.Lo, value),
|
||||
0x0400_0043 => bus.ppu.win.h[1].set(.Hi, value),
|
||||
0x0400_0044 => bus.ppu.win.v[0].set(.Lo, value),
|
||||
0x0400_0045 => bus.ppu.win.v[0].set(.Hi, value),
|
||||
0x0400_0046 => bus.ppu.win.v[1].set(.Lo, value),
|
||||
0x0400_0047 => bus.ppu.win.v[1].set(.Hi, value),
|
||||
0x0400_0048 => bus.ppu.win.setInL(value),
|
||||
0x0400_0049 => bus.ppu.win.setInH(value),
|
||||
0x0400_004A => bus.ppu.win.setOutL(value),
|
||||
|
@ -459,37 +467,57 @@ pub const BldY = extern union {
|
|||
raw: u16,
|
||||
};
|
||||
|
||||
const u8WriteKind = enum { Hi, Lo };
|
||||
|
||||
/// Write-only
|
||||
pub const WinH = extern union {
|
||||
const Self = @This();
|
||||
|
||||
x2: Bitfield(u16, 0, 8),
|
||||
x1: Bitfield(u16, 8, 8),
|
||||
raw: u16,
|
||||
|
||||
pub fn set(self: *Self, comptime K: u8WriteKind, value: u8) void {
|
||||
self.raw = switch (K) {
|
||||
.Hi => (@as(u16, value) << 8) | self.raw & 0xFF,
|
||||
.Lo => (self.raw & 0xFF00) | value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Write-only
|
||||
pub const WinV = extern union {
|
||||
const Self = @This();
|
||||
|
||||
y2: Bitfield(u16, 0, 8),
|
||||
y1: Bitfield(u16, 8, 8),
|
||||
raw: u16,
|
||||
|
||||
pub fn set(self: *Self, comptime K: u8WriteKind, value: u8) void {
|
||||
self.raw = switch (K) {
|
||||
.Hi => (@as(u16, value) << 8) | self.raw & 0xFF,
|
||||
.Lo => (self.raw & 0xFF00) | value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const WinIn = extern union {
|
||||
w0_bg: Bitfield(u16, 0, 4),
|
||||
w0_obj: Bit(u16, 4),
|
||||
w0_colour: Bit(u16, 5),
|
||||
w0_bld: Bit(u16, 5),
|
||||
w1_bg: Bitfield(u16, 8, 4),
|
||||
w1_obj: Bit(u16, 12),
|
||||
w1_colour: Bit(u16, 13),
|
||||
w1_bld: Bit(u16, 13),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
pub const WinOut = extern union {
|
||||
out_bg: Bitfield(u16, 0, 4),
|
||||
out_obj: Bit(u16, 4),
|
||||
out_colour: Bit(u16, 5),
|
||||
out_bld: Bit(u16, 5),
|
||||
obj_bg: Bitfield(u16, 8, 4),
|
||||
obj_obj: Bit(u16, 12),
|
||||
obj_colour: Bit(u16, 13),
|
||||
obj_bld: Bit(u16, 13),
|
||||
raw: u16,
|
||||
};
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ 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_audio = true; // Enable Audio Sync
|
||||
const sync_video: RunKind = .LimitedFPS; // Configure Video Sync
|
||||
pub const win_scale = 3; // 1x, 2x, 3x, etc. Window Scaling
|
||||
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;
|
||||
|
|
186
src/core/ppu.zig
186
src/core/ppu.zig
|
@ -277,16 +277,17 @@ pub const Ppu = struct {
|
|||
aff_x += self.aff_bg[n - 2].pa;
|
||||
aff_y += self.aff_bg[n - 2].pc;
|
||||
|
||||
if (!shouldDrawBackground(n, self.bldcnt, &self.scanline, i)) continue;
|
||||
const x = @bitCast(u32, ix);
|
||||
const y = @bitCast(u32, iy);
|
||||
|
||||
const win_bounds = self.windowBounds(@truncate(u9, x), @truncate(u8, y));
|
||||
if (!shouldDrawBackground(self, n, win_bounds, i)) continue;
|
||||
|
||||
if (self.bg[n].cnt.display_overflow.read()) {
|
||||
ix = if (ix > px_width) @rem(ix, px_width) else if (ix < 0) px_width + @rem(ix, px_width) else ix;
|
||||
iy = if (iy > px_height) @rem(iy, px_height) else if (iy < 0) px_height + @rem(iy, px_height) else iy;
|
||||
} else if (ix > px_width or iy > px_height or ix < 0 or iy < 0) continue;
|
||||
|
||||
const x = @bitCast(u32, ix);
|
||||
const y = @bitCast(u32, iy);
|
||||
|
||||
const tile_id: u32 = self.vram.read(u8, screen_base + ((y / 8) * @bitCast(u32, tile_width) + (x / 8)));
|
||||
const row = y & 7;
|
||||
const col = x & 7;
|
||||
|
@ -296,7 +297,7 @@ pub const Ppu = struct {
|
|||
|
||||
if (pal_id != 0) {
|
||||
const bgr555 = self.palette.read(u16, pal_id * 2);
|
||||
copyToBackgroundBuffer(n, self.bldcnt, &self.scanline, i, bgr555);
|
||||
self.copyToBackgroundBuffer(n, win_bounds, i, bgr555);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,7 +306,7 @@ pub const Ppu = struct {
|
|||
self.aff_bg[n - 2].y_latch.? += self.aff_bg[n - 2].pd; // PD is added to BGxY
|
||||
}
|
||||
|
||||
fn drawBackround(self: *Self, comptime n: u2) void {
|
||||
fn drawBackground(self: *Self, comptime n: u2) void {
|
||||
// A Tile in a charblock is a byte, while a Screen Entry is a halfword
|
||||
|
||||
const char_base = 0x4000 * @as(u32, self.bg[n].cnt.char_base.read());
|
||||
|
@ -325,10 +326,11 @@ pub const Ppu = struct {
|
|||
|
||||
var i: u32 = 0;
|
||||
while (i < width) : (i += 1) {
|
||||
if (!shouldDrawBackground(n, self.bldcnt, &self.scanline, i)) continue;
|
||||
|
||||
const x = hofs + i;
|
||||
|
||||
const win_bounds = self.windowBounds(@truncate(u9, x), @truncate(u8, y));
|
||||
if (!shouldDrawBackground(self, n, win_bounds, i)) continue;
|
||||
|
||||
// Grab the Screen Entry from VRAM
|
||||
const entry_addr = screen_base + tilemapOffset(size, x, y);
|
||||
const entry = @bitCast(ScreenEntry, self.vram.read(u16, entry_addr));
|
||||
|
@ -353,7 +355,7 @@ pub const Ppu = struct {
|
|||
|
||||
if (pal_id != 0) {
|
||||
const bgr555 = self.palette.read(u16, pal_id * 2);
|
||||
copyToBackgroundBuffer(n, self.bldcnt, &self.scanline, i, bgr555);
|
||||
self.copyToBackgroundBuffer(n, win_bounds, i, bgr555);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -379,10 +381,10 @@ pub const Ppu = struct {
|
|||
var layer: usize = 0;
|
||||
while (layer < 4) : (layer += 1) {
|
||||
self.drawSprites(@truncate(u2, layer));
|
||||
if (layer == self.bg[0].cnt.priority.read() and bg_enable & 1 == 1) self.drawBackround(0);
|
||||
if (layer == self.bg[1].cnt.priority.read() and bg_enable >> 1 & 1 == 1) self.drawBackround(1);
|
||||
if (layer == self.bg[2].cnt.priority.read() and bg_enable >> 2 & 1 == 1) self.drawBackround(2);
|
||||
if (layer == self.bg[3].cnt.priority.read() and bg_enable >> 3 & 1 == 1) self.drawBackround(3);
|
||||
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);
|
||||
if (layer == self.bg[2].cnt.priority.read() and bg_enable >> 2 & 1 == 1) self.drawBackground(2);
|
||||
if (layer == self.bg[3].cnt.priority.read() and bg_enable >> 3 & 1 == 1) self.drawBackground(3);
|
||||
}
|
||||
|
||||
// Copy Drawn Scanline to Frame Buffer
|
||||
|
@ -407,8 +409,8 @@ pub const Ppu = struct {
|
|||
var layer: usize = 0;
|
||||
while (layer < 4) : (layer += 1) {
|
||||
self.drawSprites(@truncate(u2, layer));
|
||||
if (layer == self.bg[0].cnt.priority.read() and bg_enable & 1 == 1) self.drawBackround(0);
|
||||
if (layer == self.bg[1].cnt.priority.read() and bg_enable >> 1 & 1 == 1) self.drawBackround(1);
|
||||
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);
|
||||
if (layer == self.bg[2].cnt.priority.read() and bg_enable >> 2 & 1 == 1) self.drawAffineBackground(2);
|
||||
}
|
||||
|
||||
|
@ -533,6 +535,93 @@ pub const Ppu = struct {
|
|||
return self.palette.getBackdrop();
|
||||
}
|
||||
|
||||
fn copyToBackgroundBuffer(self: *Self, comptime n: u2, bounds: ?WindowBounds, i: usize, bgr555: u16) void {
|
||||
if (self.bldcnt.mode.read() != 0b00) {
|
||||
// Standard Alpha Blending
|
||||
const a_layers = self.bldcnt.layer_a.read();
|
||||
const is_blend_enabled = (a_layers >> n) & 1 == 1;
|
||||
|
||||
// If Alpha Blending is enabled and we've found an eligible layer for
|
||||
// Pixel A, store the pixel in the bottom pixel buffer
|
||||
|
||||
const win_part = if (bounds) |win| blk: {
|
||||
// Window Enabled
|
||||
break :blk switch (win) {
|
||||
.win0 => self.win.in.w0_bld.read(),
|
||||
.win1 => self.win.in.w1_bld.read(),
|
||||
.out => self.win.out.out_bld.read(),
|
||||
};
|
||||
} else true;
|
||||
|
||||
if (win_part and is_blend_enabled) {
|
||||
self.scanline.btm()[i] = bgr555;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.scanline.top()[i] = bgr555;
|
||||
}
|
||||
|
||||
const WindowBounds = enum { win0, win1, out };
|
||||
|
||||
fn windowBounds(self: *Self, x: u9, y: u8) ?WindowBounds {
|
||||
const win0 = self.dispcnt.win_enable.read() & 1 == 1;
|
||||
const win1 = (self.dispcnt.win_enable.read() >> 1) & 1 == 1;
|
||||
const winObj = self.dispcnt.obj_win_enable.read();
|
||||
|
||||
if (!(win0 or win1 or winObj)) return null;
|
||||
|
||||
if (win0 and self.win.inRange(0, x, y)) return .win0;
|
||||
if (win1 and self.win.inRange(1, x, y)) return .win1;
|
||||
|
||||
return .out;
|
||||
}
|
||||
|
||||
fn shouldDrawBackground(self: *Self, comptime n: u2, bounds: ?WindowBounds, i: usize) bool {
|
||||
// If a pixel has been drawn on the top layer, it's because:
|
||||
// 1. The pixel is to be blended with a pixel on the bottom layer
|
||||
// 2. The pixel is not to be blended at all
|
||||
// Also, if we find a pixel on the top layer we don't need to bother with this I think?
|
||||
if (self.scanline.top()[i] != null) return false;
|
||||
|
||||
if (bounds) |win| {
|
||||
switch (win) {
|
||||
.win0 => if ((self.win.in.w0_bg.read() >> n) & 1 == 0) return false,
|
||||
.win1 => if ((self.win.in.w1_bg.read() >> n) & 1 == 0) return false,
|
||||
.out => if ((self.win.out.out_bg.read() >> n) & 1 == 0) return false,
|
||||
}
|
||||
}
|
||||
|
||||
if (self.scanline.btm()[i] != null) {
|
||||
// The pixel found in the bottom layer is:
|
||||
// 1. From a higher priority background
|
||||
// 2. From a background that is marked for blending (Pixel A)
|
||||
|
||||
// If Alpha Blending isn't enabled, then we've already found a higher prio
|
||||
// pixel, we can return early
|
||||
if (self.bldcnt.mode.read() != 0b01) return false;
|
||||
|
||||
const b_layers = self.bldcnt.layer_b.read();
|
||||
|
||||
const win_part = if (bounds) |win| blk: {
|
||||
// Window Enabled
|
||||
break :blk switch (win) {
|
||||
.win0 => self.win.in.w0_bld.read(),
|
||||
.win1 => self.win.in.w1_bld.read(),
|
||||
.out => self.win.out.out_bld.read(),
|
||||
};
|
||||
} else true;
|
||||
|
||||
// If the Background is not marked for blending, we've already found
|
||||
// a higher priority pixel, move on.
|
||||
|
||||
const is_blend_enabled = win_part and ((b_layers >> n) & 1 == 1);
|
||||
if (!is_blend_enabled) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Comment this + get a better understanding
|
||||
fn tilemapOffset(size: u2, x: u32, y: u32) u32 {
|
||||
// Current Row: (y % PIXEL_COUNT) / 8
|
||||
|
@ -794,6 +883,25 @@ const Window = struct {
|
|||
};
|
||||
}
|
||||
|
||||
fn inRange(self: *const Self, comptime id: u1, x: u9, y: u8) bool {
|
||||
const h = self.h[id];
|
||||
const v = self.v[id];
|
||||
|
||||
const y1 = v.y1.read();
|
||||
const y2 = if (y1 > v.y2.read()) 160 else std.math.min(160, v.y2.read());
|
||||
|
||||
if (y1 <= y and y < y2) {
|
||||
// Within Y bounds
|
||||
const x1 = h.x1.read();
|
||||
const x2 = if (x1 > h.x2.read()) 240 else std.math.min(240, h.x2.read());
|
||||
|
||||
// Within X Bounds
|
||||
return x1 <= x and x < x2;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn setH(self: *Self, value: u32) void {
|
||||
self.h[0].raw = @truncate(u16, value);
|
||||
self.h[1].raw = @truncate(u16, value >> 16);
|
||||
|
@ -1135,37 +1243,6 @@ fn alphaBlend(top: u16, btm: u16, bldalpha: io.BldAlpha) u16 {
|
|||
return (bld_b << 10) | (bld_g << 5) | bld_r;
|
||||
}
|
||||
|
||||
fn shouldDrawBackground(comptime n: u2, bldcnt: io.BldCnt, scanline: *Scanline, i: usize) bool {
|
||||
// If a pixel has been drawn on the top layer, it's because
|
||||
// Either the pixel is to be blended with a pixel on the bottom layer
|
||||
// or the pixel is not to be blended at all
|
||||
// Consequentially, if we find a pixel on the top layer, there's no need
|
||||
// to render anything I think?
|
||||
if (scanline.top()[i] != null) return false;
|
||||
|
||||
if (scanline.btm()[i] != null) {
|
||||
// The Pixel found in the Bottom layer is
|
||||
// 1. From a higher priority
|
||||
// 2. From a Backround that is marked for Blending (Pixel A)
|
||||
//
|
||||
// We now have to confirm whether this current Background can be used
|
||||
// as Pixel B or not.
|
||||
|
||||
// If Alpha Blending isn't enabled, we've aready found a higher
|
||||
// priority pixel to render. Move on
|
||||
if (bldcnt.mode.read() != 0b01) return false;
|
||||
|
||||
const b_layers = bldcnt.layer_b.read();
|
||||
const is_blend_enabled = (b_layers >> n) & 1 == 1;
|
||||
|
||||
// If the Background is not marked for blending, we've already found
|
||||
// a higher priority pixel, move on.
|
||||
if (!is_blend_enabled) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn shouldDrawSprite(bldcnt: io.BldCnt, scanline: *Scanline, x: u9) bool {
|
||||
if (scanline.top()[x] != null) return false;
|
||||
|
||||
|
@ -1180,23 +1257,6 @@ fn shouldDrawSprite(bldcnt: io.BldCnt, scanline: *Scanline, x: u9) bool {
|
|||
return true;
|
||||
}
|
||||
|
||||
fn copyToBackgroundBuffer(comptime n: u2, bldcnt: io.BldCnt, scanline: *Scanline, i: usize, bgr555: u16) void {
|
||||
if (bldcnt.mode.read() != 0b00) {
|
||||
// Standard Alpha Blending
|
||||
const a_layers = bldcnt.layer_a.read();
|
||||
const is_blend_enabled = (a_layers >> n) & 1 == 1;
|
||||
|
||||
// If Alpha Blending is enabled and we've found an eligible layer for
|
||||
// Pixel A, store the pixel in the bottom pixel buffer
|
||||
if (is_blend_enabled) {
|
||||
scanline.btm()[i] = bgr555;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
scanline.top()[i] = bgr555;
|
||||
}
|
||||
|
||||
fn copyToSpriteBuffer(bldcnt: io.BldCnt, scanline: *Scanline, x: u9, bgr555: u16) void {
|
||||
if (bldcnt.mode.read() != 0b00) {
|
||||
// Alpha Blending
|
||||
|
|
|
@ -2,7 +2,7 @@ const std = @import("std");
|
|||
|
||||
const Bus = @import("Bus.zig");
|
||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||
const Clock = @import("bus/GamePak.zig").Clock;
|
||||
const Clock = @import("bus/gpio.zig").Clock;
|
||||
|
||||
const Order = std.math.Order;
|
||||
const PriorityQueue = std.PriorityQueue;
|
||||
|
|
|
@ -66,36 +66,68 @@ 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');
|
||||
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
|
||||
/// array consisting of ASCII that won't make any file system angry
|
||||
/// Creates a copy of a title with all Filesystem-invalid characters replaced
|
||||
///
|
||||
/// e.g. POKEPIN R/S to POKEPIN R_S
|
||||
pub fn escape(title: [12]u8) [12]u8 {
|
||||
var result: [12]u8 = title;
|
||||
var ret: [12]u8 = title;
|
||||
|
||||
for (result) |*char| {
|
||||
if (char.* == '/' or char.* == '\\') char.* = '_';
|
||||
if (char.* == 0) break;
|
||||
}
|
||||
//TODO: Add more replacements
|
||||
std.mem.replaceScalar(u8, &ret, '/', '_');
|
||||
std.mem.replaceScalar(u8, &ret, '\\', '_');
|
||||
|
||||
return result;
|
||||
return ret;
|
||||
}
|
||||
|
||||
pub const FilePaths = struct {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
Loading…
Reference in New Issue