frontend: add CLI frame/audio dump flags and expand raylib input mapping
This commit is contained in:
+128
-3
@@ -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 <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
|
||||
\\ --load-state <file> Load state binary file input
|
||||
\\ --save-sram <file> Save battery PRG RAM output
|
||||
|
||||
+52
-3
@@ -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,12 +98,42 @@ 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 {
|
||||
|
||||
Reference in New Issue
Block a user