diff --git a/src/main.zig b/src/main.zig index 5e285fa..a17707e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,12 @@ const std = @import("std"); const Nes = @import("system").nes; +const platform = @import("platform"); +const frontend = @import("frontend"); + +const PressSpec = struct { + frame: u64, + key: platform.Key, +}; pub fn main(init: std.process.Init) !void { const allocator = init.gpa; @@ -44,10 +51,13 @@ pub fn main(init: std.process.Init) !void { const rom_path = args[2]; var target_frames: u64 = 60; + var dump_frame_path: ?[]const u8 = null; + var dump_audio_path: ?[]const u8 = null; var save_state_path: ?[]const u8 = null; var load_state_path: ?[]const u8 = null; var save_sram_path: ?[]const u8 = null; var load_sram_path: ?[]const u8 = null; + var press_spec: ?PressSpec = null; var i: usize = 3; while (i < args_count) : (i += 1) { @@ -55,6 +65,39 @@ pub fn main(init: std.process.Init) !void { if (std.mem.eql(u8, arg, "--frames") and i + 1 < args_count) { i += 1; target_frames = try std.fmt.parseInt(u64, args[i], 10); + } else if (std.mem.eql(u8, arg, "--press-at") and i + 1 < args_count) { + i += 1; + const val = args[i]; + if (std.mem.indexOfScalar(u8, val, ':')) |colon| { + const frame_str = val[0..colon]; + const btn_str = val[colon + 1 ..]; + const frame_num = std.fmt.parseInt(u64, frame_str, 10) catch 0; + var key: platform.Key = .enter; + if (std.mem.eql(u8, btn_str, "a")) { + key = .z; + } else if (std.mem.eql(u8, btn_str, "b")) { + key = .x; + } else if (std.mem.eql(u8, btn_str, "select")) { + key = .right_shift; + } else if (std.mem.eql(u8, btn_str, "start") or std.mem.eql(u8, btn_str, "enter")) { + key = .enter; + } else if (std.mem.eql(u8, btn_str, "up")) { + key = .up; + } else if (std.mem.eql(u8, btn_str, "down")) { + key = .down; + } else if (std.mem.eql(u8, btn_str, "left")) { + key = .left; + } else if (std.mem.eql(u8, btn_str, "right")) { + key = .right; + } + press_spec = .{ .frame = frame_num, .key = key }; + } + } else if (std.mem.eql(u8, arg, "--dump-frame") and i + 1 < args_count) { + i += 1; + dump_frame_path = args[i]; + } else if (std.mem.eql(u8, arg, "--dump-audio") and i + 1 < args_count) { + i += 1; + dump_audio_path = args[i]; } else if (std.mem.eql(u8, arg, "--save-state") and i + 1 < args_count) { i += 1; save_state_path = args[i]; @@ -104,19 +147,33 @@ pub fn main(init: std.process.Init) !void { } } - const frontend = @import("frontend"); - const platform = @import("platform"); - const nes_bindings = frontend.Bindings{ .keys = &.{ + // Primary A / B buttons .{ .key = .z, .target = .{ .button = .{ .device = 0, .button = 0 } } }, .{ .key = .x, .target = .{ .button = .{ .device = 0, .button = 1 } } }, + .{ .key = .c, .target = .{ .button = .{ .device = 0, .button = 0 } } }, + .{ .key = .v, .target = .{ .button = .{ .device = 0, .button = 1 } } }, + .{ .key = .k, .target = .{ .button = .{ .device = 0, .button = 0 } } }, + .{ .key = .j, .target = .{ .button = .{ .device = 0, .button = 1 } } }, + + // Select & Start .{ .key = .right_shift, .target = .{ .button = .{ .device = 0, .button = 2 } } }, + .{ .key = .left_shift, .target = .{ .button = .{ .device = 0, .button = 2 } } }, .{ .key = .enter, .target = .{ .button = .{ .device = 0, .button = 3 } } }, + .{ .key = .space, .target = .{ .button = .{ .device = 0, .button = 3 } } }, + + // D-Pad: Arrow keys & WASD .{ .key = .up, .target = .{ .button = .{ .device = 0, .button = 4 } } }, .{ .key = .down, .target = .{ .button = .{ .device = 0, .button = 5 } } }, .{ .key = .left, .target = .{ .button = .{ .device = 0, .button = 6 } } }, .{ .key = .right, .target = .{ .button = .{ .device = 0, .button = 7 } } }, + .{ .key = .w, .target = .{ .button = .{ .device = 0, .button = 4 } } }, + .{ .key = .s, .target = .{ .button = .{ .device = 0, .button = 5 } } }, + .{ .key = .a, .target = .{ .button = .{ .device = 0, .button = 6 } } }, + .{ .key = .d, .target = .{ .button = .{ .device = 0, .button = 7 } } }, + + // System actions .{ .key = .f5, .target = .{ .action = .quick_save } }, .{ .key = .f8, .target = .{ .action = .quick_load } }, .{ .key = .p, .target = .{ .action = .toggle_pause } }, @@ -143,9 +200,23 @@ pub fn main(init: std.process.Init) !void { std.debug.print("[6soz] Running NES emulator for {d} frames...\n", .{target_frames}); const start_time = std.Io.Clock.Timestamp.now(init.io, .awake); + var audio_accumulator: std.ArrayListUnmanaged(i16) = .empty; + defer audio_accumulator.deinit(allocator); + var frame: u64 = 0; while (frame < target_frames and app.state == .running) : (frame += 1) { + if (press_spec) |spec| { + if (frame >= spec.frame and frame < spec.frame + 20) { + app.handleEvent(.{ .key_down = .{ .key = spec.key } }); + } else if (frame == spec.frame + 20) { + app.handleEvent(.{ .key_up = .{ .key = spec.key } }); + } + } app.tickFrame(&driver); + if (dump_audio_path != null) { + const pcm = nes.bus.apu.getSamples(); + try audio_accumulator.appendSlice(allocator, pcm); + } } const end_time = std.Io.Clock.Timestamp.now(init.io, .awake); @@ -157,6 +228,57 @@ pub fn main(init: std.process.Init) !void { std.debug.print(" - Frames: {d}\n", .{target_frames}); std.debug.print(" - Elapsed Time: {d:.3}s\n", .{elapsed_sec}); std.debug.print(" - Performance: {d:.1} FPS\n", .{fps}); + + if (dump_audio_path) |path| { + const num_samples: u32 = @truncate(audio_accumulator.items.len); + const data_bytes: u32 = num_samples * 2; + const total_file_size: u32 = 44 + data_bytes; + const wav_buf = try allocator.alloc(u8, total_file_size); + defer allocator.free(wav_buf); + + @memcpy(wav_buf[0..4], "RIFF"); + std.mem.writeInt(u32, wav_buf[4..8], 36 + data_bytes, .little); + @memcpy(wav_buf[8..12], "WAVE"); + @memcpy(wav_buf[12..16], "fmt "); + std.mem.writeInt(u32, wav_buf[16..20], 16, .little); + std.mem.writeInt(u16, wav_buf[20..22], 1, .little); + std.mem.writeInt(u16, wav_buf[22..24], 1, .little); + std.mem.writeInt(u32, wav_buf[24..28], 44100, .little); + std.mem.writeInt(u32, wav_buf[28..32], 44100 * 2, .little); + std.mem.writeInt(u16, wav_buf[32..34], 2, .little); + std.mem.writeInt(u16, wav_buf[34..36], 16, .little); + @memcpy(wav_buf[36..40], "data"); + std.mem.writeInt(u32, wav_buf[40..44], data_bytes, .little); + @memcpy(wav_buf[44..total_file_size], std.mem.sliceAsBytes(audio_accumulator.items)); + + cwd_dir.writeFile(init.io, .{ .sub_path = path, .data = wav_buf }) catch |err| { + std.debug.print("Error writing dumped audio to '{s}': {s}\n", .{ path, @errorName(err) }); + }; + std.debug.print("[6soz] Dumped audio ({d} samples, {d:.2}s) to '{s}'\n", .{ + num_samples, + @as(f64, @floatFromInt(num_samples)) / 44100.0, + path, + }); + } + } + + if (dump_frame_path) |path| { + const vf = nes.videoFrame(0); + const raw_pixels = std.mem.bytesAsSlice(u16, vf.data); + var ppm_buf: [32 + 256 * 240 * 3]u8 = undefined; + const header = "P6\n256 240\n255\n"; + @memcpy(ppm_buf[0..header.len], header); + var offset: usize = header.len; + for (raw_pixels) |p| { + ppm_buf[offset] = @truncate(((p >> 11) & 0x1f) * 255 / 31); + ppm_buf[offset + 1] = @truncate(((p >> 5) & 0x3f) * 255 / 63); + ppm_buf[offset + 2] = @truncate((p & 0x1f) * 255 / 31); + offset += 3; + } + cwd_dir.writeFile(init.io, .{ .sub_path = path, .data = ppm_buf[0..offset] }) catch |err| { + std.debug.print("Error writing dumped frame to '{s}': {s}\n", .{ path, @errorName(err) }); + }; + std.debug.print("[6soz] Dumped frame to '{s}'\n", .{path}); } if (save_state_path) |path| { @@ -189,6 +311,9 @@ fn printUsage() void { \\ \\Options: \\ --frames Number of frames to execute (default: 60) + \\ --press-at : Press button (a, b, select, start, etc.) at frame F for 20 frames + \\ --dump-frame Dump final video frame to PPM image file + \\ --dump-audio Dump audio output to standard WAV file \\ --save-state Save state binary file output \\ --load-state Load state binary file input \\ --save-sram Save battery PRG RAM output diff --git a/src/platform/raylib.zig b/src/platform/raylib.zig index 3925eb3..d27ed18 100644 --- a/src/platform/raylib.zig +++ b/src/platform/raylib.zig @@ -16,9 +16,10 @@ pub const Driver = struct { audio_ring_head: usize = 0, audio_ring_tail: usize = 0, - event_queue: [16]platform.Event = undefined, + event_queue: [64]platform.Event = undefined, event_head: usize = 0, event_tail: usize = 0, + polled_this_frame: bool = false, pub fn init(title: [*:0]const u8, width: i32, height: i32, scale: i32) Driver { c.InitWindow(width * scale, height * scale, title); @@ -63,10 +64,22 @@ pub const Driver = struct { return ev; } + if (self.polled_this_frame) { + return null; + } + self.polled_this_frame = true; + self.event_head = 0; + self.event_tail = 0; + // Poll Raylib Key Events const key_mappings = [_]struct { c_key: c_int, key: platform.Key }{ .{ .c_key = c.KEY_Z, .key = .z }, .{ .c_key = c.KEY_X, .key = .x }, + .{ .c_key = c.KEY_C, .key = .c }, + .{ .c_key = c.KEY_V, .key = .v }, + .{ .c_key = c.KEY_J, .key = .j }, + .{ .c_key = c.KEY_K, .key = .k }, + .{ .c_key = c.KEY_SPACE, .key = .space }, .{ .c_key = c.KEY_RIGHT_SHIFT, .key = .right_shift }, .{ .c_key = c.KEY_LEFT_SHIFT, .key = .left_shift }, .{ .c_key = c.KEY_ENTER, .key = .enter }, @@ -74,6 +87,10 @@ pub const Driver = struct { .{ .c_key = c.KEY_DOWN, .key = .down }, .{ .c_key = c.KEY_LEFT, .key = .left }, .{ .c_key = c.KEY_RIGHT, .key = .right }, + .{ .c_key = c.KEY_W, .key = .w }, + .{ .c_key = c.KEY_S, .key = .s }, + .{ .c_key = c.KEY_A, .key = .a }, + .{ .c_key = c.KEY_D, .key = .d }, .{ .c_key = c.KEY_F5, .key = .f5 }, .{ .c_key = c.KEY_F8, .key = .f8 }, .{ .c_key = c.KEY_P, .key = .p }, @@ -81,13 +98,43 @@ pub const Driver = struct { for (key_mappings) |mapping| { if (c.IsKeyPressed(mapping.c_key)) { - return .{ .key_down = .{ .key = mapping.key } }; + const next = (self.event_head + 1) % self.event_queue.len; + if (next != self.event_tail) { + self.event_queue[self.event_head] = .{ .key_down = .{ .key = mapping.key } }; + self.event_head = next; + } } if (c.IsKeyReleased(mapping.c_key)) { - return .{ .key_up = .{ .key = mapping.key } }; + const next = (self.event_head + 1) % self.event_queue.len; + if (next != self.event_tail) { + self.event_queue[self.event_head] = .{ .key_up = .{ .key = mapping.key } }; + self.event_head = next; + } } } + // Support mouse left click as Start/Action button + if (c.IsMouseButtonPressed(c.MOUSE_BUTTON_LEFT)) { + const next = (self.event_head + 1) % self.event_queue.len; + if (next != self.event_tail) { + self.event_queue[self.event_head] = .{ .key_down = .{ .key = .enter } }; + self.event_head = next; + } + } + if (c.IsMouseButtonReleased(c.MOUSE_BUTTON_LEFT)) { + const next = (self.event_head + 1) % self.event_queue.len; + if (next != self.event_tail) { + self.event_queue[self.event_head] = .{ .key_up = .{ .key = .enter } }; + self.event_head = next; + } + } + + if (self.event_head != self.event_tail) { + const ev = self.event_queue[self.event_tail]; + self.event_tail = (self.event_tail + 1) % self.event_queue.len; + return ev; + } + return null; } @@ -116,6 +163,8 @@ pub const Driver = struct { .height = @floatFromInt(self.height * self.scale), }; c.DrawTexturePro(self.texture, src, dest, c.Vector2{ .x = 0, .y = 0 }, 0.0, c.WHITE); + + self.polled_this_frame = false; } pub fn queueAudio(self: *Driver, output: usize, buffer: contract.AudioBuffer) void {