frontend: add CLI frame/audio dump flags and expand raylib input mapping

This commit is contained in:
2026-08-27 07:16:29 +02:00
parent 9ba71b7b62
commit 20c1106a9a
2 changed files with 180 additions and 6 deletions
+128 -3
View File
@@ -1,5 +1,12 @@
const std = @import("std"); const std = @import("std");
const Nes = @import("system").nes; 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 { pub fn main(init: std.process.Init) !void {
const allocator = init.gpa; const allocator = init.gpa;
@@ -44,10 +51,13 @@ pub fn main(init: std.process.Init) !void {
const rom_path = args[2]; const rom_path = args[2];
var target_frames: u64 = 60; 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 save_state_path: ?[]const u8 = null;
var load_state_path: ?[]const u8 = null; var load_state_path: ?[]const u8 = null;
var save_sram_path: ?[]const u8 = null; var save_sram_path: ?[]const u8 = null;
var load_sram_path: ?[]const u8 = null; var load_sram_path: ?[]const u8 = null;
var press_spec: ?PressSpec = null;
var i: usize = 3; var i: usize = 3;
while (i < args_count) : (i += 1) { 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) { if (std.mem.eql(u8, arg, "--frames") and i + 1 < args_count) {
i += 1; i += 1;
target_frames = try std.fmt.parseInt(u64, args[i], 10); 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) { } else if (std.mem.eql(u8, arg, "--save-state") and i + 1 < args_count) {
i += 1; i += 1;
save_state_path = args[i]; 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{ const nes_bindings = frontend.Bindings{
.keys = &.{ .keys = &.{
// Primary A / B buttons
.{ .key = .z, .target = .{ .button = .{ .device = 0, .button = 0 } } }, .{ .key = .z, .target = .{ .button = .{ .device = 0, .button = 0 } } },
.{ .key = .x, .target = .{ .button = .{ .device = 0, .button = 1 } } }, .{ .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 = .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 = .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 = .up, .target = .{ .button = .{ .device = 0, .button = 4 } } },
.{ .key = .down, .target = .{ .button = .{ .device = 0, .button = 5 } } }, .{ .key = .down, .target = .{ .button = .{ .device = 0, .button = 5 } } },
.{ .key = .left, .target = .{ .button = .{ .device = 0, .button = 6 } } }, .{ .key = .left, .target = .{ .button = .{ .device = 0, .button = 6 } } },
.{ .key = .right, .target = .{ .button = .{ .device = 0, .button = 7 } } }, .{ .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 = .f5, .target = .{ .action = .quick_save } },
.{ .key = .f8, .target = .{ .action = .quick_load } }, .{ .key = .f8, .target = .{ .action = .quick_load } },
.{ .key = .p, .target = .{ .action = .toggle_pause } }, .{ .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}); std.debug.print("[6soz] Running NES emulator for {d} frames...\n", .{target_frames});
const start_time = std.Io.Clock.Timestamp.now(init.io, .awake); 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; var frame: u64 = 0;
while (frame < target_frames and app.state == .running) : (frame += 1) { 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); 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); 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(" - Frames: {d}\n", .{target_frames});
std.debug.print(" - Elapsed Time: {d:.3}s\n", .{elapsed_sec}); std.debug.print(" - Elapsed Time: {d:.3}s\n", .{elapsed_sec});
std.debug.print(" - Performance: {d:.1} FPS\n", .{fps}); 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| { if (save_state_path) |path| {
@@ -189,6 +311,9 @@ fn printUsage() void {
\\ \\
\\Options: \\Options:
\\ --frames <N> Number of frames to execute (default: 60) \\ --frames <N> Number of frames to execute (default: 60)
\\ --press-at <F>:<B> Press button (a, b, select, start, etc.) at frame F for 20 frames
\\ --dump-frame <file> Dump final video frame to PPM image file
\\ --dump-audio <file> Dump audio output to standard WAV file
\\ --save-state <file> Save state binary file output \\ --save-state <file> Save state binary file output
\\ --load-state <file> Load state binary file input \\ --load-state <file> Load state binary file input
\\ --save-sram <file> Save battery PRG RAM output \\ --save-sram <file> Save battery PRG RAM output
+52 -3
View File
@@ -16,9 +16,10 @@ pub const Driver = struct {
audio_ring_head: usize = 0, audio_ring_head: usize = 0,
audio_ring_tail: usize = 0, audio_ring_tail: usize = 0,
event_queue: [16]platform.Event = undefined, event_queue: [64]platform.Event = undefined,
event_head: usize = 0, event_head: usize = 0,
event_tail: 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 { pub fn init(title: [*:0]const u8, width: i32, height: i32, scale: i32) Driver {
c.InitWindow(width * scale, height * scale, title); c.InitWindow(width * scale, height * scale, title);
@@ -63,10 +64,22 @@ pub const Driver = struct {
return ev; 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 // Poll Raylib Key Events
const key_mappings = [_]struct { c_key: c_int, key: platform.Key }{ const key_mappings = [_]struct { c_key: c_int, key: platform.Key }{
.{ .c_key = c.KEY_Z, .key = .z }, .{ .c_key = c.KEY_Z, .key = .z },
.{ .c_key = c.KEY_X, .key = .x }, .{ .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_RIGHT_SHIFT, .key = .right_shift },
.{ .c_key = c.KEY_LEFT_SHIFT, .key = .left_shift }, .{ .c_key = c.KEY_LEFT_SHIFT, .key = .left_shift },
.{ .c_key = c.KEY_ENTER, .key = .enter }, .{ .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_DOWN, .key = .down },
.{ .c_key = c.KEY_LEFT, .key = .left }, .{ .c_key = c.KEY_LEFT, .key = .left },
.{ .c_key = c.KEY_RIGHT, .key = .right }, .{ .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_F5, .key = .f5 },
.{ .c_key = c.KEY_F8, .key = .f8 }, .{ .c_key = c.KEY_F8, .key = .f8 },
.{ .c_key = c.KEY_P, .key = .p }, .{ .c_key = c.KEY_P, .key = .p },
@@ -81,12 +98,42 @@ pub const Driver = struct {
for (key_mappings) |mapping| { for (key_mappings) |mapping| {
if (c.IsKeyPressed(mapping.c_key)) { 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)) { 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; return null;
} }
@@ -116,6 +163,8 @@ pub const Driver = struct {
.height = @floatFromInt(self.height * self.scale), .height = @floatFromInt(self.height * self.scale),
}; };
c.DrawTexturePro(self.texture, src, dest, c.Vector2{ .x = 0, .y = 0 }, 0.0, c.WHITE); 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 { pub fn queueAudio(self: *Driver, output: usize, buffer: contract.AudioBuffer) void {