Compare commits
38 Commits
baca22a004
...
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 | |||
| 5b4e968d1c | |||
| 52cc1e08b6 | |||
| 6605d86916 | |||
| cccc4fb06c |
+6
-1
@@ -2,5 +2,10 @@
|
|||||||
*.bin
|
*.bin
|
||||||
*.o
|
*.o
|
||||||
|
|
||||||
|
# Artifact output directory
|
||||||
|
build/
|
||||||
|
|
||||||
# Project management
|
# Project management
|
||||||
todo
|
todo
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
APP_NAME := my_app
|
APP_NAME := my_app
|
||||||
SRC := ./src
|
SRC := ./src/main_desktop
|
||||||
|
|
||||||
.PHONY: all dev release clean
|
.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
|
package tinyfiledialogs
|
||||||
|
|
||||||
import "base:builtin"
|
import "base:builtin"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 322 KiB |
+14
-13
@@ -1,7 +1,5 @@
|
|||||||
package machine
|
package machine
|
||||||
|
|
||||||
import "core:log"
|
|
||||||
|
|
||||||
// System struct, init, constants, fontset
|
// System struct, init, constants, fontset
|
||||||
|
|
||||||
System :: struct {
|
System :: struct {
|
||||||
@@ -48,7 +46,7 @@ FONT_SET := [80]u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init :: proc() -> System {
|
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.
|
// Structs are zero initialized so timers, sp etc are good.
|
||||||
s := System { pc = 0x200 }
|
s := System { pc = 0x200 }
|
||||||
@@ -69,17 +67,20 @@ run_machine :: proc(s: ^System, cycles: int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new_machine :: proc() -> System {
|
reset_machine :: proc(s: ^System) {
|
||||||
s: System
|
s.memory = {}
|
||||||
s.pc = 0x200
|
s.v = {}
|
||||||
s.current_key = -1
|
s.stack = {}
|
||||||
// load fonts into the memory
|
s.sp = 0
|
||||||
|
s.i = 0
|
||||||
|
s.pc = 0x200
|
||||||
|
s.display = {}
|
||||||
|
s.keypad = {}
|
||||||
|
s.current_key = -1
|
||||||
|
s.delay_timer = 0
|
||||||
|
s.sound_timer = 0
|
||||||
|
|
||||||
for v, i in FONT_SET {
|
for v, i in FONT_SET {
|
||||||
s.memory[i] = v
|
s.memory[i] = v
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
reset_machine :: proc(s: ^System) {
|
|
||||||
s^ = new_machine()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
|
#+build !js
|
||||||
package machine
|
package machine
|
||||||
|
|
||||||
// load_rom, anything rom related
|
|
||||||
|
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:os"
|
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")
|
log.info("Loading rom from file")
|
||||||
|
|
||||||
data, read_err := os.read_entire_file(file_path, context.allocator)
|
data, read_err := os.read_entire_file(file_path, context.allocator)
|
||||||
if read_err != os.ERROR_NONE {
|
if read_err != os.ERROR_NONE {
|
||||||
log.errorf("failed to read rom %v", read_err)
|
log.errorf("failed to read rom %v", read_err)
|
||||||
return read_err
|
return 0, read_err
|
||||||
}
|
}
|
||||||
defer delete(data)
|
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])
|
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:log"
|
||||||
import "core:mem"
|
import "core:mem"
|
||||||
|
|
||||||
import emu "machine"
|
import emu "../machine"
|
||||||
import sim "simulator"
|
import sim "../simulator"
|
||||||
|
|
||||||
DEV :: #config(DEV, false)
|
DEV :: #config(DEV, false)
|
||||||
|
|
||||||
@@ -22,9 +22,22 @@ main :: proc() {
|
|||||||
|
|
||||||
// Init the emu 8 "cpu"
|
// Init the emu 8 "cpu"
|
||||||
system := emu.init()
|
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.init(&s)
|
||||||
sim.run_simulator(&system)
|
|
||||||
|
for sim.should_run() {
|
||||||
|
sim.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
sim.shutdown()
|
||||||
|
|
||||||
when DEV {
|
when DEV {
|
||||||
if len(track.allocation_map) > 0 {
|
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
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,33 +0,0 @@
|
|||||||
package simulator
|
|
||||||
|
|
||||||
import "core:log"
|
|
||||||
import rl "vendor:raylib"
|
|
||||||
|
|
||||||
PADDING :: 10
|
|
||||||
|
|
||||||
gui_control_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
|
||||||
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
|
|
||||||
|
|
||||||
// Cursor moves for every btn call places them left to right with padding
|
|
||||||
cursor : f32 = rect.x + PANEL_PADDING
|
|
||||||
|
|
||||||
if btn(&cursor, rect, BUTTON_HEIGHT, BUTTON_WIDTH, PANEL_PADDING, "RUN") {
|
|
||||||
if sim.rom_loaded {
|
|
||||||
sim.paused = false
|
|
||||||
sim.running = true
|
|
||||||
} else {
|
|
||||||
log.info("no rom selected, can't run")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if btn(&cursor, rect, BUTTON_HEIGHT, BUTTON_WIDTH, PANEL_PADDING, "PAUSE") {
|
|
||||||
sim.paused = true
|
|
||||||
sim.running = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
btn :: proc(cursor: ^f32, rect: rl.Rectangle, h, w, gap: f32, label: cstring) -> bool {
|
|
||||||
r := rl.Rectangle{cursor^, rect.y + PADDING, w, h}
|
|
||||||
cursor^ += w + gap
|
|
||||||
return rl.GuiButton(r, label)
|
|
||||||
}
|
|
||||||
+178
-90
@@ -3,156 +3,244 @@ package simulator
|
|||||||
import emu "../machine"
|
import emu "../machine"
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
// Initial window size
|
sim_run: bool
|
||||||
|
|
||||||
|
// Window
|
||||||
WINDOW_WIDTH :: 1920
|
WINDOW_WIDTH :: 1920
|
||||||
WINDOW_HEIGHT :: 1080
|
WINDOW_HEIGHT :: 1080
|
||||||
|
|
||||||
// @TODO: If this grows lets move it into its own file
|
// Layout proportions
|
||||||
// ─── Layout constants ───────────────────────────────────────────────────
|
// Sidebar takes 20% of screen width on each side; display takes the rest.
|
||||||
SIDEBAR_PERCENT :: 0.20
|
// The center column splits vertically: 40% display, 60% debug panel.
|
||||||
DISPLAY_PERCENT :: 0.30
|
SIDEBAR_PERCENT :: f32(0.20)
|
||||||
CONTROL_BAR_H :: f32(50)
|
DISPLAY_V_RATIO :: f32(0.40)
|
||||||
STATUS_BAR_H :: f32(30)
|
// Fixed heights
|
||||||
PANEL_PADDING :: 10
|
CONTROL_BAR_H :: f32(50)
|
||||||
PANEL_HEADER :: 24
|
STATUS_BAR_H :: f32(30)
|
||||||
|
// Layout constants
|
||||||
|
PADDING_X :: f32(10)
|
||||||
|
PADDING_Y :: f32(8)
|
||||||
|
PANEL_HEADER :: f32(24)
|
||||||
|
// Buttons
|
||||||
BUTTON_HEIGHT :: 30
|
BUTTON_HEIGHT :: 30
|
||||||
BUTTON_WIDTH :: 120
|
BUTTON_WIDTH :: 120
|
||||||
|
// Fonts
|
||||||
BIG_FONT_SIZE :: 20
|
BIG_FONT_SIZE :: 20
|
||||||
KEYPAD_FONT_SIZE :: 18
|
KEYPAD_FONT_SIZE :: 18
|
||||||
|
LINE_HEIGHT :: 22
|
||||||
|
|
||||||
Layout :: struct {
|
Layout :: struct {
|
||||||
control_bar : rl.Rectangle,
|
control_bar : rl.Rectangle,
|
||||||
left_panel : rl.Rectangle,
|
file_loader : rl.Rectangle,
|
||||||
|
keypad : rl.Rectangle,
|
||||||
display : rl.Rectangle,
|
display : rl.Rectangle,
|
||||||
bottom_panel : rl.Rectangle,
|
bottom_panel : rl.Rectangle,
|
||||||
right_panel : rl.Rectangle,
|
cpu : rl.Rectangle,
|
||||||
status_bar : rl.Rectangle,
|
status_bar : rl.Rectangle,
|
||||||
|
info_box : rl.Rectangle,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize main the gui 'window'
|
init :: proc(sim: ^Simulator) {
|
||||||
run_gui :: proc(sim: ^Simulator) {
|
_sim = sim
|
||||||
rl.SetConfigFlags({.WINDOW_RESIZABLE})
|
sim_run = true
|
||||||
rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "raylib")
|
|
||||||
|
// desktop
|
||||||
|
when ODIN_OS != .JS {
|
||||||
|
rl.SetConfigFlags({.WINDOW_RESIZABLE})
|
||||||
|
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()
|
rl.InitAudioDevice()
|
||||||
rl.SetTargetFPS(60)
|
|
||||||
|
// Load sound
|
||||||
beep := rl.LoadSound("./assets/sounds/beep.wav")
|
beep := rl.LoadSound("./assets/sounds/beep.wav")
|
||||||
|
_sim.sound = beep
|
||||||
|
|
||||||
// Load fonts
|
// Load fonts
|
||||||
font := rl.LoadFontEx("./assets/fonts/Inter_18pt-Regular.ttf", 18, nil, 0)
|
font := rl.LoadFontEx("./assets/fonts/Inter_18pt-Regular.ttf", 18, nil, 0)
|
||||||
rl.SetTextureFilter(font.texture, .BILINEAR)
|
rl.SetTextureFilter(font.texture, .BILINEAR)
|
||||||
sim.font = font
|
_sim.font = font
|
||||||
|
|
||||||
rl.GuiLoadStyleDefault()
|
rl.GuiLoadStyleDefault()
|
||||||
rl.GuiLoadStyle("./assets/raygui_styles/style_dark.rgs")
|
rl.GuiLoadStyle("./assets/raygui_styles/genesis.rgs")
|
||||||
|
|
||||||
rl.GuiSetFont(font)
|
rl.GuiSetFont(font)
|
||||||
rl.GuiSetStyle(.DEFAULT, cast(i32)rl.GuiDefaultProperty.TEXT_SIZE, 18)
|
rl.GuiSetStyle(.DEFAULT, i32(rl.GuiDefaultProperty.TEXT_SIZE), 18)
|
||||||
|
}
|
||||||
|
|
||||||
// Draw each of the components in its own window within the main window
|
update :: proc() {
|
||||||
for !rl.WindowShouldClose() {
|
sim := _sim
|
||||||
// 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())
|
|
||||||
screen_height := f32(rl.GetScreenHeight())
|
|
||||||
sidebar_width := screen_width * 0.20
|
|
||||||
|
|
||||||
// set all the layout structs dynamically with the screen size
|
// Recalculate layout each frame based on current window size
|
||||||
layout := calc_layout(screen_width, screen_height)
|
// Pass these down to gui functions so they can setup their sizes?
|
||||||
|
screen_width := f32(rl.GetScreenWidth())
|
||||||
|
screen_height := f32(rl.GetScreenHeight())
|
||||||
|
sidebar_width := screen_width * 0.20
|
||||||
|
|
||||||
rl.BeginDrawing()
|
// set all the layout structs dynamically with the screen size
|
||||||
rl.ClearBackground(rl.BLACK)
|
layout := calc_layout(screen_width, screen_height)
|
||||||
|
|
||||||
if (!sim.paused) {
|
rl.BeginDrawing()
|
||||||
// Cycle the machine to update memory etc
|
rl.ClearBackground(rl.Color{0x18, 0x18, 0x18, 0xFF})
|
||||||
emu.run_machine(sim.machine, 12)
|
|
||||||
|
|
||||||
// Handle delay timer
|
if (!sim.paused) {
|
||||||
if sim.machine.delay_timer > 0 do sim.machine.delay_timer -= 1
|
cycles := int(sim.cpu_hz / SIM_FPS)
|
||||||
|
// Cycle the machine to update memory etc
|
||||||
// Current sound file is around 1s so stop
|
emu.run_machine(sim.machine, cycles)
|
||||||
// immediately when timer is 0
|
tick_timers(sim)
|
||||||
if sim.machine.sound_timer > 0 {
|
|
||||||
sim.machine.sound_timer -= 1
|
|
||||||
if !rl.IsSoundPlaying(beep) do rl.PlaySound(beep)
|
|
||||||
} else {
|
|
||||||
rl.StopSound(beep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gui_control_bar(layout.control_bar, sim)
|
|
||||||
gui_left_panel(layout.left_panel, sim)
|
|
||||||
gui_screen(layout.display, sim)
|
|
||||||
gui_status_bar(layout.status_bar, sim)
|
|
||||||
|
|
||||||
rl.EndDrawing()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// ------------------------------------------
|
||||||
|
gui_control_bar(layout.control_bar, sim)
|
||||||
|
|
||||||
|
// Left
|
||||||
|
// ------------------------------------------
|
||||||
|
gui_file_loader(layout.file_loader, sim)
|
||||||
|
gui_key_pad(layout.keypad, sim.machine.keypad, sim.font)
|
||||||
|
|
||||||
|
// Center
|
||||||
|
// ------------------------------------------
|
||||||
|
gui_screen(layout.display, sim)
|
||||||
|
|
||||||
|
// Right
|
||||||
|
// ------------------------------------------
|
||||||
|
gui_cpu(layout.cpu, sim)
|
||||||
|
|
||||||
|
// Bottom
|
||||||
|
// ------------------------------------------
|
||||||
|
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()
|
||||||
|
free_all(context.temp_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown :: proc() {
|
||||||
|
sim := _sim
|
||||||
rl.UnloadFont(sim.font)
|
rl.UnloadFont(sim.font)
|
||||||
rl.UnloadSound(beep)
|
rl.UnloadSound(sim.sound)
|
||||||
rl.CloseAudioDevice()
|
rl.CloseAudioDevice()
|
||||||
rl.CloseWindow()
|
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 {
|
calc_layout :: proc(screen_width: f32, screen_height: f32) -> Layout {
|
||||||
top_h := CONTROL_BAR_H
|
// Control bar is a fixed height frozen at top of gui, all items start below it.
|
||||||
bottom_h := STATUS_BAR_H
|
y_pos := CONTROL_BAR_H
|
||||||
content_h := screen_height - top_h - bottom_h
|
x_pos := f32(0)
|
||||||
content_y := top_h
|
|
||||||
|
|
||||||
sidebar_w := screen_width * SIDEBAR_PERCENT
|
// Usable gui vertical space
|
||||||
|
visible_height := screen_height - CONTROL_BAR_H - STATUS_BAR_H
|
||||||
|
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)
|
||||||
|
|
||||||
screen_h_ratio := 0.40
|
x_center := sidebar_width
|
||||||
bottom_panel_h := content_h * f32(1.0 - screen_h_ratio)
|
y_center := screen_width - sidebar_width * 2
|
||||||
screen_h := content_h * f32(screen_h_ratio)
|
|
||||||
memory_h := content_h - screen_h
|
|
||||||
|
|
||||||
center_x := sidebar_w
|
|
||||||
center_w := screen_width - sidebar_w * 2
|
|
||||||
|
|
||||||
return Layout {
|
return Layout {
|
||||||
|
// Left Area
|
||||||
control_bar = rl.Rectangle{
|
control_bar = rl.Rectangle{
|
||||||
x = 0,
|
x = 0,
|
||||||
y = 0,
|
y = 0,
|
||||||
width = screen_width,
|
width = screen_width,
|
||||||
height = top_h,
|
height = CONTROL_BAR_H,
|
||||||
},
|
},
|
||||||
|
file_loader = rl.Rectangle{
|
||||||
left_panel = rl.Rectangle{
|
|
||||||
x = 0,
|
x = 0,
|
||||||
y = top_h,
|
y = y_pos,
|
||||||
width = sidebar_w,
|
width = sidebar_width,
|
||||||
height = content_h,
|
height = keypad_height - y_pos
|
||||||
|
},
|
||||||
|
keypad = rl.Rectangle{
|
||||||
|
x = 0,
|
||||||
|
y = keypad_height,
|
||||||
|
width = sidebar_width,
|
||||||
|
height = visible_height / 2
|
||||||
},
|
},
|
||||||
|
|
||||||
// CHIP-8 screen (top center)
|
// ------------------------------------------
|
||||||
|
// Center Area
|
||||||
display = rl.Rectangle{
|
display = rl.Rectangle{
|
||||||
x = center_x,
|
x = sidebar_width,
|
||||||
y = top_h,
|
y = y_pos,
|
||||||
width = center_w,
|
width = display_width,
|
||||||
height = screen_h,
|
height = display_height,
|
||||||
},
|
},
|
||||||
|
|
||||||
// MEMORY / DEBUG panel (bottom center)
|
// ------------------------------------------
|
||||||
|
// Bottom Area
|
||||||
bottom_panel = rl.Rectangle{
|
bottom_panel = rl.Rectangle{
|
||||||
x = center_x,
|
x = sidebar_width,
|
||||||
y = top_h + screen_h,
|
y = y_pos + display_height,
|
||||||
width = center_w,
|
width = screen_width - sidebar_width,
|
||||||
height = memory_h,
|
height = visible_height - display_height,
|
||||||
},
|
},
|
||||||
|
|
||||||
right_panel = rl.Rectangle{
|
// ------------------------------------------
|
||||||
x = screen_width - sidebar_w,
|
// Right Area
|
||||||
y = top_h,
|
cpu = rl.Rectangle {
|
||||||
width = sidebar_w,
|
x = sidebar_width + display_width,
|
||||||
height = content_h,
|
y = y_pos,
|
||||||
|
width = sidebar_width * 2,
|
||||||
|
height = display_height
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Bottom Area
|
||||||
status_bar = rl.Rectangle{
|
status_bar = rl.Rectangle{
|
||||||
x = 0,
|
x = x_pos,
|
||||||
y = screen_height - bottom_h,
|
y = y_pos + visible_height,
|
||||||
width = screen_width,
|
width = screen_width,
|
||||||
height = bottom_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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package simulator
|
||||||
|
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
gui_bottom_panel :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||||
|
rl.DrawRectangleLinesEx(rect, 1, rl.GRAY)
|
||||||
|
|
||||||
|
memory_view_bounds := rl.Rectangle {
|
||||||
|
x = rect.x + PADDING_X,
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package simulator
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
gui_control_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||||
|
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
|
||||||
|
|
||||||
|
rl.DrawTextEx(sim.font, "Octal Cookie Chip 8 Sim ", {rect.x + PADDING_X, rect.y + 12}, 25, 1, rl.WHITE)
|
||||||
|
text_size := rl.MeasureTextEx(sim.font, "Octal Cookie Chip 8 Sim ", 25, 1)
|
||||||
|
|
||||||
|
// Cursor moves for every btn call places them left to right with padding
|
||||||
|
cursor : f32 = text_size.x + 5 + rect.x + PADDING_X
|
||||||
|
|
||||||
|
if btn(&cursor, rect, BUTTON_HEIGHT, BUTTON_WIDTH, PADDING_X, "RUN") {
|
||||||
|
if sim.rom_loaded {
|
||||||
|
sim.paused = false
|
||||||
|
} else {
|
||||||
|
// log.info("no rom selected, can't run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if btn(&cursor, rect, BUTTON_HEIGHT, BUTTON_WIDTH, PADDING_X, "PAUSE") {
|
||||||
|
sim.paused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btn :: proc(cursor: ^f32, rect: rl.Rectangle, h, w, gap: f32, label: cstring) -> bool {
|
||||||
|
r := rl.Rectangle{cursor^, rect.y + PADDING_X, w, h}
|
||||||
|
cursor^ += w + gap
|
||||||
|
return rl.GuiButton(r, label)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package simulator
|
||||||
|
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
CPU_SECTION_H :: 100 // enough for 3 rows of labels
|
||||||
|
REG_CELL_H :: 40 // fixed, won't grow with the panel
|
||||||
|
|
||||||
|
gui_cpu :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||||
|
rl.DrawRectangleLinesEx(rect, 1, rl.GRAY)
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
rl.GuiPanel(bounds, "CPU / Registers")
|
||||||
|
|
||||||
|
cpu_rect := rl.Rectangle {
|
||||||
|
x = bounds.x + PADDING_X,
|
||||||
|
y = bounds.y + PADDING_Y + PANEL_HEADER,
|
||||||
|
width = bounds.width - (PADDING_X * 2),
|
||||||
|
height = CPU_SECTION_H - (PADDING_Y * 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid constants
|
||||||
|
COLS :: 2
|
||||||
|
ROWS :: 3
|
||||||
|
LRATIO :: f32(0.6)
|
||||||
|
|
||||||
|
cell_w := cpu_rect.width / COLS
|
||||||
|
cell_h := cpu_rect.height / ROWS
|
||||||
|
|
||||||
|
labels := [5]string{ "Progam Counter", "Increment pointer", "Stack Pointer", "Delay Timer", "Sound Timer" }
|
||||||
|
values := [5]u16{
|
||||||
|
sim.machine.pc,
|
||||||
|
sim.machine.i,
|
||||||
|
u16(sim.machine.sp),
|
||||||
|
u16(sim.machine.delay_timer),
|
||||||
|
u16(sim.machine.sound_timer),
|
||||||
|
}
|
||||||
|
|
||||||
|
for index in 0..<5 {
|
||||||
|
col := index % COLS
|
||||||
|
row := index / COLS
|
||||||
|
|
||||||
|
cell_x := cpu_rect.x + f32(col) * cell_w
|
||||||
|
cell_y := cpu_rect.y + f32(row) * cell_h
|
||||||
|
|
||||||
|
label_rect := rl.Rectangle{cell_x + 20, cell_y, cell_w,cell_h}
|
||||||
|
box_rect := rl.Rectangle{cell_x + cell_w * LRATIO, cell_y, cell_w * (1 - LRATIO), cell_h}
|
||||||
|
|
||||||
|
rl.GuiLabel(label_rect, rl.TextFormat("%s", labels[index]))
|
||||||
|
rl.DrawRectangleLinesEx(box_rect, 1, rl.DARKGRAY)
|
||||||
|
|
||||||
|
// Right-aligned value
|
||||||
|
value_text := rl.TextFormat("0x%04X", values[index])
|
||||||
|
text_w := rl.MeasureText(value_text, 18)
|
||||||
|
value_x := box_rect.x + box_rect.width - f32(text_w) - 4
|
||||||
|
rl.GuiLabel({value_x, box_rect.y, box_rect.width, box_rect.height}, value_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
register_bounds := rl.Rectangle {
|
||||||
|
x = bounds.x,
|
||||||
|
y = bounds.y + PANEL_HEADER + PADDING_Y + CPU_SECTION_H,
|
||||||
|
width = bounds.width,
|
||||||
|
height = bounds.height - PANEL_HEADER - PADDING_Y - CPU_SECTION_H,
|
||||||
|
}
|
||||||
|
rl.GuiPanel(register_bounds, "Registers")
|
||||||
|
|
||||||
|
register_rect := rl.Rectangle {
|
||||||
|
x = register_bounds.x + PADDING_X,
|
||||||
|
y = register_bounds.y + PADDING_Y + PANEL_HEADER,
|
||||||
|
width = register_bounds.width - (PADDING_X * 2),
|
||||||
|
height = register_bounds.height - (PADDING_Y * 2) - PANEL_HEADER,
|
||||||
|
}
|
||||||
|
|
||||||
|
// V registers grid (V0–VF, 16 registers)
|
||||||
|
REG_COLS :: 4
|
||||||
|
REG_ROWS :: 4
|
||||||
|
|
||||||
|
reg_cell_w := register_rect.width / REG_COLS
|
||||||
|
|
||||||
|
for index in 0..<16 {
|
||||||
|
col := index % REG_COLS
|
||||||
|
row := index / REG_COLS
|
||||||
|
|
||||||
|
cell_x := register_rect.x + f32(col) * reg_cell_w
|
||||||
|
cell_y := register_rect.y + f32(row) * REG_CELL_H
|
||||||
|
|
||||||
|
label_rect := rl.Rectangle{cell_x + 20, cell_y, reg_cell_w, REG_CELL_H}
|
||||||
|
box_rect := rl.Rectangle{cell_x + reg_cell_w * LRATIO, cell_y, reg_cell_w * (1 - LRATIO), REG_CELL_H}
|
||||||
|
|
||||||
|
rl.GuiLabel(label_rect, rl.TextFormat("V%X", index))
|
||||||
|
rl.DrawRectangleLinesEx(box_rect, 1, rl.DARKGRAY)
|
||||||
|
|
||||||
|
// Right-aligned value
|
||||||
|
value_text := rl.TextFormat("0x%02X", sim.machine.v[index])
|
||||||
|
text_w := rl.MeasureText(value_text, 18)
|
||||||
|
value_x := box_rect.x + box_rect.width - f32(text_w) - 4
|
||||||
|
rl.GuiLabel({value_x, box_rect.y, box_rect.width, box_rect.height}, value_text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
#+build !js
|
||||||
|
package simulator
|
||||||
|
|
||||||
|
import "core:strings"
|
||||||
|
import "core:fmt"
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
import emu "../machine"
|
||||||
|
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 = top_bounds_height
|
||||||
|
}
|
||||||
|
rl.GuiPanel(bounds, nil)
|
||||||
|
|
||||||
|
btn_rect := rl.Rectangle {
|
||||||
|
bounds.x,
|
||||||
|
bounds.y,
|
||||||
|
rect.width - (PADDING_X * 2),
|
||||||
|
BUTTON_HEIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
if rl.GuiButton(btn_rect, "Open ROM") {
|
||||||
|
ret := tfd.openFileDialog("Open File Dialog", nil, 0, nil, nil, 0,)
|
||||||
|
rom_path := string(ret)
|
||||||
|
if rom_path == "" do return
|
||||||
|
load_selected_rom(sim, rom_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
dropped_file := rl.LoadDroppedFiles()
|
||||||
|
if dropped_file.count > 0 {
|
||||||
|
mouse := rl.GetMousePosition()
|
||||||
|
|
||||||
|
if rl.CheckCollisionPointRec(mouse, drop_zone) {
|
||||||
|
// path_str := string(dropped_file.paths[0])
|
||||||
|
// log.info("file dropped: ", path_str)
|
||||||
|
} else {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package simulator
|
||||||
|
|
||||||
|
import "core:strings"
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
|
||||||
|
gui_key_pad :: proc(rect: rl.Rectangle, display: [16]bool, font: rl.Font) {
|
||||||
|
rl.DrawRectangleLinesEx(rect, 1, rl.GRAY)
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
rl.GuiPanel(bounds, "Input / Keypad")
|
||||||
|
|
||||||
|
content := rl.Rectangle {
|
||||||
|
bounds.x,
|
||||||
|
bounds.y + PANEL_HEADER,
|
||||||
|
bounds.width,
|
||||||
|
bounds.height - PANEL_HEADER,
|
||||||
|
}
|
||||||
|
|
||||||
|
Key :: struct {
|
||||||
|
label: string,
|
||||||
|
index: int,
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := [16]Key {
|
||||||
|
{"1", 1}, {"2", 2}, {"3", 3}, {"C", 12},
|
||||||
|
{"4", 4}, {"5", 5}, {"6", 6}, {"D", 13},
|
||||||
|
{"7", 7}, {"8", 8}, {"9", 9}, {"E", 14},
|
||||||
|
{"A", 10}, {"0", 0}, {"B", 11}, {"F", 15},
|
||||||
|
}
|
||||||
|
|
||||||
|
btn_width := content.width / 4
|
||||||
|
btn_height := content.height / 4
|
||||||
|
|
||||||
|
for val, idx in keys {
|
||||||
|
str := strings.clone_to_cstring(val.label)
|
||||||
|
defer delete(str)
|
||||||
|
|
||||||
|
ri := idx / 4
|
||||||
|
ci := idx % 4
|
||||||
|
irect := rl.Rectangle {
|
||||||
|
x = content.x + btn_width * f32(ci),
|
||||||
|
y = content.y + btn_height * f32(ri),
|
||||||
|
width = btn_width,
|
||||||
|
height = btn_height,
|
||||||
|
}
|
||||||
|
|
||||||
|
if display[val.index] { rl.DrawRectangleRec(irect, rl.BLACK) }
|
||||||
|
|
||||||
|
rl.DrawRectangleLinesEx(irect, 1, rl.GRAY)
|
||||||
|
rl.DrawTextEx(
|
||||||
|
font,
|
||||||
|
str,
|
||||||
|
rl.Vector2{irect.x + btn_width / 2, irect.y + btn_height / 2},
|
||||||
|
KEYPAD_FONT_SIZE,
|
||||||
|
1,
|
||||||
|
rl.WHITE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,14 @@ package simulator
|
|||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
gui_screen :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
gui_screen :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
rl.GuiPanel(bounds, nil)
|
||||||
|
|
||||||
s := sim.machine
|
s := sim.machine
|
||||||
|
|
||||||
// 2 : 1
|
// 2 : 1
|
||||||
@@ -12,14 +20,14 @@ gui_screen :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
|||||||
|
|
||||||
// create viewport for the ratio
|
// create viewport for the ratio
|
||||||
view := rect
|
view := rect
|
||||||
avail_space := rect.width / rect.height
|
avail_space := bounds.width / bounds.height
|
||||||
|
|
||||||
if avail_space > aspect_ratio {
|
if avail_space > aspect_ratio {
|
||||||
view.width = rect.height * aspect_ratio
|
view.width = bounds.height * aspect_ratio
|
||||||
view.x = rect.x + (rect.width - view.width) * 0.5
|
view.x = bounds.x + (bounds.width - view.width) * 0.5
|
||||||
} else {
|
} else {
|
||||||
view.height = rect.width / aspect_ratio
|
view.height = bounds.width / aspect_ratio
|
||||||
view.y = rect.y + (rect.height - view.height) * 0.5
|
view.y = bounds.y + (bounds.height - view.height) * 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
// get scale
|
// get scale
|
||||||
@@ -32,9 +40,7 @@ gui_screen :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
|||||||
// center frame
|
// center frame
|
||||||
x := i32(view.x + (view.width - f32(draw_w)) * 0.5)
|
x := i32(view.x + (view.width - f32(draw_w)) * 0.5)
|
||||||
y := i32(view.y + (view.height - f32(draw_h)) * 0.5)
|
y := i32(view.y + (view.height - f32(draw_h)) * 0.5)
|
||||||
|
|
||||||
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
|
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
|
||||||
rl.DrawRectangleLinesEx(view, 2, rl.WHITE)
|
|
||||||
|
|
||||||
if !sim.rom_loaded {
|
if !sim.rom_loaded {
|
||||||
// centered drop-zone text
|
// centered drop-zone text
|
||||||
@@ -3,15 +3,16 @@ package simulator
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
// @TODO: render status bar text
|
StatusIconShape :: enum { CIRCLE, SQUARE }
|
||||||
|
|
||||||
gui_status_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
gui_status_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
||||||
// Left to right text draws
|
// Left to right text draws
|
||||||
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
|
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
|
||||||
|
|
||||||
cursor: f32 = rect.x + PADDING
|
cursor: f32 = rect.x + PADDING_X
|
||||||
cy := rect.y + rect.height * 0.5
|
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)
|
status_icon(&cursor, cy, rl.GREEN, .CIRCLE, "Running", sim.font)
|
||||||
} else {
|
} else {
|
||||||
status_icon(&cursor, cy, rl.RED, .SQUARE, "Paused", sim.font)
|
status_icon(&cursor, cy, rl.RED, .SQUARE, "Paused", sim.font)
|
||||||
@@ -23,7 +24,7 @@ gui_status_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
|||||||
// FPS set far right
|
// FPS set far right
|
||||||
fps_text := fmt.ctprintf("FPS: %d", rl.GetFPS())
|
fps_text := fmt.ctprintf("FPS: %d", rl.GetFPS())
|
||||||
fps_width := rl.MeasureTextEx(sim.font, fps_text, f32(sim.font.baseSize), 1).x
|
fps_width := rl.MeasureTextEx(sim.font, fps_text, f32(sim.font.baseSize), 1).x
|
||||||
fps_x := rect.x + rect.width - PADDING - fps_width
|
fps_x := rect.x + rect.width - PADDING_X - fps_width
|
||||||
|
|
||||||
rl.DrawTextEx(
|
rl.DrawTextEx(
|
||||||
sim.font,
|
sim.font,
|
||||||
@@ -35,8 +36,6 @@ gui_status_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusIconShape :: enum { CIRCLE, SQUARE }
|
|
||||||
|
|
||||||
status_icon :: proc(cursor: ^f32, cy: f32, color: rl.Color, shape: StatusIconShape, label: cstring, font: rl.Font) {
|
status_icon :: proc(cursor: ^f32, cy: f32, color: rl.Color, shape: StatusIconShape, label: cstring, font: rl.Font) {
|
||||||
size: f32 = 10
|
size: f32 = 10
|
||||||
ix := cursor^ + size * 0.5
|
ix := cursor^ + size * 0.5
|
||||||
@@ -48,15 +47,15 @@ status_icon :: proc(cursor: ^f32, cy: f32, color: rl.Color, shape: StatusIconSha
|
|||||||
|
|
||||||
cursor^ += size + 6
|
cursor^ += size + 6
|
||||||
rl.DrawTextEx(font, label, {cursor^, cy - f32(font.baseSize) * 0.5}, f32(font.baseSize), 1, rl.RAYWHITE)
|
rl.DrawTextEx(font, label, {cursor^, cy - f32(font.baseSize) * 0.5}, f32(font.baseSize), 1, rl.RAYWHITE)
|
||||||
cursor^ += rl.MeasureTextEx(font, label, f32(font.baseSize), 1).x + PADDING
|
cursor^ += rl.MeasureTextEx(font, label, f32(font.baseSize), 1).x + PADDING_X
|
||||||
}
|
}
|
||||||
|
|
||||||
status_divider :: proc(cursor: ^f32, cy: f32) {
|
status_divider :: proc(cursor: ^f32, cy: f32) {
|
||||||
rl.DrawLineV({cursor^, cy - 8}, {cursor^, cy + 8}, rl.DARKGRAY)
|
rl.DrawLineV({cursor^, cy - 8}, {cursor^, cy + 8}, rl.DARKGRAY)
|
||||||
cursor^ += PADDING
|
cursor^ += PADDING_X
|
||||||
}
|
}
|
||||||
|
|
||||||
status_text :: proc(cursor: ^f32, cy: f32, text: cstring, font: rl.Font) {
|
status_text :: proc(cursor: ^f32, cy: f32, text: cstring, font: rl.Font) {
|
||||||
rl.DrawTextEx(font, text, {cursor^, cy - f32(font.baseSize) * 0.5}, f32(font.baseSize), 1, rl.RAYWHITE)
|
rl.DrawTextEx(font, text, {cursor^, cy - f32(font.baseSize) * 0.5}, f32(font.baseSize), 1, rl.RAYWHITE)
|
||||||
cursor^ += rl.MeasureTextEx(font, text, f32(font.baseSize), 1).x + PADDING
|
cursor^ += rl.MeasureTextEx(font, text, f32(font.baseSize), 1).x + PADDING_X
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package simulator
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
MEM_INDICATOR_W :: 14
|
||||||
|
MEM_ADDRESS_W :: 72
|
||||||
|
MEM_ROW_H :: 20
|
||||||
|
MEM_BYTES_PER_ROW :: 16
|
||||||
|
MEM_TOTAL_ROWS :: 256
|
||||||
|
MEM_FONT_ROWS :: 5
|
||||||
|
MEM_ROM_START :: 512
|
||||||
|
MEM_VIRTUAL_ROWS :: MEM_FONT_ROWS + 1 + (256 - 32)
|
||||||
|
|
||||||
|
MEM_COL_LABELS := [16]string{"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"}
|
||||||
|
|
||||||
|
byte_col_w :: proc(rect: rl.Rectangle) -> f32 {
|
||||||
|
used := f32(MEM_INDICATOR_W + MEM_ADDRESS_W + 12)
|
||||||
|
return (rect.width - used) / 16
|
||||||
|
}
|
||||||
|
|
||||||
|
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), i32(PANEL_HEADER), rl.BLACK)
|
||||||
|
|
||||||
|
// Header: Address label
|
||||||
|
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}, 18, 1, rl.WHITE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header separator
|
||||||
|
rl.DrawLine(
|
||||||
|
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 + PANEL_HEADER,
|
||||||
|
rect.width,
|
||||||
|
rect.height - PANEL_HEADER,
|
||||||
|
}
|
||||||
|
|
||||||
|
content_rect := rl.Rectangle{
|
||||||
|
0, 0,
|
||||||
|
rect.width - 15,
|
||||||
|
f32(MEM_VIRTUAL_ROWS) * MEM_ROW_H,
|
||||||
|
}
|
||||||
|
|
||||||
|
view : rl.Rectangle
|
||||||
|
rl.GuiScrollPanel(panel_rect, nil, content_rect, &sim.mem_scroll, &view)
|
||||||
|
|
||||||
|
rl.BeginScissorMode(i32(view.x), i32(view.y), i32(view.width), i32(view.height))
|
||||||
|
|
||||||
|
draw_row := 0
|
||||||
|
|
||||||
|
// 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
|
||||||
|
draw_memory_row(rect, sim, draw_row, addr, row_y)
|
||||||
|
draw_row += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
sim.font,
|
||||||
|
"---- reserved (0x0050 - 0x01FF) ----",
|
||||||
|
{rect.x + MEM_INDICATOR_W, divider_y + 2},
|
||||||
|
16, 1, rl.DARKGRAY,
|
||||||
|
)
|
||||||
|
draw_row += 1
|
||||||
|
|
||||||
|
// 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
|
||||||
|
draw_memory_row(rect, sim, draw_row, addr, row_y)
|
||||||
|
draw_row += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.EndScissorMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_memory_row :: proc(rect: rl.Rectangle, sim: ^Simulator, visual_row: int, addr: int, row_y: f32) {
|
||||||
|
col_w := byte_col_w(rect)
|
||||||
|
|
||||||
|
if visual_row % 2 == 0 {
|
||||||
|
rl.DrawRectangle(i32(rect.x), i32(row_y), i32(rect.width), MEM_ROW_H, rl.ColorAlpha(rl.BLACK, 0.2))
|
||||||
|
}
|
||||||
|
|
||||||
|
addr_str := fmt.tprintf("%04X", addr)
|
||||||
|
rl.DrawTextEx(
|
||||||
|
sim.font,
|
||||||
|
strings.clone_to_cstring(addr_str, context.temp_allocator),
|
||||||
|
{rect.x + MEM_INDICATOR_W, row_y + 2},
|
||||||
|
16, 1, rl.RAYWHITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
for col in 0..<16 {
|
||||||
|
byte_val := sim.machine.memory[addr + col]
|
||||||
|
byte_str := fmt.tprintf("%02X", byte_val)
|
||||||
|
x := rect.x + MEM_INDICATOR_W + MEM_ADDRESS_W + f32(col) * col_w
|
||||||
|
color := rl.RAYWHITE if byte_val != 0 else rl.GRAY
|
||||||
|
rl.DrawTextEx(
|
||||||
|
sim.font,
|
||||||
|
strings.clone_to_cstring(byte_str, context.temp_allocator),
|
||||||
|
{x, row_y + 2},
|
||||||
|
16, 1, color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
package simulator
|
|
||||||
|
|
||||||
import "core:log"
|
|
||||||
import "core:strings"
|
|
||||||
import rl "vendor:raylib"
|
|
||||||
|
|
||||||
import emu "../machine"
|
|
||||||
import tfd "../../external/tinyfiledialogs"
|
|
||||||
|
|
||||||
gui_left_panel :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
|
||||||
// ── Top panel and components ──
|
|
||||||
top_panel := rl.Rectangle {
|
|
||||||
rect.x,
|
|
||||||
rect.y,
|
|
||||||
rect.width,
|
|
||||||
rect.height / 2,
|
|
||||||
}
|
|
||||||
rl.DrawRectangleLinesEx(top_panel, 1, rl.DARKGRAY)
|
|
||||||
|
|
||||||
// Dropzone/file loader
|
|
||||||
file_loader_rect := rl.Rectangle {
|
|
||||||
top_panel.x,
|
|
||||||
top_panel.y,
|
|
||||||
top_panel.width,
|
|
||||||
(top_panel.height / 3) - PANEL_HEADER - PANEL_PADDING * 2,
|
|
||||||
}
|
|
||||||
gui_file_loader(file_loader_rect, sim)
|
|
||||||
|
|
||||||
// ── Bottom panel and components ──
|
|
||||||
bottom_panel := rl.Rectangle {
|
|
||||||
rect.x,
|
|
||||||
rect.y + top_panel.height,
|
|
||||||
rect.width,
|
|
||||||
rect.height / 2,
|
|
||||||
}
|
|
||||||
rl.DrawRectangleLinesEx(bottom_panel, 1, rl.GREEN)
|
|
||||||
gui_key_pad(bottom_panel, sim.machine.keypad, sim.font)
|
|
||||||
}
|
|
||||||
|
|
||||||
gui_file_loader :: proc(rect: rl.Rectangle, sim: ^Simulator) {
|
|
||||||
rl.GuiPanel(rect, "Rom / File")
|
|
||||||
|
|
||||||
// drop-zone occupies the panel's content area, minus space for the button
|
|
||||||
drop_zone := rl.Rectangle {
|
|
||||||
rect.x + PANEL_PADDING,
|
|
||||||
rect.y + PANEL_HEADER + PANEL_PADDING,
|
|
||||||
rect.width - PANEL_PADDING * 2,
|
|
||||||
rect.height - PANEL_HEADER - PANEL_PADDING * 2 - BUTTON_HEIGHT - PANEL_PADDING,
|
|
||||||
}
|
|
||||||
rl.DrawRectangleLinesEx(drop_zone, 1, rl.GREEN)
|
|
||||||
|
|
||||||
// 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 + PANEL_PADDING,
|
|
||||||
BUTTON_WIDTH,
|
|
||||||
BUTTON_HEIGHT,
|
|
||||||
}
|
|
||||||
|
|
||||||
if rl.GuiButton(btn_rect, "Open ROM") {
|
|
||||||
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!")
|
|
||||||
}
|
|
||||||
|
|
||||||
sim.rom_loaded = true
|
|
||||||
sim.running = true
|
|
||||||
sim.paused = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle file drop
|
|
||||||
if rl.IsFileDropped() {
|
|
||||||
dropped_file := rl.LoadDroppedFiles()
|
|
||||||
if dropped_file.count > 0 {
|
|
||||||
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
|
|
||||||
} else {
|
|
||||||
log.info("File dropped outside drop zone, ignoring")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rl.UnloadDroppedFiles(dropped_file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gui_key_pad :: proc(rect: rl.Rectangle, display: [16]bool, font: rl.Font) {
|
|
||||||
rl.GuiPanel(rect, "Input / Keypad")
|
|
||||||
|
|
||||||
content := rl.Rectangle {
|
|
||||||
rect.x + PANEL_PADDING,
|
|
||||||
rect.y + PANEL_HEADER + PANEL_PADDING,
|
|
||||||
rect.width - PANEL_PADDING * 2,
|
|
||||||
rect.height - PANEL_HEADER - PANEL_PADDING * 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
Key :: struct {
|
|
||||||
label: string,
|
|
||||||
index: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := [16]Key {
|
|
||||||
{"1", 1}, {"2", 2}, {"3", 3}, {"C", 12},
|
|
||||||
{"4", 4}, {"5", 5}, {"6", 6}, {"D", 13},
|
|
||||||
{"7", 7}, {"8", 8}, {"9", 9}, {"E", 14},
|
|
||||||
{"A", 10}, {"0", 0}, {"B", 11}, {"F", 15},
|
|
||||||
}
|
|
||||||
|
|
||||||
btn_width := content.width / 4
|
|
||||||
btn_height := content.height / 4
|
|
||||||
|
|
||||||
for val, idx in keys {
|
|
||||||
str := strings.clone_to_cstring(val.label)
|
|
||||||
defer delete(str)
|
|
||||||
|
|
||||||
ri := idx / 4
|
|
||||||
ci := idx % 4
|
|
||||||
irect := rl.Rectangle {
|
|
||||||
x = content.x + btn_width * f32(ci),
|
|
||||||
y = content.y + btn_height * f32(ri),
|
|
||||||
width = btn_width,
|
|
||||||
height = btn_height,
|
|
||||||
}
|
|
||||||
|
|
||||||
if display[val.index] { rl.DrawRectangleRec(irect, rl.BLACK) }
|
|
||||||
|
|
||||||
rl.DrawRectangleLinesEx(irect, 1, rl.GRAY)
|
|
||||||
rl.DrawTextEx(
|
|
||||||
font,
|
|
||||||
str,
|
|
||||||
rl.Vector2{irect.x + btn_width / 2, irect.y + btn_height / 2},
|
|
||||||
KEYPAD_FONT_SIZE,
|
|
||||||
1,
|
|
||||||
rl.WHITE,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package simulator
|
|
||||||
|
|
||||||
import m "../machine"
|
|
||||||
import rl "vendor:raylib"
|
|
||||||
|
|
||||||
gui_right_panel :: proc(rect: rl.Rectangle, s: ^m.System) {
|
|
||||||
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
|
|
||||||
}
|
|
||||||
@@ -3,24 +3,55 @@ package simulator
|
|||||||
import emu "../machine"
|
import emu "../machine"
|
||||||
import rl "vendor:raylib"
|
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 {
|
Simulator :: struct {
|
||||||
machine: ^emu.System,
|
// Emulator
|
||||||
rom_loaded: bool,
|
machine: ^emu.System,
|
||||||
running: bool,
|
rom_loaded: bool,
|
||||||
paused: bool,
|
paused: bool,
|
||||||
cycles_per_second: int,
|
step: bool,
|
||||||
font: rl.Font,
|
cpu_hz: f32,
|
||||||
|
info_box: bool,
|
||||||
|
// GUI
|
||||||
|
sound: rl.Sound,
|
||||||
|
font: rl.Font,
|
||||||
|
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
|
reset_sim :: proc(sim: ^Simulator) {
|
||||||
run_simulator :: proc(s: ^emu.System) {
|
sim.paused = true
|
||||||
sim := Simulator {
|
sim.disasm_count = 0
|
||||||
machine = s,
|
sim.rom_loaded = false
|
||||||
rom_loaded = false,
|
sim.selected_rom = ""
|
||||||
running = false,
|
|
||||||
paused = true,
|
|
||||||
cycles_per_second = 12
|
|
||||||
}
|
|
||||||
|
|
||||||
run_gui(&sim)
|
emu.reset_machine(sim.machine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user