Compare commits

..

7 Commits

Author SHA1 Message Date
jasonhilder 30a37f26c5 Updated fzf ignore file, added raylib window title 2026-06-24 10:34:02 +02:00
jasonhilder 97f34d72a7 Added a stack view in bottom panel 2026-06-24 10:33:39 +02:00
jasonhilder 65b692f293 Added an info box for name, repo link. 2026-06-24 10:33:09 +02:00
jasonhilder 6d706a34cd Dropped tab layout, floating info window and clean
No longer using tabs, all info in one bottom panel.
Added a floating info window, can open from control bar.
Cleanup vertical widths for scroll bars in some panes.
2026-06-24 10:31:09 +02:00
jasonhilder 6a43058033 Added adjustable speed for simulator.
Added struct fields, calcs for the correct cycles per frame by hz value.
Updated control bar to wire it all up and make sure it updates in real
time.
2026-06-24 10:21:52 +02:00
jasonhilder 7403efa6cf Tiny cleanup 2026-06-24 10:13:37 +02:00
jasonhilder 9d83c49872 Updated showcase. 2026-06-24 08:12:35 +02:00
12 changed files with 216 additions and 72 deletions
+1
View File
@@ -1,2 +1,3 @@
external/ external/
assets/ assets/
src/roms
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 322 KiB

-2
View File
@@ -1,7 +1,5 @@
package machine package machine
// load_rom, anything rom related
import "core:log" import "core:log"
import "core:os" import "core:os"
+18 -3
View File
@@ -35,12 +35,13 @@ Layout :: struct {
bottom_panel : rl.Rectangle, bottom_panel : rl.Rectangle,
cpu : rl.Rectangle, cpu : rl.Rectangle,
status_bar : rl.Rectangle, status_bar : rl.Rectangle,
info_box : rl.Rectangle,
} }
// Initialize main the gui 'window' // Initialize main the gui 'window'
run_gui :: proc(sim: ^Simulator) { run_gui :: proc(sim: ^Simulator) {
rl.SetConfigFlags({.WINDOW_RESIZABLE}) rl.SetConfigFlags({.WINDOW_RESIZABLE})
rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "raylib") rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Octal Cookie - Chip 8 Simulator")
rl.InitAudioDevice() rl.InitAudioDevice()
rl.SetTargetFPS(60) rl.SetTargetFPS(60)
beep := rl.LoadSound("./assets/sounds/beep.wav") beep := rl.LoadSound("./assets/sounds/beep.wav")
@@ -70,9 +71,10 @@ run_gui :: proc(sim: ^Simulator) {
rl.BeginDrawing() rl.BeginDrawing()
rl.ClearBackground(rl.Color{0x18, 0x18, 0x18, 0xFF}) rl.ClearBackground(rl.Color{0x18, 0x18, 0x18, 0xFF})
cycles := int(sim.cpu_hz / SIM_FPS)
if (!sim.paused) { if (!sim.paused) {
// Cycle the machine to update memory etc // Cycle the machine to update memory etc
emu.run_machine(sim.machine, 12) emu.run_machine(sim.machine, cycles)
tick_timers(sim, beep) tick_timers(sim, beep)
} }
@@ -102,9 +104,13 @@ run_gui :: proc(sim: ^Simulator) {
// Bottom // Bottom
// ------------------------------------------ // ------------------------------------------
gui_bottom_tabs(layout.bottom_panel, sim) gui_bottom_panel(layout.bottom_panel, sim)
gui_status_bar(layout.status_bar, sim) gui_status_bar(layout.status_bar, sim)
// Info Box
// ------------------------------------------
gui_info_box(layout.info_box, sim, screen_width, screen_height)
rl.EndDrawing() rl.EndDrawing()
} }
@@ -195,5 +201,14 @@ calc_layout :: proc(screen_width: f32, screen_height: f32) -> Layout {
width = screen_width, width = screen_width,
height = STATUS_BAR_H, height = STATUS_BAR_H,
}, },
// ------------------------------------------
// Info Box
info_box = rl.Rectangle{
x = (screen_width / 2 - 200),
y = (screen_height / 2 - 200),
width = 400,
height = 140
}
} }
} }
+15 -25
View File
@@ -2,40 +2,30 @@ package simulator
import rl "vendor:raylib" import rl "vendor:raylib"
gui_bottom_tabs :: proc(rect: rl.Rectangle, sim: ^Simulator) { gui_bottom_panel :: proc(rect: rl.Rectangle, sim: ^Simulator) {
rl.DrawRectangleLinesEx(rect, 1, rl.GRAY) rl.DrawRectangleLinesEx(rect, 1, rl.GRAY)
// Setup tab bar
tabs := [?]cstring{ "Memory", "Log" }
tab_bar_rect := rl.Rectangle{
rect.x,
rect.y,
rect.width,
BUTTON_HEIGHT + PADDING_Y
}
// inside draw loop:
rl.GuiTabBar(tab_bar_rect, &tabs[0], i32(len(tabs)), &sim.active_tab)
memory_view_bounds := rl.Rectangle { memory_view_bounds := rl.Rectangle {
x = rect.x + PADDING_X, x = rect.x + PADDING_X,
y = rect.y + PADDING_Y + tab_bar_rect.height, y = rect.y + PADDING_Y,
width = (rect.width - (PADDING_X * 2)) / 2, width = (rect.width - (PADDING_X * 2)) / 2,
height = rect.height - (PADDING_Y * 2) - tab_bar_rect.height, height = rect.height - (PADDING_Y * 2),
} }
gui_tab_memory(memory_view_bounds, sim)
disasm_view_bounds := rl.Rectangle { disasm_view_bounds := rl.Rectangle {
x = rect.x + memory_view_bounds.width + PADDING_X, x = rect.x + memory_view_bounds.width + PADDING_X,
y = rect.y + PADDING_Y + tab_bar_rect.height, y = rect.y + PADDING_Y,
width = (rect.width - (PADDING_X * 2)) / 2, width = ((rect.width - (PADDING_X * 2)) / 2) / 2,
height = rect.height - (PADDING_Y * 2) - tab_bar_rect.height, height = rect.height - (PADDING_Y * 2),
} }
gui_tab_disasm(disasm_view_bounds, sim)
switch sim.active_tab { stack_view_bounds := rl.Rectangle {
case 0: // draw registers panel x = disasm_view_bounds.x + disasm_view_bounds.width,
gui_tab_memory(memory_view_bounds, sim) y = rect.y + PADDING_Y,
gui_tab_disasm(disasm_view_bounds, sim) width = ((rect.width - (PADDING_X * 2)) / 2) / 2,
case 1: // draw memory panel height = rect.height - (PADDING_Y * 2),
case 2: // draw display panel
} }
gui_tab_stack(stack_view_bounds, sim)
} }
+32 -4
View File
@@ -1,5 +1,6 @@
package simulator package simulator
import "core:fmt"
import "core:log" import "core:log"
import emu "../machine" import emu "../machine"
@@ -36,14 +37,41 @@ gui_control_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
emu.reset_machine(sim.machine) emu.reset_machine(sim.machine)
} }
// @TODO: Get this working
slider_rect := rl.Rectangle{ slider_rect := rl.Rectangle{
x = rect.width - 100 - 50, x = rect.width - 210,
y = rect.y + 15, y = rect.y + PADDING_X + 5,
width = 100, width = 100,
height = 15 height = 15
} }
rl.GuiSlider(slider_rect, "Speed ", nil, &sim.speed_value, 0, 100) hz_label := fmt.ctprintf("%dHz", int(sim.cpu_hz))
rl.GuiSlider(slider_rect, "Speed ", hz_label, &sim.cpu_hz, 200, MAX_HZ)
mouse := rl.GetMousePosition()
if rl.CheckCollisionPointRec(mouse, slider_rect) {
rl.SetMouseCursor(.POINTING_HAND)
} else {
rl.SetMouseCursor(.DEFAULT)
}
reset_rect := rl.Rectangle{
x = slider_rect.x - slider_rect.width - 20,
y = slider_rect.y,
width = 55,
height = 15,
}
if rl.GuiButton(reset_rect, "reset") {
sim.cpu_hz = DEFAULT_CPU_HZ
}
info_rect := rl.Rectangle{
x = rect.width - 35,
y = rect.y + PADDING_X,
width = 25,
height = BUTTON_HEIGHT,
}
if rl.GuiButton(info_rect, " ? ") {
sim.info_box = true
}
} }
btn :: proc(cursor: ^f32, rect: rl.Rectangle, h, w, gap: f32, label: cstring) -> bool { btn :: proc(cursor: ^f32, rect: rl.Rectangle, h, w, gap: f32, label: cstring) -> bool {
+1 -1
View File
@@ -88,7 +88,7 @@ gui_file_loader :: proc(rect: rl.Rectangle, sim: ^Simulator) {
content_rect := rl.Rectangle { content_rect := rl.Rectangle {
x = panel_rect.x, x = panel_rect.x,
y = panel_rect.y - PANEL_HEADER, y = panel_rect.y - PANEL_HEADER,
width = panel_rect.width + PANEL_HEADER, width = panel_rect.width - 15,
height = f32(len(roms)) * LINE_HEIGHT height = f32(len(roms)) * LINE_HEIGHT
} }
+51
View File
@@ -0,0 +1,51 @@
package simulator
import rl "vendor:raylib"
gui_info_box :: proc(rect: rl.Rectangle, sim: ^Simulator, screen_width: f32, screen_height: f32) {
if sim.info_box {
screen_rect := rl.Rectangle{0, 0, screen_width, screen_height}
rl.DrawRectangleRec(screen_rect, {0, 0, 0, 140})
if rl.GuiWindowBox(rect, "App Info") > 0 {
sim.info_box = false
}
content_y := rect.y + 24
center_x := rect.x + rect.width / 2
name_text : cstring = "Octal Cookie"
desc_text : cstring = "A CHIP-8 emulator and simulator written in Odin."
source_text : cstring = "https://codeberg.org/jasonhilder/octal_cookie"
name_size := rl.MeasureTextEx(sim.font, name_text, 18, 1)
desc_size := rl.MeasureTextEx(sim.font, desc_text, 18, 1)
source_size := rl.MeasureTextEx(sim.font, source_text, 18, 1)
rl.DrawTextEx(sim.font, name_text, {center_x - name_size.x / 2, content_y + 20}, 18, 1, rl.WHITE)
rl.DrawTextEx(sim.font, desc_text, {center_x - desc_size.x / 2, content_y + 46}, 18, 1, rl.WHITE)
source_pos := rl.Vector2{center_x - source_size.x / 2, content_y + 72}
rl.DrawTextEx(sim.font, source_text, source_pos, 18, 1, rl.SKYBLUE)
source_rect := rl.Rectangle{
x = source_pos.x,
y = source_pos.y,
width = source_size.x,
height = source_size.y,
}
mouse := rl.GetMousePosition()
if rl.CheckCollisionPointRec(mouse, source_rect) {
rl.SetMouseCursor(.POINTING_HAND)
rl.DrawRectangleRec( {source_pos.x, source_pos.y + source_size.y - 1, source_size.x, 1}, rl.SKYBLUE,)
if rl.IsMouseButtonPressed(.LEFT) {
rl.OpenURL("https://codeberg.org/jasonhilder/octal_cookie")
}
} else {
rl.SetMouseCursor(.DEFAULT)
}
}
}
+24 -32
View File
@@ -1,15 +1,11 @@
package simulator package simulator
import "core:fmt" import "core:fmt"
import "core:strings"
import rl "vendor:raylib" import rl "vendor:raylib"
gui_tab_disasm :: proc(rect: rl.Rectangle, sim: ^Simulator) { gui_tab_disasm :: proc(rect: rl.Rectangle, sim: ^Simulator) {
rl.DrawRectangleRec(rect, rl.DARKGRAY)
// Header background // Header background
rl.DrawRectangle(i32(rect.x), i32(rect.y), i32(rect.width), i32(PANEL_HEADER), rl.BLACK) rl.DrawRectangle(i32(rect.x), i32(rect.y), i32(rect.width), i32(PANEL_HEADER), rl.BLACK)
// Header: Address label
rl.DrawTextEx(sim.font, "Disassembled Instructions", {rect.x + 4, rect.y + 4}, 18, 1, rl.WHITE) rl.DrawTextEx(sim.font, "Disassembled Instructions", {rect.x + 4, rect.y + 4}, 18, 1, rl.WHITE)
follow_label : cstring = "Follow: ON" if sim.disasm_follow else "Follow: OFF" follow_label : cstring = "Follow: ON" if sim.disasm_follow else "Follow: OFF"
@@ -27,51 +23,47 @@ gui_tab_disasm :: proc(rect: rl.Rectangle, sim: ^Simulator) {
} }
rl.GuiPanel(panel_rect, nil) rl.GuiPanel(panel_rect, nil)
// Total height of all instructions
content_rect := rl.Rectangle { content_rect := rl.Rectangle {
x = panel_rect.x, x = panel_rect.x,
y = panel_rect.y - PANEL_HEADER, y = panel_rect.y - PANEL_HEADER,
width = panel_rect.width + PANEL_HEADER, width = panel_rect.width - 15,
height = f32(len(sim.disasm)) * LINE_HEIGHT height = f32(len(sim.disasm)) * LINE_HEIGHT,
} }
view: rl.Rectangle view: rl.Rectangle
rl.GuiScrollPanel(panel_rect, nil, content_rect, &sim.disasm_scroll, &view) rl.GuiScrollPanel(panel_rect, nil, content_rect, &sim.disasm_scroll, &view)
rl.BeginScissorMode(i32(view.x), i32(view.y), i32(view.width), i32(view.height)) rl.BeginScissorMode(i32(view.x), i32(view.y), i32(view.width), i32(view.height))
defer rl.EndScissorMode() defer rl.EndScissorMode()
index_x := panel_rect.x + PADDING_X
disasm_x := index_x + 70
for entry, i in sim.disasm[:sim.disasm_count] { for entry, i in sim.disasm[:sim.disasm_count] {
y_pos := panel_rect.y + (f32(i) * LINE_HEIGHT) + sim.disasm_scroll.y y_pos := panel_rect.y + (f32(i) * LINE_HEIGHT) + sim.disasm_scroll.y
is_active := entry.address == sim.machine.pc
color := rl.WHITE
txt := fmt.tprintf("%d : %s", i, entry.str) if is_active {
bg_color := rl.DARKGRAY if entry.address != sim.machine.pc else rl.BLACK rl.DrawRectangleV(
txt_color := rl.WHITE {panel_rect.x + sim.disasm_scroll.x, y_pos},
{panel_rect.width, LINE_HEIGHT},
rl.BLACK,
)
}
rl.DrawRectangleV( txt_index := fmt.ctprintf("0x%04X", entry.address)
{panel_rect.x + sim.disasm_scroll.x, y_pos}, txt_disasm := fmt.ctprintf(": %s", entry.str)
{panel_rect.width, LINE_HEIGHT},
bg_color, rl.DrawTextEx(sim.font, txt_index, {index_x + sim.disasm_scroll.x, y_pos}, 18, 1, color)
) rl.DrawTextEx(sim.font, txt_disasm, {disasm_x + sim.disasm_scroll.x, y_pos}, 18, 1, color)
rl.DrawTextEx(
sim.font,
strings.clone_to_cstring(txt, context.temp_allocator),
{panel_rect.x + PADDING_X + sim.disasm_scroll.x, y_pos},
18, 1,
txt_color,
)
} }
if sim.disasm_follow { if sim.disasm_follow {
for entry, i in sim.disasm[:sim.disasm_count] { for entry, i in sim.disasm[:sim.disasm_count] {
if entry.address == sim.machine.pc { if entry.address == sim.machine.pc {
target_y := f32(i) * LINE_HEIGHT target_y := f32(i) * LINE_HEIGHT
visible_height := panel_rect.height visible_height := panel_rect.height
// Center the active instruction in the panel
sim.disasm_scroll.y = -(target_y - visible_height / 2) sim.disasm_scroll.y = -(target_y - visible_height / 2)
// Clamp so we don't scroll past the content
max_scroll := -(f32(sim.disasm_count) * LINE_HEIGHT - visible_height) max_scroll := -(f32(sim.disasm_count) * LINE_HEIGHT - visible_height)
sim.disasm_scroll.y = clamp(sim.disasm_scroll.y, max_scroll, 0) sim.disasm_scroll.y = clamp(sim.disasm_scroll.y, max_scroll, 0)
break break
+1 -1
View File
@@ -54,7 +54,7 @@ gui_tab_memory :: proc(rect: rl.Rectangle, sim: ^Simulator) {
content_rect := rl.Rectangle{ content_rect := rl.Rectangle{
0, 0, 0, 0,
rect.width - 12, rect.width - 15,
f32(MEM_VIRTUAL_ROWS) * MEM_ROW_H, f32(MEM_VIRTUAL_ROWS) * MEM_ROW_H,
} }
+63
View File
@@ -0,0 +1,63 @@
package simulator
import "core:fmt"
import rl "vendor:raylib"
gui_tab_stack :: proc(rect: rl.Rectangle, sim: ^Simulator) {
// Panel below header
panel_rect := rl.Rectangle{
rect.x,
rect.y + PANEL_HEADER,
rect.width,
rect.height - PANEL_HEADER,
}
rl.GuiPanel(panel_rect, nil)
// Header background
rl.DrawRectangle(i32(rect.x), i32(rect.y), i32(rect.width), i32(PANEL_HEADER), rl.BLACK)
rl.DrawTextEx(sim.font, "Stack", {rect.x + 4, rect.y + 4}, 18, 1, rl.WHITE)
// SP indicator in header
sp_label := fmt.ctprintf("SP: %d", sim.machine.sp)
sp_size := rl.MeasureTextEx(sim.font, sp_label, 18, 1)
rl.DrawTextEx(sim.font, sp_label, {rect.x + rect.width - sp_size.x - 8, rect.y + 4}, 18, 1, rl.WHITE)
// Use full panel height so empty slots fill the space
total_height := max(f32(16) * LINE_HEIGHT, panel_rect.height)
content_rect := rl.Rectangle{
x = panel_rect.x,
y = panel_rect.y - PANEL_HEADER,
width = panel_rect.width - 15,
height = total_height,
}
view: rl.Rectangle
rl.GuiScrollPanel(panel_rect, nil, content_rect, &sim.stack_scroll, &view)
rl.BeginScissorMode(i32(view.x), i32(view.y), i32(view.width), i32(view.height))
defer rl.EndScissorMode()
index_x := panel_rect.x + PADDING_X
colon_x := index_x + 30
for i in 0..<16 {
y_pos := panel_rect.y + (f32(i) * LINE_HEIGHT) + sim.stack_scroll.y
is_active := i < int(sim.machine.sp)
is_top := i == int(sim.machine.sp) - 1
// Only draw a background for active slots
if is_active {
rl.DrawRectangleV(
{panel_rect.x + sim.stack_scroll.x, y_pos},
{panel_rect.width, LINE_HEIGHT},
rl.BLACK,
)
}
sp_marker := ""
if is_top do sp_marker = " <- SP"
color := rl.WHITE if is_top else (rl.BLACK if is_active else rl.WHITE)
txt_index := fmt.ctprintf("%02d", i)
txt_value := fmt.ctprintf(": 0x%04X%s", sim.machine.stack[i], sp_marker)
rl.DrawTextEx(sim.font, txt_index, {index_x + sim.stack_scroll.x, y_pos}, 18, 1, color)
rl.DrawTextEx(sim.font, txt_value, {colon_x + sim.stack_scroll.x, y_pos}, 18, 1, color)
}
}
+10 -4
View File
@@ -9,6 +9,10 @@ roms := #load_directory("../roms")
// CHIP-8 ROM can be at most 3584 bytes (4096 - 0x200 reserved) // CHIP-8 ROM can be at most 3584 bytes (4096 - 0x200 reserved)
// Each instruction is 2 bytes, 3584 / 2 = 1792 instructions. // Each instruction is 2 bytes, 3584 / 2 = 1792 instructions.
MAX_INSTRUCTIONS :: 1792 MAX_INSTRUCTIONS :: 1792
SIM_FPS :: 60
DEFAULT_CPU_HZ :: 700
MIN_HZ :: 100
MAX_HZ :: 1500
Instruction :: struct { Instruction :: struct {
address: u16, address: u16,
@@ -22,14 +26,16 @@ Simulator :: struct {
rom_loaded: bool, rom_loaded: bool,
paused: bool, paused: bool,
step: bool, step: bool,
cycles_per_second: int, cpu_hz: f32,
speed_value: f32, info_box: bool,
// GUI // GUI
font: rl.Font, font: rl.Font,
active_tab: i32, active_tab: i32,
mem_scroll: rl.Vector2, mem_scroll: rl.Vector2,
rompick_scroll: rl.Vector2, rompick_scroll: rl.Vector2,
selected_rom: string, selected_rom: string,
// stack props
stack_scroll: rl.Vector2,
// disassembly props // disassembly props
disasm: [MAX_INSTRUCTIONS]Instruction, disasm: [MAX_INSTRUCTIONS]Instruction,
disasm_follow: bool, disasm_follow: bool,
@@ -44,8 +50,8 @@ run_simulator :: proc(s: ^emu.System) {
rom_loaded = false, rom_loaded = false,
paused = true, paused = true,
step = false, step = false,
cycles_per_second = 12, cpu_hz = 700,
disasm_follow = true disasm_follow = true,
} }
run_gui(&sim) run_gui(&sim)