360 lines
12 KiB
Zig
360 lines
12 KiB
Zig
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();
|
|
}
|