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/
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
// load_rom, anything rom related
import "core:log"
import "core:os"
+18 -3
View File
@@ -35,12 +35,13 @@ Layout :: struct {
bottom_panel : rl.Rectangle,
cpu : rl.Rectangle,
status_bar : rl.Rectangle,
info_box : rl.Rectangle,
}
// Initialize main the gui 'window'
run_gui :: proc(sim: ^Simulator) {
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.SetTargetFPS(60)
beep := rl.LoadSound("./assets/sounds/beep.wav")
@@ -70,9 +71,10 @@ run_gui :: proc(sim: ^Simulator) {
rl.BeginDrawing()
rl.ClearBackground(rl.Color{0x18, 0x18, 0x18, 0xFF})
cycles := int(sim.cpu_hz / SIM_FPS)
if (!sim.paused) {
// Cycle the machine to update memory etc
emu.run_machine(sim.machine, 12)
emu.run_machine(sim.machine, cycles)
tick_timers(sim, beep)
}
@@ -102,9 +104,13 @@ run_gui :: proc(sim: ^Simulator) {
// Bottom
// ------------------------------------------
gui_bottom_tabs(layout.bottom_panel, sim)
gui_bottom_panel(layout.bottom_panel, sim)
gui_status_bar(layout.status_bar, sim)
// Info Box
// ------------------------------------------
gui_info_box(layout.info_box, sim, screen_width, screen_height)
rl.EndDrawing()
}
@@ -195,5 +201,14 @@ calc_layout :: proc(screen_width: f32, screen_height: f32) -> Layout {
width = screen_width,
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"
gui_bottom_tabs :: proc(rect: rl.Rectangle, sim: ^Simulator) {
gui_bottom_panel :: proc(rect: rl.Rectangle, sim: ^Simulator) {
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 {
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,
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 {
x = rect.x + memory_view_bounds.width + PADDING_X,
y = rect.y + PADDING_Y + tab_bar_rect.height,
width = (rect.width - (PADDING_X * 2)) / 2,
height = rect.height - (PADDING_Y * 2) - tab_bar_rect.height,
y = rect.y + PADDING_Y,
width = ((rect.width - (PADDING_X * 2)) / 2) / 2,
height = rect.height - (PADDING_Y * 2),
}
gui_tab_disasm(disasm_view_bounds, sim)
switch sim.active_tab {
case 0: // draw registers panel
gui_tab_memory(memory_view_bounds, sim)
gui_tab_disasm(disasm_view_bounds, sim)
case 1: // draw memory panel
case 2: // draw display panel
stack_view_bounds := rl.Rectangle {
x = disasm_view_bounds.x + disasm_view_bounds.width,
y = rect.y + PADDING_Y,
width = ((rect.width - (PADDING_X * 2)) / 2) / 2,
height = rect.height - (PADDING_Y * 2),
}
gui_tab_stack(stack_view_bounds, sim)
}
+32 -4
View File
@@ -1,5 +1,6 @@
package simulator
import "core:fmt"
import "core:log"
import emu "../machine"
@@ -36,14 +37,41 @@ gui_control_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
emu.reset_machine(sim.machine)
}
// @TODO: Get this working
slider_rect := rl.Rectangle{
x = rect.width - 100 - 50,
y = rect.y + 15,
x = rect.width - 210,
y = rect.y + PADDING_X + 5,
width = 100,
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 {
+1 -1
View File
@@ -88,7 +88,7 @@ gui_file_loader :: proc(rect: rl.Rectangle, sim: ^Simulator) {
content_rect := rl.Rectangle {
x = panel_rect.x,
y = panel_rect.y - PANEL_HEADER,
width = panel_rect.width + PANEL_HEADER,
width = panel_rect.width - 15,
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
import "core:fmt"
import "core:strings"
import rl "vendor:raylib"
gui_tab_disasm :: proc(rect: rl.Rectangle, sim: ^Simulator) {
rl.DrawRectangleRec(rect, rl.DARKGRAY)
// Header background
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)
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)
// Total height of all instructions
content_rect := rl.Rectangle {
x = panel_rect.x,
y = panel_rect.y - PANEL_HEADER,
width = panel_rect.width + PANEL_HEADER,
height = f32(len(sim.disasm)) * LINE_HEIGHT
x = panel_rect.x,
y = panel_rect.y - PANEL_HEADER,
width = panel_rect.width - 15,
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.BeginScissorMode(i32(view.x), i32(view.y), i32(view.width), i32(view.height))
defer rl.EndScissorMode()
index_x := panel_rect.x + PADDING_X
disasm_x := index_x + 70
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)
bg_color := rl.DARKGRAY if entry.address != sim.machine.pc else rl.BLACK
txt_color := rl.WHITE
if is_active {
rl.DrawRectangleV(
{panel_rect.x + sim.disasm_scroll.x, y_pos},
{panel_rect.width, LINE_HEIGHT},
rl.BLACK,
)
}
rl.DrawRectangleV(
{panel_rect.x + sim.disasm_scroll.x, y_pos},
{panel_rect.width, LINE_HEIGHT},
bg_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,
)
txt_index := fmt.ctprintf("0x%04X", entry.address)
txt_disasm := fmt.ctprintf(": %s", entry.str)
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)
}
if sim.disasm_follow {
for entry, i in sim.disasm[:sim.disasm_count] {
if entry.address == sim.machine.pc {
target_y := f32(i) * LINE_HEIGHT
target_y := f32(i) * LINE_HEIGHT
visible_height := panel_rect.height
// Center the active instruction in the panel
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)
sim.disasm_scroll.y = clamp(sim.disasm_scroll.y, max_scroll, 0)
break
+1 -1
View File
@@ -54,7 +54,7 @@ gui_tab_memory :: proc(rect: rl.Rectangle, sim: ^Simulator) {
content_rect := rl.Rectangle{
0, 0,
rect.width - 12,
rect.width - 15,
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)
// Each instruction is 2 bytes, 3584 / 2 = 1792 instructions.
MAX_INSTRUCTIONS :: 1792
SIM_FPS :: 60
DEFAULT_CPU_HZ :: 700
MIN_HZ :: 100
MAX_HZ :: 1500
Instruction :: struct {
address: u16,
@@ -22,14 +26,16 @@ Simulator :: struct {
rom_loaded: bool,
paused: bool,
step: bool,
cycles_per_second: int,
speed_value: f32,
cpu_hz: f32,
info_box: bool,
// GUI
font: rl.Font,
active_tab: i32,
mem_scroll: rl.Vector2,
rompick_scroll: rl.Vector2,
selected_rom: string,
// stack props
stack_scroll: rl.Vector2,
// disassembly props
disasm: [MAX_INSTRUCTIONS]Instruction,
disasm_follow: bool,
@@ -44,8 +50,8 @@ run_simulator :: proc(s: ^emu.System) {
rom_loaded = false,
paused = true,
step = false,
cycles_per_second = 12,
disasm_follow = true
cpu_hz = 700,
disasm_follow = true,
}
run_gui(&sim)