Compare commits
34 Commits
5b4e968d1c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 745ee7cecb | |||
| 68737e12da | |||
| 85e8481cf4 | |||
| ec8bcdb5ed | |||
| 747702d256 | |||
| 717ef479fa | |||
| 5edae5d2d8 | |||
| d43ec53d8d | |||
| 6140db0d8f | |||
| cc1962ff79 | |||
| 8830331774 | |||
| eded8b60b7 | |||
| 30a37f26c5 | |||
| 97f34d72a7 | |||
| 65b692f293 | |||
| 6d706a34cd | |||
| 6a43058033 | |||
| 7403efa6cf | |||
| 9d83c49872 | |||
| 6ca732f178 | |||
| baa1e1a5cf | |||
| c909d28587 | |||
| 356ed2408a | |||
| 0b5006f985 | |||
| e9cf387640 | |||
| d314ef651e | |||
| 16b97b24b0 | |||
| 61471536ca | |||
| 6f701560df | |||
| 1a6346895a | |||
| 350a26d1b9 | |||
| 19f4593e0e | |||
| c1c67d596d | |||
| ff3ff4bfda |
@@ -2,5 +2,10 @@
|
||||
*.bin
|
||||
*.o
|
||||
|
||||
# Artifact output directory
|
||||
build/
|
||||
|
||||
# Project management
|
||||
todo
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
APP_NAME := my_app
|
||||
SRC := ./src
|
||||
SRC := ./src/main_desktop
|
||||
|
||||
.PHONY: all dev release clean
|
||||
|
||||
|
||||
@@ -1,3 +1,80 @@
|
||||
# octal_cookie
|
||||
# Octal Cookie
|
||||
|
||||
A Chip 8 emulator written in Odin
|
||||
A CHIP-8 emulator / simulator written in [Odin](https://odin-lang.org/) using [Raylib](https://www.raylib.com/).
|
||||
|
||||
[](showcase.png)
|
||||
|
||||
**[Try it in your browser →](https://jasonhilder.dev/sim)**
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
This was my refresher project to get back into lower-level programming — specifically the [Odin](https://odin-lang.org/) language and [Raylib](https://www.raylib.com/). Building a CHIP-8 emulator felt like the right scope: small enough to actually finish, but with enough surface area (opcode decoding, memory, timers, a display, input) to shake the rust off.
|
||||
|
||||
It's not perfect, but it's functioning and usable for the most part. Compiles to native and to WebAssembly, and the web build is playable directly at the link above.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- Full CHIP-8 instruction set emulation
|
||||
- Simulator GUI with dedicated panels for the screen, CPU state, keypad, file loader, memory viewer, and control bar
|
||||
- Built-in collection of classic game ROMs (Pong, Tetris, Space Invaders, Brix, and more)
|
||||
- Load your own ROMs via the file loader panel
|
||||
- Dev and release build modes via `make`
|
||||
- Compiles to WebAssembly for running in-browser
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
Future work, mainly once I'm back in the headspace for it:
|
||||
|
||||
- General refactor/cleanup pass now that the core is working
|
||||
- CHIP-8 quirks support (configurable behavior differences between interpreters)
|
||||
- SUPER-CHIP instruction support
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [Odin compiler](https://odin-lang.org/docs/install/)
|
||||
- [Raylib](https://www.raylib.com/) (used via Odin's vendor bindings)
|
||||
- [tinyfiledialogs](https://sourceforge.net/projects/tinyfiledialogs/) (included under `external/`)
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
# Run directly in dev mode
|
||||
make
|
||||
|
||||
# Build a dev binary
|
||||
make dev
|
||||
|
||||
# Build an optimised release binary
|
||||
make release
|
||||
|
||||
# Clean build output
|
||||
make clean
|
||||
```
|
||||
|
||||
The output binary is named `my_app` by default. You can change `APP_NAME` in the `Makefile`.
|
||||
|
||||
---
|
||||
|
||||
## Included ROMs
|
||||
|
||||
A set of public domain game ROMs is bundled under `assets/game_roms/`:
|
||||
|
||||
`15PUZZLE` · `BLINKY` · `BLITZ` · `BRIX` · `CONNECT4` · `GUESS` · `HIDDEN` · `INVADERS` · `KALEID` · `MAZE` · `MERLIN` · `MISSILE` · `PONG` · `PONG2` · `PUZZLE` · `SYZYGY` · `TANK` · `TETRIS` · `TICTAC` · `UFO` · `VBRIX` · `VERS` · `WIPEOFF`
|
||||
|
||||
Test ROMs are available under `assets/test_roms/` for development and debugging.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
|
||||
Binary file not shown.
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash -eu
|
||||
|
||||
# Point this to where you installed emscripten. Optional on systems that already
|
||||
# have `emcc` in the path.
|
||||
EMSCRIPTEN_SDK_DIR="$HOME/Probe/emsdk"
|
||||
OUT_DIR="build/web"
|
||||
|
||||
mkdir -p $OUT_DIR
|
||||
|
||||
export EMSDK_QUIET=1
|
||||
[[ -f "$EMSCRIPTEN_SDK_DIR/emsdk_env.sh" ]] && . "$EMSCRIPTEN_SDK_DIR/emsdk_env.sh"
|
||||
|
||||
# Note RAYLIB_WASM_LIB=env.o -- env.o is an internal WASM object file. You can
|
||||
# see how RAYLIB_WASM_LIB is used inside <odin>/vendor/raylib/raylib.odin.
|
||||
#
|
||||
# The emcc call will be fed the actual raylib library file. That stuff will end
|
||||
# up in env.o
|
||||
#
|
||||
# Note that there is a rayGUI equivalent: -define:RAYGUI_WASM_LIB=env.o
|
||||
# odin build src/main_web -target:js_wasm32 -build-mode:obj -define:RAYLIB_WASM_LIB=env.o -define:RAYGUI_WASM_LIB=env.o -vet -strict-style -out:$OUT_DIR/game.wasm.o
|
||||
odin build src/main_web -target:js_wasm32 -build-mode:obj -define:RAYLIB_WASM_LIB=env.o -define:RAYGUI_WASM_LIB=env.o -out:$OUT_DIR/game.wasm.o
|
||||
|
||||
ODIN_PATH=$(odin root)
|
||||
|
||||
cp $ODIN_PATH/core/sys/wasm/js/odin.js $OUT_DIR
|
||||
|
||||
files="$OUT_DIR/game.wasm.o ${ODIN_PATH}/vendor/raylib/wasm/libraylib.a ${ODIN_PATH}/vendor/raylib/wasm/libraygui.a"
|
||||
|
||||
# index_template.html contains the javascript code that calls the procedures in
|
||||
# src/main_web/main_web.odin
|
||||
flags="-sEXPORTED_RUNTIME_METHODS=['HEAPF32'] -sSTACK_SIZE=524288 -sUSE_GLFW=3 -sWASM_BIGINT -sWARN_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS --shell-file src/main_web/index_template.html --preload-file assets"
|
||||
|
||||
# For debugging: Add `-g` to `emcc` (gives better error callstack in chrome)
|
||||
emcc -o $OUT_DIR/index.html $files $flags
|
||||
|
||||
rm $OUT_DIR/game.wasm.o
|
||||
|
||||
echo "Web build created in ${OUT_DIR}"
|
||||
@@ -1,3 +1,4 @@
|
||||
#+build !js
|
||||
package tinyfiledialogs
|
||||
|
||||
import "base:builtin"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 322 KiB |
+12
-11
@@ -1,7 +1,5 @@
|
||||
package machine
|
||||
|
||||
import "core:log"
|
||||
|
||||
// System struct, init, constants, fontset
|
||||
|
||||
System :: struct {
|
||||
@@ -48,7 +46,7 @@ FONT_SET := [80]u8 {
|
||||
}
|
||||
|
||||
init :: proc() -> System {
|
||||
log.info("Booting chip 8 cpu")
|
||||
// log.info("Booting chip 8 cpu")
|
||||
|
||||
// Structs are zero initialized so timers, sp etc are good.
|
||||
s := System { pc = 0x200 }
|
||||
@@ -69,17 +67,20 @@ run_machine :: proc(s: ^System, cycles: int) {
|
||||
}
|
||||
}
|
||||
|
||||
new_machine :: proc() -> System {
|
||||
s: System
|
||||
reset_machine :: proc(s: ^System) {
|
||||
s.memory = {}
|
||||
s.v = {}
|
||||
s.stack = {}
|
||||
s.sp = 0
|
||||
s.i = 0
|
||||
s.pc = 0x200
|
||||
s.display = {}
|
||||
s.keypad = {}
|
||||
s.current_key = -1
|
||||
// load fonts into the memory
|
||||
s.delay_timer = 0
|
||||
s.sound_timer = 0
|
||||
|
||||
for v, i in FONT_SET {
|
||||
s.memory[i] = v
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
reset_machine :: proc(s: ^System) {
|
||||
s^ = new_machine()
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
#+build !js
|
||||
package machine
|
||||
|
||||
// load_rom, anything rom related
|
||||
|
||||
import "core:log"
|
||||
import "core:os"
|
||||
|
||||
load_rom :: proc(s: ^System, file_path: string) -> os.Error {
|
||||
load_rom :: proc(s: ^System, file_path: string) -> (int, os.Error) {
|
||||
log.info("Loading rom from file")
|
||||
|
||||
data, read_err := os.read_entire_file(file_path, context.allocator)
|
||||
if read_err != os.ERROR_NONE {
|
||||
log.errorf("failed to read rom %v", read_err)
|
||||
return read_err
|
||||
return 0, read_err
|
||||
}
|
||||
defer delete(data)
|
||||
|
||||
@@ -21,5 +20,5 @@ load_rom :: proc(s: ^System, file_path: string) -> os.Error {
|
||||
|
||||
log.infof("First few bytes: %X %X %X %X", s.memory[0x200], s.memory[0x201], s.memory[0x202], s.memory[0x203])
|
||||
|
||||
return nil
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package main
|
||||
import "core:log"
|
||||
import "core:mem"
|
||||
|
||||
import emu "machine"
|
||||
import sim "simulator"
|
||||
import emu "../machine"
|
||||
import sim "../simulator"
|
||||
|
||||
DEV :: #config(DEV, false)
|
||||
|
||||
@@ -22,9 +22,22 @@ main :: proc() {
|
||||
|
||||
// Init the emu 8 "cpu"
|
||||
system := emu.init()
|
||||
s := sim.Simulator {
|
||||
machine = &system,
|
||||
rom_loaded = false,
|
||||
paused = true,
|
||||
step = false,
|
||||
cpu_hz = 700,
|
||||
disasm_follow = true,
|
||||
}
|
||||
|
||||
// Initilize sim, gui etc
|
||||
sim.run_simulator(&system)
|
||||
sim.init(&s)
|
||||
|
||||
for sim.should_run() {
|
||||
sim.update()
|
||||
}
|
||||
|
||||
sim.shutdown()
|
||||
|
||||
when DEV {
|
||||
if len(track.allocation_map) > 0 {
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
This allocator uses the malloc, calloc, free and realloc procs that emscripten
|
||||
exposes in order to allocate memory. Just like Odin's default heap allocator
|
||||
this uses proper alignment, so that maps and simd works.
|
||||
*/
|
||||
|
||||
package main_web
|
||||
|
||||
import "core:mem"
|
||||
import "core:c"
|
||||
import "base:intrinsics"
|
||||
|
||||
// This will create bindings to emscripten's implementation of libc
|
||||
// memory allocation features.
|
||||
@(default_calling_convention = "c")
|
||||
foreign {
|
||||
calloc :: proc(num, size: c.size_t) -> rawptr ---
|
||||
free :: proc(ptr: rawptr) ---
|
||||
malloc :: proc(size: c.size_t) -> rawptr ---
|
||||
realloc :: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
|
||||
}
|
||||
|
||||
emscripten_allocator :: proc "contextless" () -> mem.Allocator {
|
||||
return mem.Allocator{emscripten_allocator_proc, nil}
|
||||
}
|
||||
|
||||
emscripten_allocator_proc :: proc(
|
||||
allocator_data: rawptr,
|
||||
mode: mem.Allocator_Mode,
|
||||
size, alignment: int,
|
||||
old_memory: rawptr,
|
||||
old_size: int,
|
||||
location := #caller_location
|
||||
) -> (data: []byte, err: mem.Allocator_Error) {
|
||||
// These aligned alloc procs are almost indentical those in
|
||||
// `_heap_allocator_proc` in `core:os`. Without the proper alignment you
|
||||
// cannot use maps and simd features.
|
||||
|
||||
aligned_alloc :: proc(size, alignment: int, zero_memory: bool, old_ptr: rawptr = nil) -> ([]byte, mem.Allocator_Error) {
|
||||
a := max(alignment, align_of(rawptr))
|
||||
space := size + a - 1
|
||||
|
||||
allocated_mem: rawptr
|
||||
if old_ptr != nil {
|
||||
original_old_ptr := mem.ptr_offset((^rawptr)(old_ptr), -1)^
|
||||
allocated_mem = realloc(original_old_ptr, c.size_t(space+size_of(rawptr)))
|
||||
} else if zero_memory {
|
||||
// calloc automatically zeros memory, but it takes a number + size
|
||||
// instead of just size.
|
||||
allocated_mem = calloc(c.size_t(space+size_of(rawptr)), 1)
|
||||
} else {
|
||||
allocated_mem = malloc(c.size_t(space+size_of(rawptr)))
|
||||
}
|
||||
aligned_mem := rawptr(mem.ptr_offset((^u8)(allocated_mem), size_of(rawptr)))
|
||||
|
||||
ptr := uintptr(aligned_mem)
|
||||
aligned_ptr := (ptr - 1 + uintptr(a)) & -uintptr(a)
|
||||
diff := int(aligned_ptr - ptr)
|
||||
if (size + diff) > space || allocated_mem == nil {
|
||||
return nil, .Out_Of_Memory
|
||||
}
|
||||
|
||||
aligned_mem = rawptr(aligned_ptr)
|
||||
mem.ptr_offset((^rawptr)(aligned_mem), -1)^ = allocated_mem
|
||||
|
||||
return mem.byte_slice(aligned_mem, size), nil
|
||||
}
|
||||
|
||||
aligned_free :: proc(p: rawptr) {
|
||||
if p != nil {
|
||||
free(mem.ptr_offset((^rawptr)(p), -1)^)
|
||||
}
|
||||
}
|
||||
|
||||
aligned_resize :: proc(p: rawptr, old_size: int, new_size: int, new_alignment: int) -> ([]byte, mem.Allocator_Error) {
|
||||
if p == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return aligned_alloc(new_size, new_alignment, true, p)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case .Alloc:
|
||||
return aligned_alloc(size, alignment, true)
|
||||
|
||||
case .Alloc_Non_Zeroed:
|
||||
return aligned_alloc(size, alignment, false)
|
||||
|
||||
case .Free:
|
||||
aligned_free(old_memory)
|
||||
return nil, nil
|
||||
|
||||
case .Resize:
|
||||
if old_memory == nil {
|
||||
return aligned_alloc(size, alignment, true)
|
||||
}
|
||||
|
||||
bytes := aligned_resize(old_memory, old_size, size, alignment) or_return
|
||||
|
||||
// realloc doesn't zero the new bytes, so we do it manually.
|
||||
if size > old_size {
|
||||
new_region := raw_data(bytes[old_size:])
|
||||
intrinsics.mem_zero(new_region, size - old_size)
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
|
||||
case .Resize_Non_Zeroed:
|
||||
if old_memory == nil {
|
||||
return aligned_alloc(size, alignment, false)
|
||||
}
|
||||
|
||||
return aligned_resize(old_memory, old_size, size, alignment)
|
||||
|
||||
case .Query_Features:
|
||||
set := (^mem.Allocator_Mode_Set)(old_memory)
|
||||
if set != nil {
|
||||
set^ = {.Alloc, .Free, .Resize, .Query_Features}
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
case .Free_All, .Query_Info:
|
||||
return nil, .Mode_Not_Implemented
|
||||
}
|
||||
return nil, .Mode_Not_Implemented
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
This logger is largely a copy of the console logger in `core:log`, but it uses
|
||||
emscripten's `puts` proc to write into he console of the web browser.
|
||||
|
||||
This is more or less identical to the logger in Aronicu's repository:
|
||||
https://github.com/Aronicu/Raylib-WASM/tree/main
|
||||
*/
|
||||
|
||||
package main_web
|
||||
|
||||
import "core:c"
|
||||
import "core:fmt"
|
||||
import "core:log"
|
||||
import "core:strings"
|
||||
|
||||
Emscripten_Logger_Opts :: log.Options{.Level, .Short_File_Path, .Line}
|
||||
|
||||
create_emscripten_logger :: proc (lowest := log.Level.Debug, opt := Emscripten_Logger_Opts) -> log.Logger {
|
||||
return log.Logger{data = nil, procedure = logger_proc, lowest_level = lowest, options = opt}
|
||||
}
|
||||
|
||||
// This create's a binding to `puts` which will be linked in as part of the
|
||||
// emscripten runtime.
|
||||
@(default_calling_convention = "c")
|
||||
foreign {
|
||||
puts :: proc(buffer: cstring) -> c.int ---
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
logger_proc :: proc(
|
||||
logger_data: rawptr,
|
||||
level: log.Level,
|
||||
text: string,
|
||||
options: log.Options,
|
||||
location := #caller_location
|
||||
) {
|
||||
b := strings.builder_make(context.temp_allocator)
|
||||
strings.write_string(&b, Level_Headers[level])
|
||||
do_location_header(options, &b, location)
|
||||
fmt.sbprint(&b, text)
|
||||
|
||||
if bc, bc_err := strings.to_cstring(&b); bc_err == nil {
|
||||
puts(bc)
|
||||
}
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
Level_Headers := [?]string {
|
||||
0 ..< 10 = "[DEBUG] --- ",
|
||||
10 ..< 20 = "[INFO ] --- ",
|
||||
20 ..< 30 = "[WARN ] --- ",
|
||||
30 ..< 40 = "[ERROR] --- ",
|
||||
40 ..< 50 = "[FATAL] --- ",
|
||||
}
|
||||
|
||||
@(private="file")
|
||||
do_location_header :: proc(opts: log.Options, buf: ^strings.Builder, location := #caller_location) {
|
||||
if log.Location_Header_Opts & opts == nil {
|
||||
return
|
||||
}
|
||||
fmt.sbprint(buf, "[")
|
||||
file := location.file_path
|
||||
if .Short_File_Path in opts {
|
||||
last := 0
|
||||
for r, i in location.file_path {
|
||||
if r == '/' {
|
||||
last = i + 1
|
||||
}
|
||||
}
|
||||
file = location.file_path[last:]
|
||||
}
|
||||
|
||||
if log.Location_File_Opts & opts != nil {
|
||||
fmt.sbprint(buf, file)
|
||||
}
|
||||
if .Line in opts {
|
||||
if log.Location_File_Opts & opts != nil {
|
||||
fmt.sbprint(buf, ":")
|
||||
}
|
||||
fmt.sbprint(buf, location.line)
|
||||
}
|
||||
|
||||
if .Procedure in opts {
|
||||
if (log.Location_File_Opts | {.Line}) & opts != nil {
|
||||
fmt.sbprint(buf, ":")
|
||||
}
|
||||
fmt.sbprintf(buf, "%s()", location.procedure)
|
||||
}
|
||||
|
||||
fmt.sbprint(buf, "] ")
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
|
||||
<title>Odin + Raylib on the web</title>
|
||||
<meta name="title" content="Odin + Raylib on the web">
|
||||
<meta name="description" content="Make games using Odin + Raylib that work in the browser">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0px;
|
||||
overflow: hidden;
|
||||
background-color: black;
|
||||
}
|
||||
canvas.game_canvas {
|
||||
max-width: 1550px;
|
||||
border: 0px none;
|
||||
background-color: black;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas class="game_canvas" id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1" onmousedown="event.target.focus()" onkeydown="event.preventDefault()"></canvas>
|
||||
<script type="text/javascript" src="odin.js"></script>
|
||||
<script>
|
||||
var odinMemoryInterface = new odin.WasmMemoryInterface();
|
||||
odinMemoryInterface.setIntSize(4);
|
||||
var odinImports = odin.setupDefaultImports(odinMemoryInterface);
|
||||
|
||||
// The Module is used as configuration for emscripten.
|
||||
var Module = {
|
||||
// This is called by emscripten when it starts up.
|
||||
instantiateWasm: (imports, successCallback) => {
|
||||
const newImports = {
|
||||
...odinImports,
|
||||
...imports
|
||||
}
|
||||
|
||||
return WebAssembly.instantiateStreaming(fetch("index.wasm"), newImports).then(function(output) {
|
||||
var e = output.instance.exports;
|
||||
odinMemoryInterface.setExports(e);
|
||||
odinMemoryInterface.setMemory(e.memory);
|
||||
return successCallback(output.instance);
|
||||
});
|
||||
},
|
||||
// This happens a bit after `instantiateWasm`, when everything is
|
||||
// done setting up. At that point we can run code.
|
||||
onRuntimeInitialized: () => {
|
||||
var e = wasmExports;
|
||||
|
||||
// Calls any procedure marked with @init
|
||||
e._start();
|
||||
|
||||
// See source/main_web/main_web.odin for main_start,
|
||||
// main_update and main_end.
|
||||
e.main_start();
|
||||
|
||||
function send_resize() {
|
||||
var canvas = document.getElementById('canvas');
|
||||
e.web_window_size_changed(canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', function(event) {
|
||||
send_resize();
|
||||
}, true);
|
||||
|
||||
// This can probably be done better: Ideally we'd feed the
|
||||
// initial size to `main_start`. But there seems to be a
|
||||
// race condition. `canvas` doesn't have it's correct size yet.
|
||||
send_resize();
|
||||
|
||||
// Runs the "main loop".
|
||||
function do_main_update() {
|
||||
if (!e.main_update()) {
|
||||
e.main_end();
|
||||
|
||||
// Calls procedures marked with @fini
|
||||
e._end();
|
||||
return;
|
||||
}
|
||||
window.requestAnimationFrame(do_main_update);
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(do_main_update);
|
||||
},
|
||||
print: (function() {
|
||||
var element = document.getElementById("output");
|
||||
if (element) element.value = ''; // clear browser cache
|
||||
return function(text) {
|
||||
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
|
||||
console.log(text);
|
||||
if (element) {
|
||||
element.value += text + "\n";
|
||||
element.scrollTop = element.scrollHeight; // focus on bottom
|
||||
}
|
||||
};
|
||||
})(),
|
||||
canvas: (function() {
|
||||
return document.getElementById("canvas");
|
||||
})()
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Emscripten injects its javascript here -->
|
||||
{{{ SCRIPT }}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
// These procs are the ones that will be called from `index.html`, which is
|
||||
// generated from `index_template.html`.
|
||||
|
||||
package main_web
|
||||
|
||||
import "base:runtime"
|
||||
import "core:c"
|
||||
import "core:mem"
|
||||
|
||||
import emu "../machine"
|
||||
import sim "../simulator"
|
||||
|
||||
@(private="file")
|
||||
web_context: runtime.Context
|
||||
|
||||
@export
|
||||
main_start :: proc "c" () {
|
||||
context = runtime.default_context()
|
||||
|
||||
// The WASM allocator doesn't seem to work properly in combination with
|
||||
// emscripten. There is some kind of conflict with how the manage memory.
|
||||
// So this sets up an allocator that uses emscripten's malloc.
|
||||
context.allocator = emscripten_allocator()
|
||||
runtime.init_global_temporary_allocator(4*mem.Megabyte)
|
||||
|
||||
// Since we now use js_wasm32 we should be able to remove this and use
|
||||
// context.logger = log.create_console_logger(). However, that one produces
|
||||
// extra newlines on web. So it's a bug in that core lib.
|
||||
context.logger = create_emscripten_logger()
|
||||
|
||||
web_context = context
|
||||
|
||||
system := new(emu.System)
|
||||
system^ = emu.init()
|
||||
|
||||
s := new(sim.Simulator)
|
||||
s^ = sim.Simulator {
|
||||
machine = system,
|
||||
rom_loaded = false,
|
||||
paused = true,
|
||||
step = false,
|
||||
cpu_hz = 700,
|
||||
disasm_follow = true,
|
||||
}
|
||||
sim.init(s)
|
||||
}
|
||||
|
||||
@export
|
||||
main_update :: proc "c" () -> bool {
|
||||
context = web_context
|
||||
|
||||
sim.update()
|
||||
return sim.should_run()
|
||||
}
|
||||
|
||||
@export
|
||||
main_end :: proc "c" () {
|
||||
context = web_context
|
||||
sim.shutdown()
|
||||
}
|
||||
|
||||
@export
|
||||
web_window_size_changed :: proc "c" (w: c.int, h: c.int) {
|
||||
context = web_context
|
||||
}
|
||||
+88
-34
@@ -3,28 +3,31 @@ package simulator
|
||||
import emu "../machine"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
// @ Window
|
||||
sim_run: bool
|
||||
|
||||
// Window
|
||||
WINDOW_WIDTH :: 1920
|
||||
WINDOW_HEIGHT :: 1080
|
||||
|
||||
// @ Layout proportions
|
||||
// Layout proportions
|
||||
// Sidebar takes 20% of screen width on each side; display takes the rest.
|
||||
// The center column splits vertically: 40% display, 60% debug panel.
|
||||
SIDEBAR_PERCENT :: f32(0.20)
|
||||
DISPLAY_V_RATIO :: f32(0.40)
|
||||
// @ Fixed heights
|
||||
// Fixed heights
|
||||
CONTROL_BAR_H :: f32(50)
|
||||
STATUS_BAR_H :: f32(30)
|
||||
// @ Layout constants
|
||||
// Layout constants
|
||||
PADDING_X :: f32(10)
|
||||
PADDING_Y :: f32(8)
|
||||
PANEL_HEADER :: f32(24)
|
||||
// @ Buttons
|
||||
// Buttons
|
||||
BUTTON_HEIGHT :: 30
|
||||
BUTTON_WIDTH :: 120
|
||||
// @ Fonts
|
||||
// Fonts
|
||||
BIG_FONT_SIZE :: 20
|
||||
KEYPAD_FONT_SIZE :: 18
|
||||
LINE_HEIGHT :: 22
|
||||
|
||||
Layout :: struct {
|
||||
control_bar : rl.Rectangle,
|
||||
@@ -34,29 +37,46 @@ 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) {
|
||||
init :: proc(sim: ^Simulator) {
|
||||
_sim = sim
|
||||
sim_run = true
|
||||
|
||||
// desktop
|
||||
when ODIN_OS != .JS {
|
||||
rl.SetConfigFlags({.WINDOW_RESIZABLE})
|
||||
rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "raylib")
|
||||
rl.InitAudioDevice()
|
||||
rl.SetTargetFPS(60)
|
||||
}
|
||||
|
||||
// web
|
||||
when ODIN_OS == .JS {
|
||||
// rl.SetConfigFlags({.VSYNC_HINT})
|
||||
}
|
||||
|
||||
rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Octal Cookie - Chip 8 Simulator")
|
||||
rl.InitAudioDevice()
|
||||
|
||||
// Load sound
|
||||
beep := rl.LoadSound("./assets/sounds/beep.wav")
|
||||
_sim.sound = beep
|
||||
|
||||
// Load fonts
|
||||
font := rl.LoadFontEx("./assets/fonts/Inter_18pt-Regular.ttf", 18, nil, 0)
|
||||
rl.SetTextureFilter(font.texture, .BILINEAR)
|
||||
sim.font = font
|
||||
_sim.font = font
|
||||
|
||||
rl.GuiLoadStyleDefault()
|
||||
rl.GuiLoadStyle("./assets/raygui_styles/style_dark.rgs")
|
||||
rl.GuiLoadStyle("./assets/raygui_styles/genesis.rgs")
|
||||
|
||||
rl.GuiSetFont(font)
|
||||
rl.GuiSetStyle(.DEFAULT, i32(rl.GuiDefaultProperty.TEXT_SIZE), 18)
|
||||
}
|
||||
|
||||
update :: proc() {
|
||||
sim := _sim
|
||||
|
||||
// Draw each of the components in its own window within the main window
|
||||
for !rl.WindowShouldClose() {
|
||||
// Recalculate layout each frame based on current window size
|
||||
// Pass these down to gui functions so they can setup their sizes?
|
||||
screen_width := f32(rl.GetScreenWidth())
|
||||
@@ -67,23 +87,20 @@ run_gui :: proc(sim: ^Simulator) {
|
||||
layout := calc_layout(screen_width, screen_height)
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(rl.BLACK)
|
||||
rl.ClearBackground(rl.Color{0x18, 0x18, 0x18, 0xFF})
|
||||
|
||||
if (!sim.paused) {
|
||||
cycles := int(sim.cpu_hz / SIM_FPS)
|
||||
// Cycle the machine to update memory etc
|
||||
emu.run_machine(sim.machine, 12)
|
||||
|
||||
// Handle delay timer
|
||||
if sim.machine.delay_timer > 0 do sim.machine.delay_timer -= 1
|
||||
|
||||
// Current sound file is around 1s so stop
|
||||
// immediately when timer is 0
|
||||
if sim.machine.sound_timer > 0 {
|
||||
sim.machine.sound_timer -= 1
|
||||
if !rl.IsSoundPlaying(beep) do rl.PlaySound(beep)
|
||||
} else {
|
||||
rl.StopSound(beep)
|
||||
emu.run_machine(sim.machine, cycles)
|
||||
tick_timers(sim)
|
||||
}
|
||||
|
||||
if(sim.paused && sim.step) {
|
||||
// Cycle the machine to update memory etc
|
||||
emu.run_machine(sim.machine, 1)
|
||||
tick_timers(sim)
|
||||
sim.step = false
|
||||
}
|
||||
|
||||
// Top
|
||||
@@ -105,19 +122,46 @@ 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)
|
||||
|
||||
rl.EndDrawing()
|
||||
}
|
||||
// Info Box
|
||||
// ------------------------------------------
|
||||
gui_info_box(layout.info_box, sim, screen_width, screen_height)
|
||||
|
||||
rl.EndDrawing()
|
||||
free_all(context.temp_allocator)
|
||||
}
|
||||
|
||||
shutdown :: proc() {
|
||||
sim := _sim
|
||||
rl.UnloadFont(sim.font)
|
||||
rl.UnloadSound(beep)
|
||||
rl.UnloadSound(sim.sound)
|
||||
rl.CloseAudioDevice()
|
||||
rl.CloseWindow()
|
||||
}
|
||||
|
||||
// @TODO: If this grows lets move it into its own file
|
||||
should_run :: proc() -> bool {
|
||||
when ODIN_OS != .JS {
|
||||
return !rl.WindowShouldClose()
|
||||
}
|
||||
// web loop is controlled by JS requestAnimationFrame
|
||||
return true
|
||||
}
|
||||
|
||||
tick_timers :: proc(sim: ^Simulator) {
|
||||
beep := sim.sound
|
||||
|
||||
if sim.machine.delay_timer > 0 do sim.machine.delay_timer -= 1
|
||||
if sim.machine.sound_timer > 0 {
|
||||
sim.machine.sound_timer -= 1
|
||||
if !rl.IsSoundPlaying(beep) do rl.PlaySound(beep)
|
||||
} else {
|
||||
// only stop if actually playing
|
||||
if rl.IsSoundPlaying(beep) do rl.StopSound(beep)
|
||||
}
|
||||
}
|
||||
|
||||
calc_layout :: proc(screen_width: f32, screen_height: f32) -> Layout {
|
||||
// Control bar is a fixed height frozen at top of gui, all items start below it.
|
||||
y_pos := CONTROL_BAR_H
|
||||
@@ -128,6 +172,7 @@ calc_layout :: proc(screen_width: f32, screen_height: f32) -> Layout {
|
||||
sidebar_width := screen_width * SIDEBAR_PERCENT
|
||||
display_width := screen_width - (sidebar_width * 2) - sidebar_width
|
||||
display_height := visible_height * DISPLAY_V_RATIO
|
||||
keypad_height := (y_pos + visible_height - visible_height / 2)
|
||||
|
||||
x_center := sidebar_width
|
||||
y_center := screen_width - sidebar_width * 2
|
||||
@@ -144,11 +189,11 @@ calc_layout :: proc(screen_width: f32, screen_height: f32) -> Layout {
|
||||
x = 0,
|
||||
y = y_pos,
|
||||
width = sidebar_width,
|
||||
height = visible_height / 3
|
||||
height = keypad_height - y_pos
|
||||
},
|
||||
keypad = rl.Rectangle{
|
||||
x = 0,
|
||||
y = (y_pos + visible_height - visible_height / 2),
|
||||
y = keypad_height,
|
||||
width = sidebar_width,
|
||||
height = visible_height / 2
|
||||
},
|
||||
@@ -188,5 +233,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,33 +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", "Disassembly", "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)
|
||||
|
||||
bounds := rl.Rectangle {
|
||||
memory_view_bounds := rl.Rectangle {
|
||||
x = rect.x + PADDING_X,
|
||||
y = rect.y + PADDING_Y + tab_bar_rect.height,
|
||||
width = rect.width - (PADDING_X * 2),
|
||||
height = rect.height - (PADDING_Y * 2) - tab_bar_rect.height,
|
||||
y = rect.y + PADDING_Y,
|
||||
width = (rect.width - (PADDING_X * 2)) / 2,
|
||||
height = rect.height - (PADDING_Y * 2),
|
||||
}
|
||||
gui_tab_memory(memory_view_bounds, sim)
|
||||
|
||||
switch sim.active_tab {
|
||||
case 0: // draw registers panel
|
||||
gui_tab_memory(bounds, sim)
|
||||
case 1: // draw memory panel
|
||||
case 2: // draw display panel
|
||||
disasm_view_bounds := rl.Rectangle {
|
||||
x = rect.x + memory_view_bounds.width + PADDING_X,
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package simulator
|
||||
|
||||
import "core:log"
|
||||
import "core:fmt"
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
gui_control_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
@@ -15,19 +16,57 @@ gui_control_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
if btn(&cursor, rect, BUTTON_HEIGHT, BUTTON_WIDTH, PADDING_X, "RUN") {
|
||||
if sim.rom_loaded {
|
||||
sim.paused = false
|
||||
sim.running = true
|
||||
} else {
|
||||
log.info("no rom selected, can't run")
|
||||
// log.info("no rom selected, can't run")
|
||||
}
|
||||
}
|
||||
|
||||
if btn(&cursor, rect, BUTTON_HEIGHT, BUTTON_WIDTH, PADDING_X, "PAUSE") {
|
||||
sim.paused = true
|
||||
sim.running = false
|
||||
}
|
||||
|
||||
if btn(&cursor, rect, BUTTON_HEIGHT, BUTTON_WIDTH, PADDING_X, "STEP") {
|
||||
if !sim.step do sim.step = true
|
||||
}
|
||||
|
||||
if btn(&cursor, rect, BUTTON_HEIGHT, BUTTON_WIDTH, PADDING_X, "RESET") {
|
||||
reset_sim(sim)
|
||||
}
|
||||
|
||||
slider_rect := rl.Rectangle{
|
||||
x = rect.width - 210,
|
||||
y = rect.y + PADDING_X + 5,
|
||||
width = 100,
|
||||
height = 15
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#+build !js
|
||||
package simulator
|
||||
|
||||
import "core:log"
|
||||
import "core:strings"
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
import emu "../machine"
|
||||
@@ -9,34 +11,21 @@ import tfd "../../external/tinyfiledialogs"
|
||||
gui_file_loader :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
rl.DrawRectangleLinesEx(rect, 1, rl.GRAY)
|
||||
|
||||
// FILE SECTION:
|
||||
// ---------------------------------------------
|
||||
top_bounds_height := (rect.height - (PADDING_Y * 2)) / 3
|
||||
bounds := rl.Rectangle {
|
||||
x = rect.x + PADDING_X,
|
||||
y = rect.y + PADDING_Y,
|
||||
width = rect.width - (PADDING_X * 2),
|
||||
height = rect.height - (PADDING_Y * 2),
|
||||
height = top_bounds_height
|
||||
}
|
||||
rl.GuiPanel(bounds, "Rom / File")
|
||||
rl.GuiPanel(bounds, nil)
|
||||
|
||||
// drop-zone occupies the panel's content area, minus space for the button
|
||||
drop_zone := rl.Rectangle {
|
||||
bounds.x + PADDING_X,
|
||||
bounds.y + PADDING_Y,
|
||||
bounds.width - (PADDING_X * 2),
|
||||
bounds.height - ((PADDING_Y * 2) + (BUTTON_HEIGHT + PADDING_Y))
|
||||
}
|
||||
|
||||
// centered drop-zone text
|
||||
text: cstring = "Drop a CHIP-8 ROM here"
|
||||
text_width := rl.MeasureText(text, BIG_FONT_SIZE)
|
||||
text_x := drop_zone.x + (drop_zone.width - f32(text_width)) / 2
|
||||
text_y := drop_zone.y + (drop_zone.height - f32(BIG_FONT_SIZE)) / 2
|
||||
rl.DrawTextEx(sim.font, text, {text_x, text_y}, BIG_FONT_SIZE, 1, rl.WHITE)
|
||||
|
||||
// open rom button below drop-zone
|
||||
btn_rect := rl.Rectangle {
|
||||
drop_zone.x,
|
||||
drop_zone.y + drop_zone.height + PADDING_Y,
|
||||
drop_zone.width,
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
rect.width - (PADDING_X * 2),
|
||||
BUTTON_HEIGHT,
|
||||
}
|
||||
|
||||
@@ -44,19 +33,24 @@ gui_file_loader :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
ret := tfd.openFileDialog("Open File Dialog", nil, 0, nil, nil, 0,)
|
||||
rom_path := string(ret)
|
||||
if rom_path == "" do return
|
||||
|
||||
// reset machine state
|
||||
emu.reset_machine(sim.machine)
|
||||
|
||||
// load new rom
|
||||
err := emu.load_rom(sim.machine, rom_path)
|
||||
if err != nil {
|
||||
// @TODO: update status bar here
|
||||
panic("failed to load rom!")
|
||||
load_selected_rom(sim, rom_path)
|
||||
}
|
||||
|
||||
sim.rom_loaded = true
|
||||
// drop-zone occupies the panel's content area, minus space for the button
|
||||
drop_zone := rl.Rectangle {
|
||||
bounds.x + PADDING_X,
|
||||
bounds.y + btn_rect.height + PADDING_Y * 2,
|
||||
bounds.width - (PADDING_X * 2),
|
||||
bounds.height - (btn_rect.height) - (PADDING_Y * 4)
|
||||
}
|
||||
rl.DrawRectangleLinesEx(drop_zone, 1, rl.LIGHTGRAY)
|
||||
|
||||
// centered drop-zone text
|
||||
text: cstring = "Drop a CHIP-8 ROM here"
|
||||
text_width := rl.MeasureText(text, BIG_FONT_SIZE)
|
||||
text_x := drop_zone.x + (drop_zone.width - f32(text_width))
|
||||
text_y := drop_zone.y + (drop_zone.height - f32(BIG_FONT_SIZE)) / 2
|
||||
rl.DrawTextEx(sim.font, text, {text_x, text_y}, BIG_FONT_SIZE, 1, rl.WHITE)
|
||||
|
||||
// Handle file drop
|
||||
if rl.IsFileDropped() {
|
||||
@@ -65,13 +59,187 @@ gui_file_loader :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
mouse := rl.GetMousePosition()
|
||||
|
||||
if rl.CheckCollisionPointRec(mouse, drop_zone) {
|
||||
path_str := string(dropped_file.paths[0])
|
||||
log.info("file dropped: ", path_str)
|
||||
// @TODO: Stop sim, reset mem etc, load new rom
|
||||
// path_str := string(dropped_file.paths[0])
|
||||
// log.info("file dropped: ", path_str)
|
||||
} else {
|
||||
log.info("File dropped outside drop zone, ignoring")
|
||||
// log.info("File dropped outside drop zone, ignoring")
|
||||
}
|
||||
}
|
||||
rl.UnloadDroppedFiles(dropped_file)
|
||||
}
|
||||
|
||||
// Embedded Rom Section:
|
||||
// ---------------------------------------------
|
||||
bottom_bounds := rl.Rectangle {
|
||||
x = bounds.x,
|
||||
y = bounds.y + top_bounds_height,
|
||||
width = rect.width - (PADDING_X * 2),
|
||||
height = rect.height - top_bounds_height - (PADDING_X * 2)
|
||||
}
|
||||
rl.GuiPanel(bottom_bounds, "ROM List")
|
||||
|
||||
panel_rect := rl.Rectangle {
|
||||
x = bottom_bounds.x,
|
||||
y = bottom_bounds.y + PANEL_HEADER,
|
||||
width = rect.width - (PADDING_X * 2),
|
||||
height = rect.height - top_bounds_height - (PADDING_X * 2) - PANEL_HEADER
|
||||
}
|
||||
|
||||
content_rect := rl.Rectangle {
|
||||
x = panel_rect.x,
|
||||
y = panel_rect.y - PANEL_HEADER,
|
||||
width = panel_rect.width - 15,
|
||||
height = f32(len(roms)) * LINE_HEIGHT
|
||||
}
|
||||
|
||||
view: rl.Rectangle
|
||||
rl.GuiScrollPanel(panel_rect, nil, content_rect, &sim.rompick_scroll, &view)
|
||||
|
||||
rl.BeginScissorMode(i32(view.x), i32(view.y), i32(view.width), i32(view.height))
|
||||
defer rl.EndScissorMode()
|
||||
|
||||
for path, index in roms {
|
||||
y_pos := panel_rect.y + f32(index * LINE_HEIGHT) + sim.rompick_scroll.y
|
||||
|
||||
row_rect := rl.Rectangle {
|
||||
x = view.x,
|
||||
y = y_pos,
|
||||
width = view.width,
|
||||
height = LINE_HEIGHT,
|
||||
}
|
||||
|
||||
// make selected visible
|
||||
if path.name == sim.selected_rom {
|
||||
rl.DrawRectangleRec(row_rect, rl.ColorAlpha(rl.SKYBLUE, 0.25))
|
||||
}
|
||||
|
||||
// hover rows
|
||||
mouse := rl.GetMousePosition()
|
||||
if rl.CheckCollisionPointRec(mouse, row_rect) {
|
||||
rl.DrawRectangleRec(row_rect, rl.ColorAlpha(rl.WHITE, 0.08))
|
||||
if rl.IsMouseButtonPressed(.LEFT) {
|
||||
sim.selected_rom = path.name
|
||||
load_embedded_rom(sim, path.name, roms[index].data)
|
||||
}
|
||||
}
|
||||
|
||||
txt := fmt.tprintf("%v: %v", (index + 1), path.name)
|
||||
rl.DrawTextEx(
|
||||
sim.font,
|
||||
strings.clone_to_cstring(txt, context.temp_allocator),
|
||||
{panel_rect.x + PADDING_X + sim.rompick_scroll.x, y_pos + (LINE_HEIGHT - 18) * 0.5},
|
||||
18, 1,
|
||||
rl.WHITE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
load_selected_rom :: proc(sim: ^Simulator, rom_path: string) {
|
||||
reset_sim(sim)
|
||||
rom_size, err := emu.load_rom(sim.machine, rom_path)
|
||||
if err != nil {
|
||||
// @TODO: update status bar here
|
||||
panic("failed to load rom!")
|
||||
}
|
||||
|
||||
sim.rom_loaded = true
|
||||
sim.disasm_count = disassemble_rom_n(sim.machine.memory[:], 0x200, 0x200 + u16(rom_size), sim.disasm[:])
|
||||
}
|
||||
|
||||
load_embedded_rom :: proc(sim: ^Simulator, path: string, data: []byte) {
|
||||
reset_sim(sim)
|
||||
|
||||
copy(sim.machine.memory[0x200:], data)
|
||||
sim.rom_loaded = true
|
||||
sim.disasm_count = disassemble_rom_n(sim.machine.memory[:], 0x200, 0x200 + u16(len(data)), sim.disasm[:])
|
||||
}
|
||||
|
||||
disassemble_rom_n :: proc(memory: []u8, start_addr: u16, end_addr: u16, out: []Instruction) -> int {
|
||||
count := 0
|
||||
i := start_addr
|
||||
|
||||
for i < end_addr {
|
||||
if int(i) + 1 >= len(memory) do break
|
||||
if count >= len(out) do break
|
||||
|
||||
addr := i
|
||||
high_byte := memory[i]
|
||||
low_byte := memory[i + 1]
|
||||
i += 2
|
||||
|
||||
// Get full opcode from the 2 bytes
|
||||
opcode := (u16(high_byte) << 8) | u16(low_byte)
|
||||
|
||||
// Pre-decode common components of opcodes
|
||||
first_nibble := (opcode & 0xF000) >> 12
|
||||
vx := (opcode & 0x0F00) >> 8 // A 4-bit value, the lower 4 bits of the high byte of the instruction
|
||||
vy := (opcode & 0x00F0) >> 4 // A 4-bit value, the upper 4 bits of the low byte of the instruction
|
||||
last_nibble := (opcode & 0x000F) // A 4-bit value, the lowest 4 bits of the instruction
|
||||
kk := u8(opcode & 0x00FF) // An 8-bit value, the lowest 8 bits of the instruction
|
||||
nnn := (opcode & 0x0FFF) // A 12-bit value, the lowest 12 bits of the instruction
|
||||
|
||||
|
||||
instruction := &out[count]
|
||||
instruction.address = addr
|
||||
instruction.raw = opcode
|
||||
|
||||
switch first_nibble {
|
||||
case 0x0:
|
||||
switch opcode {
|
||||
//case 0x00E0: str = "CLS"
|
||||
case 0x00E0: fmt.bprintf(instruction.str[:], "CLS")
|
||||
case 0x00EE: fmt.bprintf(instruction.str[:], "RET")
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
case 0x1: fmt.bprintf(instruction.str[:], "JP 0x%03X", nnn)
|
||||
case 0x2: fmt.bprintf(instruction.str[:], "CALL 0x%03X", nnn)
|
||||
case 0x3: fmt.bprintf(instruction.str[:], "SE V%X, 0x%02X", vx, kk)
|
||||
case 0x4: fmt.bprintf(instruction.str[:], "SNE V%X, 0x%02X", vx, kk)
|
||||
case 0x5: fmt.bprintf(instruction.str[:], "SE V%X, V%X", vx, vy)
|
||||
case 0x6: fmt.bprintf(instruction.str[:], "LD V%X, 0x%02X", vx, kk)
|
||||
case 0x7: fmt.bprintf(instruction.str[:], "ADD V%X, 0x%02X", vx, kk)
|
||||
case 0x8:
|
||||
switch last_nibble {
|
||||
case 0x0: fmt.bprintf(instruction.str[:], "LD V%X, V%X", vx, vy)
|
||||
case 0x1: fmt.bprintf(instruction.str[:], "OR V%X, V%X", vx, vy)
|
||||
case 0x2: fmt.bprintf(instruction.str[:], "AND V%X, V%X", vx, vy)
|
||||
case 0x3: fmt.bprintf(instruction.str[:], "XOR V%X, V%X", vx, vy)
|
||||
case 0x4: fmt.bprintf(instruction.str[:], "ADD V%X, V%X", vx, vy)
|
||||
case 0x5: fmt.bprintf(instruction.str[:], "SUB V%X, V%X", vx, vy)
|
||||
case 0x6: fmt.bprintf(instruction.str[:], "SHR V%X", vx)
|
||||
case 0x7: fmt.bprintf(instruction.str[:], "SUBN V%X, V%X", vx, vy)
|
||||
case 0xE: fmt.bprintf(instruction.str[:], "SHL V%X", vx)
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
case 0x9: fmt.bprintf(instruction.str[:], "SNE V%X, V%X", vx, vy)
|
||||
case 0xA: fmt.bprintf(instruction.str[:], "LD I, 0x%03X", nnn)
|
||||
case 0xB: fmt.bprintf(instruction.str[:], "JP V0, 0x%03X", nnn)
|
||||
case 0xC: fmt.bprintf(instruction.str[:], "RND V%X, 0x%02X", vx, kk)
|
||||
case 0xD: fmt.bprintf(instruction.str[:], "DRW V%X, V%X, %X", vx, vy, last_nibble)
|
||||
case 0xE:
|
||||
switch kk {
|
||||
case 0x9E: fmt.bprintf(instruction.str[:], "SKP V%X", vx)
|
||||
case 0xA1: fmt.bprintf(instruction.str[:], "SKNP V%X", vx)
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
case 0xF:
|
||||
switch kk {
|
||||
case 0x07: fmt.bprintf(instruction.str[:], "LD V%X, DT", vx)
|
||||
case 0x0A: fmt.bprintf(instruction.str[:], "LD V%X, K", vx)
|
||||
case 0x15: fmt.bprintf(instruction.str[:], "LD DT, V%X", vx)
|
||||
case 0x18: fmt.bprintf(instruction.str[:], "LD ST, V%X", vx)
|
||||
case 0x1E: fmt.bprintf(instruction.str[:], "ADD I, V%X", vx)
|
||||
case 0x29: fmt.bprintf(instruction.str[:], "LD F, V%X", vx)
|
||||
case 0x33: fmt.bprintf(instruction.str[:], "LD B, V%X", vx)
|
||||
case 0x55: fmt.bprintf(instruction.str[:], "LD [I], V%X", vx)
|
||||
case 0x65: fmt.bprintf(instruction.str[:], "LD V%X, [I]", vx)
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
|
||||
count += 1
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#+build js
|
||||
package simulator
|
||||
|
||||
import "core:strings"
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
import emu "../machine"
|
||||
|
||||
gui_file_loader :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
rl.DrawRectangleLinesEx(rect, 1, rl.GRAY)
|
||||
|
||||
// Embedded Rom Section:
|
||||
// ---------------------------------------------
|
||||
bottom_bounds := rl.Rectangle {
|
||||
x = rect.x,
|
||||
y = rect.y,
|
||||
width = rect.width - (PADDING_X * 2),
|
||||
height = rect.height - (PADDING_X * 2)
|
||||
}
|
||||
rl.GuiPanel(bottom_bounds, "ROM List")
|
||||
|
||||
panel_rect := rl.Rectangle {
|
||||
x = bottom_bounds.x,
|
||||
y = bottom_bounds.y + PANEL_HEADER,
|
||||
width = rect.width - (PADDING_X * 2),
|
||||
height = rect.height - (PADDING_X * 2) - PANEL_HEADER
|
||||
}
|
||||
|
||||
content_rect := rl.Rectangle {
|
||||
x = panel_rect.x,
|
||||
y = panel_rect.y - PANEL_HEADER,
|
||||
width = panel_rect.width - 15,
|
||||
height = f32(len(roms)) * LINE_HEIGHT
|
||||
}
|
||||
|
||||
view: rl.Rectangle
|
||||
rl.GuiScrollPanel(panel_rect, nil, content_rect, &sim.rompick_scroll, &view)
|
||||
|
||||
rl.BeginScissorMode(i32(view.x), i32(view.y), i32(view.width), i32(view.height))
|
||||
defer rl.EndScissorMode()
|
||||
|
||||
for path, index in roms {
|
||||
y_pos := panel_rect.y + f32(index * LINE_HEIGHT) + sim.rompick_scroll.y
|
||||
|
||||
row_rect := rl.Rectangle {
|
||||
x = view.x,
|
||||
y = y_pos,
|
||||
width = view.width,
|
||||
height = LINE_HEIGHT,
|
||||
}
|
||||
|
||||
// make selected visible
|
||||
if path.name == sim.selected_rom {
|
||||
rl.DrawRectangleRec(row_rect, rl.ColorAlpha(rl.SKYBLUE, 0.25))
|
||||
}
|
||||
|
||||
// hover rows
|
||||
mouse := rl.GetMousePosition()
|
||||
if rl.CheckCollisionPointRec(mouse, row_rect) {
|
||||
rl.DrawRectangleRec(row_rect, rl.ColorAlpha(rl.WHITE, 0.08))
|
||||
if rl.IsMouseButtonPressed(.LEFT) {
|
||||
sim.selected_rom = path.name
|
||||
load_embedded_rom(sim, path.name, roms[index].data)
|
||||
}
|
||||
}
|
||||
|
||||
txt := fmt.tprintf("%v: %v", (index + 1), path.name)
|
||||
rl.DrawTextEx(
|
||||
sim.font,
|
||||
strings.clone_to_cstring(txt, context.temp_allocator),
|
||||
{panel_rect.x + PADDING_X + sim.rompick_scroll.x, y_pos + (LINE_HEIGHT - 18) * 0.5},
|
||||
18, 1,
|
||||
rl.WHITE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
load_embedded_rom :: proc(sim: ^Simulator, path: string, data: []byte) {
|
||||
reset_sim(sim)
|
||||
copy(sim.machine.memory[0x200:], data)
|
||||
sim.rom_loaded = true
|
||||
sim.disasm_count = disassemble_rom_n(sim.machine.memory[:], 0x200, 0x200 + u16(len(data)), sim.disasm[:])
|
||||
}
|
||||
|
||||
disassemble_rom_n :: proc(memory: []u8, start_addr: u16, end_addr: u16, out: []Instruction) -> int {
|
||||
count := 0
|
||||
i := start_addr
|
||||
|
||||
for i < end_addr {
|
||||
if int(i) + 1 >= len(memory) do break
|
||||
if count >= len(out) do break
|
||||
|
||||
addr := i
|
||||
high_byte := memory[i]
|
||||
low_byte := memory[i + 1]
|
||||
i += 2
|
||||
|
||||
// Get full opcode from the 2 bytes
|
||||
opcode := (u16(high_byte) << 8) | u16(low_byte)
|
||||
|
||||
// Pre-decode common components of opcodes
|
||||
first_nibble := (opcode & 0xF000) >> 12
|
||||
vx := (opcode & 0x0F00) >> 8 // A 4-bit value, the lower 4 bits of the high byte of the instruction
|
||||
vy := (opcode & 0x00F0) >> 4 // A 4-bit value, the upper 4 bits of the low byte of the instruction
|
||||
last_nibble := (opcode & 0x000F) // A 4-bit value, the lowest 4 bits of the instruction
|
||||
kk := u8(opcode & 0x00FF) // An 8-bit value, the lowest 8 bits of the instruction
|
||||
nnn := (opcode & 0x0FFF) // A 12-bit value, the lowest 12 bits of the instruction
|
||||
|
||||
|
||||
instruction := &out[count]
|
||||
instruction.address = addr
|
||||
instruction.raw = opcode
|
||||
|
||||
switch first_nibble {
|
||||
case 0x0:
|
||||
switch opcode {
|
||||
//case 0x00E0: str = "CLS"
|
||||
case 0x00E0: fmt.bprintf(instruction.str[:], "CLS")
|
||||
case 0x00EE: fmt.bprintf(instruction.str[:], "RET")
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
case 0x1: fmt.bprintf(instruction.str[:], "JP 0x%03X", nnn)
|
||||
case 0x2: fmt.bprintf(instruction.str[:], "CALL 0x%03X", nnn)
|
||||
case 0x3: fmt.bprintf(instruction.str[:], "SE V%X, 0x%02X", vx, kk)
|
||||
case 0x4: fmt.bprintf(instruction.str[:], "SNE V%X, 0x%02X", vx, kk)
|
||||
case 0x5: fmt.bprintf(instruction.str[:], "SE V%X, V%X", vx, vy)
|
||||
case 0x6: fmt.bprintf(instruction.str[:], "LD V%X, 0x%02X", vx, kk)
|
||||
case 0x7: fmt.bprintf(instruction.str[:], "ADD V%X, 0x%02X", vx, kk)
|
||||
case 0x8:
|
||||
switch last_nibble {
|
||||
case 0x0: fmt.bprintf(instruction.str[:], "LD V%X, V%X", vx, vy)
|
||||
case 0x1: fmt.bprintf(instruction.str[:], "OR V%X, V%X", vx, vy)
|
||||
case 0x2: fmt.bprintf(instruction.str[:], "AND V%X, V%X", vx, vy)
|
||||
case 0x3: fmt.bprintf(instruction.str[:], "XOR V%X, V%X", vx, vy)
|
||||
case 0x4: fmt.bprintf(instruction.str[:], "ADD V%X, V%X", vx, vy)
|
||||
case 0x5: fmt.bprintf(instruction.str[:], "SUB V%X, V%X", vx, vy)
|
||||
case 0x6: fmt.bprintf(instruction.str[:], "SHR V%X", vx)
|
||||
case 0x7: fmt.bprintf(instruction.str[:], "SUBN V%X, V%X", vx, vy)
|
||||
case 0xE: fmt.bprintf(instruction.str[:], "SHL V%X", vx)
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
case 0x9: fmt.bprintf(instruction.str[:], "SNE V%X, V%X", vx, vy)
|
||||
case 0xA: fmt.bprintf(instruction.str[:], "LD I, 0x%03X", nnn)
|
||||
case 0xB: fmt.bprintf(instruction.str[:], "JP V0, 0x%03X", nnn)
|
||||
case 0xC: fmt.bprintf(instruction.str[:], "RND V%X, 0x%02X", vx, kk)
|
||||
case 0xD: fmt.bprintf(instruction.str[:], "DRW V%X, V%X, %X", vx, vy, last_nibble)
|
||||
case 0xE:
|
||||
switch kk {
|
||||
case 0x9E: fmt.bprintf(instruction.str[:], "SKP V%X", vx)
|
||||
case 0xA1: fmt.bprintf(instruction.str[:], "SKNP V%X", vx)
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
case 0xF:
|
||||
switch kk {
|
||||
case 0x07: fmt.bprintf(instruction.str[:], "LD V%X, DT", vx)
|
||||
case 0x0A: fmt.bprintf(instruction.str[:], "LD V%X, K", vx)
|
||||
case 0x15: fmt.bprintf(instruction.str[:], "LD DT, V%X", vx)
|
||||
case 0x18: fmt.bprintf(instruction.str[:], "LD ST, V%X", vx)
|
||||
case 0x1E: fmt.bprintf(instruction.str[:], "ADD I, V%X", vx)
|
||||
case 0x29: fmt.bprintf(instruction.str[:], "LD F, V%X", vx)
|
||||
case 0x33: fmt.bprintf(instruction.str[:], "LD B, V%X", vx)
|
||||
case 0x55: fmt.bprintf(instruction.str[:], "LD [I], V%X", vx)
|
||||
case 0x65: fmt.bprintf(instruction.str[:], "LD V%X, [I]", vx)
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
case: fmt.bprintf(instruction.str[:], "DATA 0x%04X", opcode)
|
||||
}
|
||||
|
||||
count += 1
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import rl "vendor:raylib"
|
||||
|
||||
StatusIconShape :: enum { CIRCLE, SQUARE }
|
||||
|
||||
// @TODO: render status bar text
|
||||
gui_status_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
// Left to right text draws
|
||||
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
|
||||
@@ -13,7 +12,7 @@ gui_status_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
cursor: f32 = rect.x + PADDING_X
|
||||
cy := rect.y + rect.height * 0.5
|
||||
|
||||
if sim.running && !sim.paused {
|
||||
if !sim.paused {
|
||||
status_icon(&cursor, cy, rl.GREEN, .CIRCLE, "Running", sim.font)
|
||||
} else {
|
||||
status_icon(&cursor, cy, rl.RED, .SQUARE, "Paused", sim.font)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package simulator
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
gui_tab_disasm :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
|
||||
// Header background
|
||||
rl.DrawRectangle(i32(rect.x), i32(rect.y), i32(rect.width), i32(PANEL_HEADER), rl.BLACK)
|
||||
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_rect := rl.Rectangle{rect.x + rect.width - 110, rect.y + 4, 100, 20}
|
||||
if rl.GuiButton(follow_rect, follow_label) {
|
||||
sim.disasm_follow = !sim.disasm_follow
|
||||
}
|
||||
|
||||
// Scroll panel area (below header)
|
||||
panel_rect := rl.Rectangle{
|
||||
rect.x,
|
||||
rect.y + PANEL_HEADER,
|
||||
rect.width,
|
||||
rect.height - PANEL_HEADER,
|
||||
}
|
||||
rl.GuiPanel(panel_rect, nil)
|
||||
|
||||
content_rect := rl.Rectangle {
|
||||
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
|
||||
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
|
||||
is_active := entry.address == sim.machine.pc
|
||||
color := rl.WHITE
|
||||
|
||||
if is_active {
|
||||
rl.DrawRectangleV(
|
||||
{panel_rect.x + sim.disasm_scroll.x, y_pos},
|
||||
{panel_rect.width, LINE_HEIGHT},
|
||||
rl.BLACK,
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
visible_height := panel_rect.height
|
||||
sim.disasm_scroll.y = -(target_y - visible_height / 2)
|
||||
max_scroll := -(f32(sim.disasm_count) * LINE_HEIGHT - visible_height)
|
||||
sim.disasm_scroll.y = clamp(sim.disasm_scroll.y, max_scroll, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import rl "vendor:raylib"
|
||||
MEM_INDICATOR_W :: 14
|
||||
MEM_ADDRESS_W :: 72
|
||||
MEM_ROW_H :: 20
|
||||
MEM_HEADER_H :: 24
|
||||
MEM_BYTES_PER_ROW :: 16
|
||||
MEM_TOTAL_ROWS :: 256
|
||||
MEM_FONT_ROWS :: 5
|
||||
@@ -25,37 +24,37 @@ gui_tab_memory :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
rl.DrawRectangleRec(rect, rl.DARKGRAY)
|
||||
|
||||
// Header background
|
||||
rl.DrawRectangle(i32(rect.x), i32(rect.y), i32(rect.width), MEM_HEADER_H, 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, "Address", {rect.x + MEM_INDICATOR_W, rect.y + 4}, 16, 1, rl.GRAY)
|
||||
rl.DrawTextEx(sim.font, "Address", {rect.x + MEM_INDICATOR_W, rect.y + 4}, 18, 1, rl.WHITE)
|
||||
|
||||
// Header: column labels 0-F
|
||||
col_w := byte_col_w(rect)
|
||||
for col in 0..<16 {
|
||||
x := rect.x + MEM_INDICATOR_W + MEM_ADDRESS_W + f32(col) * col_w
|
||||
label := strings.clone_to_cstring(MEM_COL_LABELS[col], context.temp_allocator)
|
||||
rl.DrawTextEx(sim.font, label, {x, rect.y + 4}, 16, 1, rl.GRAY)
|
||||
rl.DrawTextEx(sim.font, label, {x, rect.y + 4}, 18, 1, rl.WHITE)
|
||||
}
|
||||
|
||||
// Header separator
|
||||
rl.DrawLine(
|
||||
i32(rect.x), i32(rect.y + MEM_HEADER_H),
|
||||
i32(rect.x + rect.width), i32(rect.y + MEM_HEADER_H),
|
||||
i32(rect.x), i32(rect.y + PANEL_HEADER),
|
||||
i32(rect.x + rect.width), i32(rect.y + PANEL_HEADER),
|
||||
rl.GRAY,
|
||||
)
|
||||
|
||||
// Scroll panel area (below header)
|
||||
panel_rect := rl.Rectangle{
|
||||
rect.x,
|
||||
rect.y + MEM_HEADER_H,
|
||||
rect.y + PANEL_HEADER,
|
||||
rect.width,
|
||||
rect.height - MEM_HEADER_H,
|
||||
rect.height - PANEL_HEADER,
|
||||
}
|
||||
|
||||
content_rect := rl.Rectangle{
|
||||
0, 0,
|
||||
rect.width - 12,
|
||||
rect.width - 15,
|
||||
f32(MEM_VIRTUAL_ROWS) * MEM_ROW_H,
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ gui_tab_memory :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
|
||||
draw_row := 0
|
||||
|
||||
// --- Font rows (0x000 - 0x04F) ---
|
||||
// Font rows (0x000 - 0x04F)
|
||||
for row in 0..<MEM_FONT_ROWS {
|
||||
addr := row * MEM_BYTES_PER_ROW
|
||||
row_y := panel_rect.y + f32(draw_row) * MEM_ROW_H + sim.mem_scroll.y
|
||||
@@ -74,7 +73,6 @@ gui_tab_memory :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
draw_row += 1
|
||||
}
|
||||
|
||||
// --- Divider ---
|
||||
divider_y := panel_rect.y + f32(draw_row) * MEM_ROW_H + sim.mem_scroll.y
|
||||
rl.DrawRectangle(i32(rect.x), i32(divider_y), i32(rect.width), MEM_ROW_H, rl.ColorAlpha(rl.BLACK, 0.4))
|
||||
rl.DrawTextEx(
|
||||
@@ -85,7 +83,7 @@ gui_tab_memory :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||
)
|
||||
draw_row += 1
|
||||
|
||||
// --- ROM rows (0x200 onwards) ---
|
||||
// ROM rows (0x200 onwards)
|
||||
for i := 0; MEM_ROM_START + i * MEM_BYTES_PER_ROW < 4096; i += 1 {
|
||||
addr := MEM_ROM_START + i * MEM_BYTES_PER_ROW
|
||||
row_y := panel_rect.y + f32(draw_row) * MEM_ROW_H + sim.mem_scroll.y
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -3,28 +3,55 @@ package simulator
|
||||
import emu "../machine"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
@(private="package")
|
||||
_sim: ^Simulator
|
||||
|
||||
// Embed roms
|
||||
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,
|
||||
raw: u16,
|
||||
str: [32]u8
|
||||
}
|
||||
|
||||
Simulator :: struct {
|
||||
// Emulator
|
||||
machine: ^emu.System,
|
||||
rom_loaded: bool,
|
||||
running: bool,
|
||||
paused: bool,
|
||||
cycles_per_second: int,
|
||||
step: bool,
|
||||
cpu_hz: f32,
|
||||
info_box: bool,
|
||||
// GUI
|
||||
sound: rl.Sound,
|
||||
font: rl.Font,
|
||||
active_tab: i32,
|
||||
mem_scroll : rl.Vector2,
|
||||
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,
|
||||
disasm_count: int,
|
||||
disasm_scroll: rl.Vector2,
|
||||
}
|
||||
|
||||
// Requires an initilized emulatore System Struct
|
||||
run_simulator :: proc(s: ^emu.System) {
|
||||
sim := Simulator {
|
||||
machine = s,
|
||||
rom_loaded = false,
|
||||
running = false,
|
||||
paused = true,
|
||||
cycles_per_second = 12
|
||||
}
|
||||
reset_sim :: proc(sim: ^Simulator) {
|
||||
sim.paused = true
|
||||
sim.disasm_count = 0
|
||||
sim.rom_loaded = false
|
||||
sim.selected_rom = ""
|
||||
|
||||
run_gui(&sim)
|
||||
emu.reset_machine(sim.machine)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user