From 02a29b12b2df81f4627b4fec1861ce3c7759de05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20=C5=9Een?= Date: Fri, 14 Aug 2026 16:31:13 +0200 Subject: [PATCH] add raylib dependency and desktop platform driver with interactive play mode --- build.zig | 18 ++++++- build.zig.zon | 4 ++ src/main.zig | 57 +++++++++++++------- src/platform/raylib.zig | 116 ++++++++++++++++++++++++++++++++++++++++ src/platform/root.zig | 2 + 5 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 src/platform/raylib.zig diff --git a/build.zig b/build.zig index 5c6e704..3bed062 100644 --- a/build.zig +++ b/build.zig @@ -38,7 +38,6 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/platform/root.zig"), }); platform_mod.addImport("contract", contract_mod); - const frontend_mod = b.createModule(.{ .target = target, .optimize = optimize, @@ -63,6 +62,23 @@ pub fn build(b: *std.Build) void { .root_module = exe_mod, }); + if (b.lazyDependency("raylib", .{ + .target = target, + .optimize = optimize, + })) |raylib_dep| { + const raylib_lib = raylib_dep.artifact("raylib"); + exe_mod.linkLibrary(raylib_lib); + + const raylib_c = b.addTranslateC(.{ + .target = target, + .optimize = optimize, + .root_source_file = raylib_dep.path("src/raylib.h"), + }); + const craylib_mod = raylib_c.createModule(); + platform_mod.addImport("c_raylib", craylib_mod); + exe_mod.addImport("c_raylib", craylib_mod); + } + b.installArtifact(exe); // Test Step Configuration diff --git a/build.zig.zon b/build.zig.zon index 4a8f95a..a244bc6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,6 +13,10 @@ .hash = "N-V-__8AAKmLHACkwIMCMRdqnsyLtL5hbQdi8cm3nGs8n-O7", .lazy = true, }, + .raylib = .{ + .url = "https://github.com/raysan5/raylib/archive/refs/heads/master.tar.gz", + .hash = "raylib-6.0.0-whq8uIPTKwXybRNNu7BJNM6bxip5fYUgAh-bctTa7oHR", + }, }, .paths = .{""}, diff --git a/src/main.zig b/src/main.zig index 76940ed..5e285fa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -29,13 +29,16 @@ pub fn main(init: std.process.Init) !void { return; } - if (!std.mem.eql(u8, command, "run")) { + const is_play = std.mem.eql(u8, command, "play"); + const is_run = std.mem.eql(u8, command, "run"); + + if (!is_play and !is_run) { std.debug.print("Unknown command '{s}'. Use '6soz --help' for usage.\n", .{command}); return; } if (args_count < 3) { - std.debug.print("Error: missing ROM file path.\nUsage: 6soz run \n", .{}); + std.debug.print("Error: missing ROM file path.\nUsage: 6soz play or 6soz run \n", .{}); return; } @@ -102,6 +105,7 @@ pub fn main(init: std.process.Init) !void { } const frontend = @import("frontend"); + const platform = @import("platform"); const nes_bindings = frontend.Bindings{ .keys = &.{ @@ -121,26 +125,40 @@ pub fn main(init: std.process.Init) !void { var quick_save_buffer: [64 * 1024]u8 = undefined; var app = frontend.App(Nes).init(&nes, nes_bindings, .{ .quick_state_buffer = &quick_save_buffer }); - var driver = frontend.HeadlessDriver.init(); - std.debug.print("[6soz] Running NES emulator for {d} frames...\n", .{target_frames}); - const start_time = std.Io.Clock.Timestamp.now(init.io, .awake); + if (is_play) { + std.debug.print("[6soz] Launching Raylib Desktop Window...\n", .{}); + var driver = platform.raylib.Driver.init("6soz NES Emulator Engine", 256, 240, 3); + defer driver.deinit(); - var frame: u64 = 0; - while (frame < target_frames and app.state == .running) : (frame += 1) { - app.tickFrame(&driver); + while (app.state != .stopped and !driver.shouldClose()) { + while (driver.pollEvent()) |event| { + app.handleEvent(event); + } + app.tickFrame(&driver); + } + } else { + var driver = frontend.HeadlessDriver.init(); + + 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 frame: u64 = 0; + while (frame < target_frames and app.state == .running) : (frame += 1) { + app.tickFrame(&driver); + } + + const end_time = std.Io.Clock.Timestamp.now(init.io, .awake); + const elapsed_ns = end_time.raw.nanoseconds - start_time.raw.nanoseconds; + const elapsed_sec = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000_000.0; + const fps = if (elapsed_sec > 0) @as(f64, @floatFromInt(target_frames)) / elapsed_sec else 0.0; + + std.debug.print("[6soz] Benchmark Complete:\n", .{}); + 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}); } - const end_time = std.Io.Clock.Timestamp.now(init.io, .awake); - const elapsed_ns = end_time.raw.nanoseconds - start_time.raw.nanoseconds; - const elapsed_sec = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000_000.0; - const fps = if (elapsed_sec > 0) @as(f64, @floatFromInt(target_frames)) / elapsed_sec else 0.0; - - std.debug.print("[6soz] Benchmark Complete:\n", .{}); - 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 (save_state_path) |path| { var state_buf: [64 * 1024]u8 = undefined; if (nes.saveState(&state_buf)) |written| { @@ -166,7 +184,8 @@ fn printUsage() void { std.debug.print( \\6soz - 6502 / 6510 & NES Emulator Engine \\Usage: - \\ 6soz run [options] + \\ 6soz play Launch interactive Raylib desktop window + \\ 6soz run [options] Run headless emulator benchmark \\ \\Options: \\ --frames Number of frames to execute (default: 60) diff --git a/src/platform/raylib.zig b/src/platform/raylib.zig new file mode 100644 index 0000000..4eced7c --- /dev/null +++ b/src/platform/raylib.zig @@ -0,0 +1,116 @@ +const std = @import("std"); +const contract = @import("contract"); +const platform = @import("root.zig"); +const c = @import("c_raylib"); + +pub const Driver = struct { + texture: c.Texture2D, + audio_stream: c.AudioStream, + width: i32, + height: i32, + scale: i32, + + event_queue: [16]platform.Event = undefined, + event_head: usize = 0, + event_tail: usize = 0, + + pub fn init(title: [*:0]const u8, width: i32, height: i32, scale: i32) Driver { + c.InitWindow(width * scale, height * scale, title); + c.SetTargetFPS(60); + + c.InitAudioDevice(); + c.SetAudioStreamBufferSizeDefault(1024); + + const stream = c.LoadAudioStream(44100, 16, 1); + c.PlayAudioStream(stream); + + const img = c.GenImageColor(width, height, c.BLACK); + defer c.UnloadImage(img); + + const tex = c.LoadTextureFromImage(img); + c.SetTextureFilter(tex, c.TEXTURE_FILTER_POINT); + + return .{ + .texture = tex, + .audio_stream = stream, + .width = width, + .height = height, + .scale = scale, + }; + } + + pub fn deinit(self: *Driver) void { + c.UnloadAudioStream(self.audio_stream); + c.CloseAudioDevice(); + c.UnloadTexture(self.texture); + c.CloseWindow(); + } + + pub fn shouldClose(_: *Driver) bool { + return c.WindowShouldClose(); + } + + pub fn pollEvent(self: *Driver) ?platform.Event { + 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; + } + + // 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_RIGHT_SHIFT, .key = .right_shift }, + .{ .c_key = c.KEY_LEFT_SHIFT, .key = .left_shift }, + .{ .c_key = c.KEY_ENTER, .key = .enter }, + .{ .c_key = c.KEY_UP, .key = .up }, + .{ .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_F5, .key = .f5 }, + .{ .c_key = c.KEY_F8, .key = .f8 }, + .{ .c_key = c.KEY_P, .key = .p }, + }; + + for (key_mappings) |mapping| { + if (c.IsKeyPressed(mapping.c_key)) { + return .{ .key_down = .{ .key = mapping.key } }; + } + if (c.IsKeyReleased(mapping.c_key)) { + return .{ .key_up = .{ .key = mapping.key } }; + } + } + + return null; + } + + pub fn renderVideo(self: *Driver, output: usize, frame: contract.VideoFrame) void { + _ = output; + if (frame.data.len > 0) { + c.UpdateTexture(self.texture, frame.data.ptr); + } + + c.BeginDrawing(); + defer c.EndDrawing(); + + c.ClearBackground(c.BLACK); + const src = c.Rectangle{ .x = 0, .y = 0, .width = @floatFromInt(self.width), .height = @floatFromInt(self.height) }; + const dest = c.Rectangle{ + .x = 0, + .y = 0, + .width = @floatFromInt(self.width * self.scale), + .height = @floatFromInt(self.height * self.scale), + }; + c.DrawTexturePro(self.texture, src, dest, c.Vector2{ .x = 0, .y = 0 }, 0.0, c.WHITE); + } + + pub fn queueAudio(self: *Driver, output: usize, buffer: contract.AudioBuffer) void { + _ = output; + if (buffer.frames > 0 and c.IsAudioStreamProcessed(self.audio_stream)) { + c.UpdateAudioStream(self.audio_stream, buffer.data.ptr, @intCast(buffer.frames)); + } + } + + pub fn setDeviceFeedback(_: *Driver, _: usize, _: contract.DeviceFeedback) void {} +}; diff --git a/src/platform/root.zig b/src/platform/root.zig index cc4f681..3995d54 100644 --- a/src/platform/root.zig +++ b/src/platform/root.zig @@ -130,6 +130,8 @@ pub const Headless = struct { } }; +pub const raylib = @import("raylib.zig"); + test "platform: headless has no events" { var platform_driver = Headless.init();