const Self = @This(); const rl = @import("raylib"); const gui = @import("raygui"); const std = @import("std"); const roms = @import("./roms.zig"); const GUILayout = @import("./gui-layout.zig"); const VerticalLayout = @import("./vertical-layout.zig"); const HorizontalLayout = @import("./horizontal-layout.zig"); const ROM = roms.ROM; const ChipContext = @import("chip.zig"); const RaylibChip = @import("raylib-chip.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const StringList = std.ArrayList([]const u8); const rom_list = roms.listROMs(); allocator: Allocator, chip: *ChipContext, raylib_chip: *RaylibChip, chip_sound: rl.Sound, is_gui_open: bool = true, selected_rom_index: i32 = 3, rom_list_scroll_index: i32 = 0, pub fn genSinWave(wave: *rl.Wave, frequency: f32) void { assert(wave.sampleSize == 16); // Only 16 bits are supported const sample_rate: f32 = @floatFromInt(wave.sampleRate); const sample_size: u5 = @truncate(wave.sampleSize); const max_sample_value: f32 = @floatFromInt(@shlExact(@as(u32, 1), sample_size - 1)); const data: [*]i16 = @ptrCast(@alignCast(wave.data)); for (0..wave.frameCount) |i| { const i_f32: f32 = @floatFromInt(i); const sin_value: f32 = @sin(std.math.pi*2*frequency/sample_rate*i_f32); data[i] = @intFromFloat(sin_value*max_sample_value); } } pub fn init(allocator: Allocator) !Self { var chip = try allocator.create(ChipContext); chip.* = try ChipContext.init(allocator); const sample_rate = 44100; var data = try allocator.alloc(i16, sample_rate); defer allocator.free(data); var chip_wave = rl.Wave{ .frameCount = sample_rate, .sampleRate = sample_rate, .sampleSize = 16, .channels = 1, .data = @ptrCast(data.ptr), }; genSinWave(&chip_wave, 440); var chip_sound = rl.LoadSoundFromWave(chip_wave); rl.SetSoundVolume(chip_sound, 0.2); var raylib_chip = try allocator.create(RaylibChip); raylib_chip.* = RaylibChip.init(chip, chip_sound); var self = Self { .allocator = allocator, .chip = chip, .raylib_chip = raylib_chip, .chip_sound = chip_sound, }; const breakout = comptime roms.findIndexbyName(&rom_list, "br8kout") orelse unreachable; self.set_rom(@intCast(breakout)); return self; } pub fn deinit(self: *Self) void { rl.UnloadSound(self.chip_sound); self.chip.deinit(); self.allocator.destroy(self.raylib_chip); self.allocator.destroy(self.chip); } pub fn set_rom(self: *Self, index: i32) void { self.selected_rom_index = index; self.reset_rom(); } pub fn reset_rom(self: *Self) void { const rom = rom_list[@intCast(self.selected_rom_index)]; self.chip.reset(); self.chip.set_memory(0x200, rom.data); } pub fn update(self: *Self, dt: f32) void { if (!self.is_gui_open) { rl.ResumeSound(self.chip_sound); self.raylib_chip.update(dt); } else { self.raylib_chip.update_input(); rl.PauseSound(self.chip_sound); } } pub fn draw(self: *Self) void { rl.ClearBackground(rl.Color{ .r = 33, .g = 33, .b = 33 }); const display_width = self.chip.display_width; const display_height = self.chip.display_height; const screen_width = rl.GetScreenWidth(); const screen_height = rl.GetScreenHeight(); const x_scale = @as(f32, @floatFromInt(screen_width)) / @as(f32, @floatFromInt(display_width)); const y_scale = @as(f32, @floatFromInt(screen_height)) / @as(f32, @floatFromInt(display_height)); const max_scale = @min(x_scale, y_scale); const render_width: i32 = @intFromFloat(@as(f32, @floatFromInt(display_width)) * max_scale); const render_height: i32 = @intFromFloat(@as(f32, @floatFromInt(display_height)) * max_scale); self.raylib_chip.render( @divTrunc(screen_width - render_width, 2), @divTrunc(screen_height - render_height, 2), render_width, render_height ); self.drawGui(self.allocator) catch @panic("GUI OOM"); } fn GuiListView( allocator: Allocator, bounds: rl.Rectangle, items: []const []const u8, scroll_index: *i32, active: *i32, ) !i32 { var allocated_count: usize = 0; var items_z = try allocator.alloc([:0]const u8, items.len); var items_zn = try allocator.alloc([*:0]const u8, items.len); defer { for (0..allocated_count) |i| { allocator.free(items_z[i]); } allocator.free(items_z); allocator.free(items_zn); } for (0.., items) |i, item| { items_z[i] = try allocator.dupeZ(u8, item); items_zn[i] = items_z[i].ptr; allocated_count += 1; } return gui.GuiListViewEx(bounds, items_zn.ptr, @intCast(items_z.len), scroll_index, active, null); } fn GuiListViewMinWidth(allocator: Allocator, height: i32, items: []const []const u8) !i32 { const border_width = gui.GuiGetStyle(.DEFAULT , @intFromEnum(gui.GuiControlProperty.BORDER_WIDTH)); const item_spacing = gui.GuiGetStyle(.LISTVIEW, @intFromEnum(gui.GuiListViewProperty.LIST_ITEMS_SPACING)); const item_height = gui.GuiGetStyle(.LISTVIEW, @intFromEnum(gui.GuiListViewProperty.LIST_ITEMS_HEIGHT)); const font_size = gui.GuiGetStyle(.DEFAULT , @intFromEnum(gui.GuiDefaultProperty.TEXT_SIZE)); const scrollbar_width = gui.GuiGetStyle(.LISTVIEW, @intFromEnum(gui.GuiListViewProperty.SCROLLBAR_WIDTH)); var max_item_width: i32 = 0; for (items) |item| { const item_z = try allocator.dupeZ(u8, item); defer allocator.free(item_z); const item_width = rl.MeasureText(item_z, font_size); max_item_width = @max(max_item_width, item_width); } const item_count: i32 = @intCast(items.len); const need_scrollbar = ((item_height + item_spacing)*item_count > height); var min_width = max_item_width + 2*item_spacing + border_width; if (need_scrollbar) { min_width += scrollbar_width; } return min_width; } pub fn getWindowBodyBounds(bounds: rl.Rectangle) rl.Rectangle { const status_bar_height = gui.RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT; const margin = 8; return rl.Rectangle { .x = bounds.x + margin, .y = bounds.y + margin + status_bar_height, .width = bounds.width - 2*margin, .height = bounds.height - 2*margin - status_bar_height, }; } pub fn pushGuiWindowBox(layout: *GUILayout, bounds: rl.Rectangle, title: [*:0]const u8) bool { const body = getWindowBodyBounds(bounds); layout.push(); layout.translate(body.x, body.y); return gui.GuiWindowBox(bounds, title) != 0; } fn GetColor(hex_value: i32) rl.Color { const hex_value_unsigned: u32 = @bitCast(hex_value); return rl.Color{ .r = @truncate(hex_value_unsigned >> 24), .g = @truncate(hex_value_unsigned >> 16), .b = @truncate(hex_value_unsigned >> 8), .a = @truncate(hex_value_unsigned) }; } fn drawKeyboard(bounds: rl.Rectangle, keyboard_labels: [16]u8, keyboard_state: [16]bool) void { const font_size = gui.GuiGetStyle(.DEFAULT , @intFromEnum(gui.GuiDefaultProperty.TEXT_SIZE))*2; const text_color_normal = GetColor(gui.GuiGetStyle(.LABEL, @intFromEnum(gui.GuiControlProperty.TEXT_COLOR_NORMAL))); const text_color_pressed = GetColor(gui.GuiGetStyle(.LABEL, @intFromEnum(gui.GuiControlProperty.TEXT_COLOR_PRESSED))); const border_color_normal = GetColor(gui.GuiGetStyle(.LABEL, @intFromEnum(gui.GuiControlProperty.BORDER_COLOR_NORMAL))); const border_color_pressed = GetColor(gui.GuiGetStyle(.LABEL, @intFromEnum(gui.GuiControlProperty.BORDER_COLOR_PRESSED))); const cell_margin = 2; const cell_size = font_size+2*cell_margin; rl.rlPushMatrix(); rl.rlTranslatef(bounds.x, bounds.y, 0); const key_indexes = [16]usize { 0x1, 0x2, 0x3, 0xC, 0x4, 0x5, 0x6, 0xD, 0x7, 0x8, 0x9, 0xE, 0xA, 0x0, 0xB, 0xF, }; for (0.., key_indexes) |i, key_index| { const cell_y: i32 = @intCast(@divTrunc(i, 4)); const cell_x: i32 = @intCast(i % 4); const label = [_:0]u8{ keyboard_labels[key_index], 0 }; const label_width = rl.MeasureText(&label, font_size); var color = text_color_normal; var border_color = border_color_normal; if (keyboard_state[key_index]) { color = text_color_pressed; color = border_color_pressed; } const x = cell_x*(cell_size-1); const y = cell_y*(cell_size-1); rl.DrawRectangleLines(x, y, cell_size, cell_size, border_color); rl.DrawText(&label, x + @divTrunc(cell_size - label_width, 2), y+cell_margin, font_size, color); } rl.rlPopMatrix(); } pub fn drawGui(self: *Self, allocator: Allocator) !void { if (rl.IsKeyPressed(.KEY_TAB)) { self.is_gui_open = !self.is_gui_open; } if (!self.is_gui_open) return; var window_layout = GUILayout.init(); defer window_layout.deinit(); // Draw overlay, to indicate that emulator is paused rl.DrawRectangle(0, 0, rl.GetScreenWidth(), rl.GetScreenHeight(), .{ .r = 22, .g = 22, .b = 22, .a = 128, }); const window_width: f32 = 400; const window_height: f32 = 200; const screen_width: f32 = @floatFromInt(rl.GetScreenWidth()); const screen_height: f32 = @floatFromInt(rl.GetScreenHeight()); const window = rl.Rectangle{ .x = @trunc((screen_width-window_width)/2), .y = @trunc((screen_height-window_height)/2), .width = window_width, .height = window_height }; if (pushGuiWindowBox(&window_layout, window, "Settings")) { self.is_gui_open = false; } const rom_list_names = try allocator.alloc([]const u8, rom_list.len); defer allocator.free(rom_list_names); for (0.., rom_list) |i, rom| { rom_list_names[i] = rom.name; } const rom_list_height = 100; const rom_list_width = try GuiListViewMinWidth(allocator, rom_list_height, rom_list_names) + 20; var columns = HorizontalLayout.init(32, getWindowBodyBounds(window)); { // First column var column1 = VerticalLayout.init(4, columns.column(@floatFromInt(rom_list_width))); if (gui.GuiButton(column1.row_sized(100, 24), "Reset") == 1) { self.reset_rom(); } _ = gui.GuiLabel(column1.row(16), "Current ROM:"); { // ROM list view var selected_rom = self.selected_rom_index; _ = try GuiListView( allocator, column1.row_sized(@floatFromInt(rom_list_width), rom_list_height), rom_list_names, &self.rom_list_scroll_index, &selected_rom ); if (selected_rom != self.selected_rom_index) { self.set_rom(selected_rom); } } } { // Controls column var column2 = VerticalLayout.init(0, columns.column(200)); _ = gui.GuiLabel(column2.row(16), "Controls:"); _ = gui.GuiLabel(column2.row(16), "TAB - Toggle settings (this window)"); column2.add_gap(8); var keyboard_columns = HorizontalLayout.init(0, column2.row_full()); const keyboard_width = 128; { var keyboard_column1 = VerticalLayout.init(0, keyboard_columns.column(keyboard_width)); _ = gui.GuiLabel(keyboard_column1.row(16), "Input state (QWERTY):"); var keyboard_labels: [16]u8 = undefined; for (0.., RaylibChip.default_controls) |i, key| { keyboard_labels[i] = @intCast(@intFromEnum(key)); } drawKeyboard(keyboard_column1.row_full(), keyboard_labels, self.chip.input); } { var keyboard_column2 = VerticalLayout.init(0, keyboard_columns.column(keyboard_width)); _ = gui.GuiLabel(keyboard_column2.row(16), "Input state:"); var reference_labels = [16]u8{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', }; drawKeyboard(keyboard_column2.row_full(), reference_labels, self.chip.input); } } window_layout.pop(); }