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, rgba_buffer: [256 * 240]u32 = [_]u32{0xFF000000} ** (256 * 240), audio_ring: [16384]i16 = [_]i16{0} ** 16384, audio_ring_head: usize = 0, audio_ring_tail: usize = 0, 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); 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; } 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 }, .{ .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_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 }, }; for (key_mappings) |mapping| { if (c.IsKeyPressed(mapping.c_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)) { 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; } pub fn renderVideo(self: *Driver, output: usize, frame: contract.VideoFrame) void { _ = output; if (frame.data.len >= 256 * 240 * 2) { const raw_pixels = std.mem.bytesAsSlice(u16, frame.data); for (raw_pixels[0..@min(raw_pixels.len, self.rgba_buffer.len)], 0..) |pixel, i| { const r = @as(u32, (pixel >> 11) & 0x1F) * 255 / 31; const g = @as(u32, (pixel >> 5) & 0x3F) * 255 / 63; const b = @as(u32, pixel & 0x1F) * 255 / 31; self.rgba_buffer[i] = r | (g << 8) | (b << 16) | (0xFF << 24); } c.UpdateTexture(self.texture, &self.rgba_buffer); } 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); self.polled_this_frame = false; } 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 {} };