1
0
chip8-zig/src/main-scene.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();
}