Compare commits

...

46 Commits

Author SHA1 Message Date
jasonhilder 745ee7cecb Readme update for v1. 2026-07-03 11:26:21 +02:00
jasonhilder 68737e12da test remote. 2026-07-02 10:26:01 +02:00
jasonhilder 85e8481cf4 Testing new remote. 2026-07-02 10:24:22 +02:00
jasonhilder ec8bcdb5ed Fighting the wasm build... 2026-07-02 10:20:35 +02:00
jasonhilder 747702d256 Updated gitignore to exclude build output. 2026-06-28 07:01:00 +02:00
jasonhilder 717ef479fa Added globals and refactor for wasm. 2026-06-28 06:59:46 +02:00
jasonhilder 5edae5d2d8 Web build script and updates to web source.
Added Karl Zylinski web build script.
Updated template for better width.
Removed resizing function.
2026-06-28 06:57:48 +02:00
jasonhilder d43ec53d8d Added build tag for non web only. 2026-06-28 06:55:13 +02:00
jasonhilder 6140db0d8f Initial web build commit. 2026-06-25 06:29:30 +02:00
jasonhilder cc1962ff79 Updated for new directories. 2026-06-25 06:26:13 +02:00
jasonhilder 8830331774 Restructure of code for wasm build. 2026-06-25 06:25:44 +02:00
jasonhilder eded8b60b7 Updated main to be wasm friendly.
Updated code to use an init, update, shutdown and should_run proc.
To get it ready to use wasm for the web build requires this structure.
2026-06-25 06:24:24 +02:00
jasonhilder 30a37f26c5 Updated fzf ignore file, added raylib window title 2026-06-24 10:34:02 +02:00
jasonhilder 97f34d72a7 Added a stack view in bottom panel 2026-06-24 10:33:39 +02:00
jasonhilder 65b692f293 Added an info box for name, repo link. 2026-06-24 10:33:09 +02:00
jasonhilder 6d706a34cd Dropped tab layout, floating info window and clean
No longer using tabs, all info in one bottom panel.
Added a floating info window, can open from control bar.
Cleanup vertical widths for scroll bars in some panes.
2026-06-24 10:31:09 +02:00
jasonhilder 6a43058033 Added adjustable speed for simulator.
Added struct fields, calcs for the correct cycles per frame by hz value.
Updated control bar to wire it all up and make sure it updates in real
time.
2026-06-24 10:21:52 +02:00
jasonhilder 7403efa6cf Tiny cleanup 2026-06-24 10:13:37 +02:00
jasonhilder 9d83c49872 Updated showcase. 2026-06-24 08:12:35 +02:00
jasonhilder 6ca732f178 Updated showcase. 2026-06-23 08:40:51 +02:00
jasonhilder baa1e1a5cf added game roms to src for embedding. 2026-06-23 08:40:34 +02:00
jasonhilder c909d28587 Added embed roms and cleaned up selecting roms.
Added 2 new helper functions one to load a rom selected on system and
anothe to load a rom into memory from the embedded roms.
2026-06-23 08:06:08 +02:00
jasonhilder 356ed2408a Refactor disassembled instructions to be fixed arr
Updated code to make disassembled instructions a fixed array since we
know the cap at runtime, Also added toggle switch to follow the current
instruction
2026-06-19 12:35:12 +02:00
jasonhilder 0b5006f985 Step, reset, tick_timer proc and struct cleanup.
Added a tick_timer proc so the code handles the step neatly.
Added some new struct fields to accomodate this and moved danglings
struct defs into the simulator definition.
2026-06-19 12:33:47 +02:00
jasonhilder e9cf387640 Cleanup of comments. 2026-06-19 12:31:20 +02:00
jasonhilder d314ef651e Renamed style_dark to dark. 2026-06-19 11:51:15 +02:00
jasonhilder 16b97b24b0 Updated themes for potential theme picker. 2026-06-19 11:43:35 +02:00
jasonhilder 61471536ca Updated screenshot/showcase. 2026-06-18 08:29:57 +02:00
jasonhilder 6f701560df added section for file loading + memory cleanup
Make sure to clean up any memory allocated to the sim in terms of
disassembly instructions etc
2026-06-18 08:26:14 +02:00
jasonhilder 1a6346895a Small ui changes to fit the window better. 2026-06-18 08:25:14 +02:00
jasonhilder 350a26d1b9 Added disassembly component. 2026-06-18 07:40:54 +02:00
jasonhilder 19f4593e0e Update README.md 2026-06-18 05:55:30 +02:00
jasonhilder c1c67d596d Fixed screenshot 2026-06-17 07:55:28 +02:00
jasonhilder ff3ff4bfda First version of Readme 2026-06-17 07:54:47 +02:00
jasonhilder 5b4e968d1c Added some default game roms 2026-06-17 07:48:34 +02:00
jasonhilder 52cc1e08b6 Small additions 2026-06-17 07:48:25 +02:00
jasonhilder 6605d86916 Added/updated components to be isolated rectangles. 2026-06-17 07:48:07 +02:00
jasonhilder cccc4fb06c Removed unneeded components, renamed reused.
Updated the components to start with a gui_ prefix, changed the layout
idea that each component is responsible for their own bounding box and
inner content.
2026-06-17 07:46:24 +02:00
jasonhilder baca22a004 Reorganized all usable assets. 2026-06-13 09:34:14 +02:00
jasonhilder d40a00c74a Added .ignore file for fzf searching 2026-06-13 09:31:50 +02:00
jasonhilder 2c664b24a9 Cleaning up ui components making uniform. 2026-06-13 09:31:32 +02:00
jasonhilder d28aa8a401 added field for checking rom loaded state. 2026-06-13 09:31:17 +02:00
jasonhilder c3367cdf3e Added left panel rom loader.
Left panel has tinyfieldialogs to open a file picker, so you can swap
out roms, required a reset machine function aswell.
2026-06-13 09:29:53 +02:00
jasonhilder 2e83baea8e Added tinyfilediaglogs external dependancy 2026-06-13 09:29:01 +02:00
jasonhilder 4839adf6c1 Initial workings for components. 2026-06-12 09:44:24 +02:00
jasonhilder 231c260bc6 Added uniform constants for neater components. 2026-06-12 09:43:47 +02:00
77 changed files with 10524 additions and 306 deletions
+5
View File
@@ -2,5 +2,10 @@
*.bin
*.o
# Artifact output directory
build/
# Project management
todo
+3
View File
@@ -0,0 +1,3 @@
external/
assets/
src/roms
+1 -1
View File
@@ -1,5 +1,5 @@
APP_NAME := my_app
SRC := ./src
SRC := ./src/main_desktop
.PHONY: all dev release clean
+79 -2
View File
@@ -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](showcase.png)](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.
View File
Executable
+38
View File
@@ -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}"
+6
View File
@@ -0,0 +1,6 @@
@echo off
cl /c tinyfiledialogs.c /Fo:tinyfiledialogs.obj
lib /OUT:tinyfiledialogs.lib tinyfiledialogs.obj
move tinyfiledialogs.lib \windows\
del tinyfiledialogs.obj
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
case $(uname) in
"Darwin")
case $(uname -m) in
"arm64") lib_path="macos-arm64" ;;
*) lib_path="macos" ;;
esac
# Static
clang -c -o tinyfiledialogs.o tinyfiledialogs.c -fPIC
ar r tinyfiledialogs.a tinyfiledialogs.o
mv tinyfiledialogs.a $lib_path/tinyfiledialogs.a
rm tinyfiledialogs.o
;;
*)
gcc -c tinyfiledialogs.c -o linux/tinyfiledialogs.o
ar rcs linux/tinyfiledialogs.a linux/tinyfiledialogs.o
rm linux/tinyfiledialogs.o
;;
esac
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
File diff suppressed because it is too large Load Diff
+314
View File
@@ -0,0 +1,314 @@
/* SPDX-License-Identifier: Zlib
Copyright (c) 2014 - 2024 Guillaume Vareille http://ysengrin.com
____________________________________________________________________
| |
| 100% compatible C C++ -> You can rename tinfiledialogs.c as .cpp |
|____________________________________________________________________|
********* TINY FILE DIALOGS OFFICIAL WEBSITE IS ON SOURCEFORGE *********
_________
/ \ tinyfiledialogs.h v3.18.2 [Jun 8, 2024]
|tiny file| Unique header file created [November 9, 2014]
| dialogs |
\____ ___/ http://tinyfiledialogs.sourceforge.net
\| git clone http://git.code.sf.net/p/tinyfiledialogs/code tinyfd
____________________________________________
| |
| email: tinyfiledialogs at ysengrin.com |
|____________________________________________|
________________________________________________________________________________
| ____________________________________________________________________________ |
| | | |
| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
| | | |
| | on windows: | |
| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
| | | |
| | - _wfopen() requires wchar_t | |
| | - fopen() uses char but expects ASCII or MBCS (not UTF-8) | |
| | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | |
| | | |
| | - alternatively, tinyfiledialogs provides | |
| | functions to convert between UTF-8, UTF-16 and MBCS | |
| |____________________________________________________________________________| |
|________________________________________________________________________________|
If you like tinyfiledialogs, please upvote my stackoverflow answer
https://stackoverflow.com/a/47651444
- License -
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
__________________________________________
| ______________________________________ |
| | | |
| | DO NOT USE USER INPUT IN THE DIALOGS | |
| |______________________________________| |
|__________________________________________|
*/
#ifndef TINYFILEDIALOGS_H
#define TINYFILEDIALOGS_H
#ifdef __cplusplus
extern "C" {
#endif
/******************************************************************************************************/
/**************************************** UTF-8 on Windows ********************************************/
/******************************************************************************************************/
#ifdef _WIN32
/* On windows, if you want to use UTF-8 ( instead of the UTF-16/wchar_t functions at the end of this file )
Make sure your code is really prepared for UTF-8 (on windows, functions like fopen() expect MBCS and not UTF-8) */
extern int tinyfd_winUtf8; /* on windows char strings can be 1:UTF-8(default) or 0:MBCS */
/* for MBCS change this to 0, in tinyfiledialogs.c or in your code */
/* Here are some functions to help you convert between UTF-16 UTF-8 MBSC */
char * tinyfd_utf8toMbcs(char const * aUtf8string);
char * tinyfd_utf16toMbcs(wchar_t const * aUtf16string);
wchar_t * tinyfd_mbcsTo16(char const * aMbcsString);
char * tinyfd_mbcsTo8(char const * aMbcsString);
wchar_t * tinyfd_utf8to16(char const * aUtf8string);
char * tinyfd_utf16to8(wchar_t const * aUtf16string);
#endif
/******************************************************************************************************/
/******************************************************************************************************/
/******************************************************************************************************/
/************* 3 funtions for C# (you don't need this in C or C++) : */
char const * tinyfd_getGlobalChar(char const * aCharVariableName); /* returns NULL on error */
int tinyfd_getGlobalInt(char const * aIntVariableName); /* returns -1 on error */
int tinyfd_setGlobalInt(char const * aIntVariableName, int aValue); /* returns -1 on error */
/* aCharVariableName: "tinyfd_version" "tinyfd_needs" "tinyfd_response"
aIntVariableName : "tinyfd_verbose" "tinyfd_silent" "tinyfd_allowCursesDialogs"
"tinyfd_forceConsole" "tinyfd_assumeGraphicDisplay" "tinyfd_winUtf8"
**************/
extern char tinyfd_version[8]; /* contains tinyfd current version number */
extern char tinyfd_needs[]; /* info about requirements */
extern int tinyfd_verbose; /* 0 (default) or 1 : on unix, prints the command line calls */
extern int tinyfd_silent; /* 1 (default) or 0 : on unix, hide errors and warnings from called dialogs */
/** Curses dialogs are difficult to use and counter-intuitive.
On windows they are only ascii and still uses the unix backslash ! **/
extern int tinyfd_allowCursesDialogs; /* 0 (default) or 1 */
extern int tinyfd_forceConsole; /* 0 (default) or 1 */
/* for unix & windows: 0 (graphic mode) or 1 (console mode).
0: try to use a graphic solution, if it fails then it uses console mode.
1: forces all dialogs into console mode even when an X server is present.
if enabled, it can use the package Dialog or dialog.exe.
on windows it only make sense for console applications */
extern int tinyfd_assumeGraphicDisplay; /* 0 (default) or 1 */
/* some systems don't set the environment variable DISPLAY even when a graphic display is present.
set this to 1 to tell tinyfiledialogs to assume the existence of a graphic display */
extern char tinyfd_response[1024];
/* if you pass "tinyfd_query" as aTitle,
the functions will not display the dialogs
but will return 0 for console mode, 1 for graphic mode.
tinyfd_response is then filled with the retain solution.
possible values for tinyfd_response are (all lowercase)
for graphic mode:
windows_wchar windows applescript kdialog zenity zenity3 yad matedialog
shellementary qarma python2-tkinter python3-tkinter python-dbus
perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst
for console mode:
dialog whiptail basicinput no_solution */
void tinyfd_beep(void);
int tinyfd_notifyPopup(
char const * aTitle, /* NULL or "" */
char const * aMessage, /* NULL or "" may contain \n \t */
char const * aIconType); /* "info" "warning" "error" */
/* return has only meaning for tinyfd_query */
int tinyfd_messageBox(
char const * aTitle , /* NULL or "" */
char const * aMessage , /* NULL or "" may contain \n \t */
char const * aDialogType , /* "ok" "okcancel" "yesno" "yesnocancel" */
char const * aIconType , /* "info" "warning" "error" "question" */
int aDefaultButton ) ;
/* 0 for cancel/no , 1 for ok/yes , 2 for no in yesnocancel */
char * tinyfd_inputBox(
char const * aTitle , /* NULL or "" */
char const * aMessage , /* NULL or "" (\n and \t have no effect) */
char const * aDefaultInput ) ; /* NULL = passwordBox, "" = inputbox */
/* returns NULL on cancel */
char * tinyfd_saveFileDialog(
char const * aTitle , /* NULL or "" */
char const * aDefaultPathAndOrFile , /* NULL or "" , ends with / to set only a directory */
int aNumOfFilterPatterns , /* 0 (1 in the following example) */
char const * const * aFilterPatterns , /* NULL or char const * lFilterPatterns[1]={"*.txt"} */
char const * aSingleFilterDescription ) ; /* NULL or "text files" */
/* returns NULL on cancel */
char * tinyfd_openFileDialog(
char const * aTitle, /* NULL or "" */
char const * aDefaultPathAndOrFile, /* NULL or "" , ends with / to set only a directory */
int aNumOfFilterPatterns , /* 0 (2 in the following example) */
char const * const * aFilterPatterns, /* NULL or char const * lFilterPatterns[2]={"*.png","*.jpg"}; */
char const * aSingleFilterDescription, /* NULL or "image files" */
int aAllowMultipleSelects ) ; /* 0 or 1 */
/* in case of multiple files, the separator is | */
/* returns NULL on cancel */
char * tinyfd_selectFolderDialog(
char const * aTitle, /* NULL or "" */
char const * aDefaultPath); /* NULL or "" */
/* returns NULL on cancel */
char * tinyfd_colorChooser(
char const * aTitle, /* NULL or "" */
char const * aDefaultHexRGB, /* NULL or "" or "#FF0000" */
unsigned char const aDefaultRGB[3] , /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */
unsigned char aoResultRGB[3] ) ; /* unsigned char lResultRGB[3]; */
/* aDefaultRGB is used only if aDefaultHexRGB is absent */
/* aDefaultRGB and aoResultRGB can be the same array */
/* returns NULL on cancel */
/* returns the hexcolor as a string "#FF0000" */
/* aoResultRGB also contains the result */
/************ WINDOWS ONLY SECTION ************************/
#ifdef _WIN32
/* windows only - utf-16 version */
int tinyfd_notifyPopupW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aMessage, /* NULL or L"" may contain \n \t */
wchar_t const * aIconType); /* L"info" L"warning" L"error" */
/* windows only - utf-16 version */
int tinyfd_messageBoxW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aMessage, /* NULL or L"" may contain \n \t */
wchar_t const * aDialogType, /* L"ok" L"okcancel" L"yesno" */
wchar_t const * aIconType, /* L"info" L"warning" L"error" L"question" */
int aDefaultButton ); /* 0 for cancel/no , 1 for ok/yes */
/* returns 0 for cancel/no , 1 for ok/yes */
/* windows only - utf-16 version */
wchar_t * tinyfd_inputBoxW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aMessage, /* NULL or L"" (\n nor \t not respected) */
wchar_t const * aDefaultInput); /* NULL passwordBox, L"" inputbox */
/* windows only - utf-16 version */
wchar_t * tinyfd_saveFileDialogW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */
int aNumOfFilterPatterns, /* 0 (1 in the following example) */
wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[1]={L"*.txt"} */
wchar_t const * aSingleFilterDescription); /* NULL or L"text files" */
/* returns NULL on cancel */
/* windows only - utf-16 version */
wchar_t * tinyfd_openFileDialogW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aDefaultPathAndOrFile, /* NULL or L"" , ends with / to set only a directory */
int aNumOfFilterPatterns , /* 0 (2 in the following example) */
wchar_t const * const * aFilterPatterns, /* NULL or wchar_t const * lFilterPatterns[2]={L"*.png","*.jpg"} */
wchar_t const * aSingleFilterDescription, /* NULL or L"image files" */
int aAllowMultipleSelects ) ; /* 0 or 1 */
/* in case of multiple files, the separator is | */
/* returns NULL on cancel */
/* windows only - utf-16 version */
wchar_t * tinyfd_selectFolderDialogW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aDefaultPath); /* NULL or L"" */
/* returns NULL on cancel */
/* windows only - utf-16 version */
wchar_t * tinyfd_colorChooserW(
wchar_t const * aTitle, /* NULL or L"" */
wchar_t const * aDefaultHexRGB, /* NULL or L"#FF0000" */
unsigned char const aDefaultRGB[3], /* unsigned char lDefaultRGB[3] = { 0 , 128 , 255 }; */
unsigned char aoResultRGB[3]); /* unsigned char lResultRGB[3]; */
/* returns the hexcolor as a string L"#FF0000" */
/* aoResultRGB also contains the result */
/* aDefaultRGB is used only if aDefaultHexRGB is NULL */
/* aDefaultRGB and aoResultRGB can be the same array */
/* returns NULL on cancel */
#endif /*_WIN32 */
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif /* TINYFILEDIALOGS_H */
/*
________________________________________________________________________________
| ____________________________________________________________________________ |
| | | |
| | on windows: | |
| | - for UTF-16, use the wchar_t functions at the bottom of the header file | |
| | - _wfopen() requires wchar_t | |
| | | |
| | - in tinyfiledialogs, char is UTF-8 by default (since v3.6) | |
| | - but fopen() expects MBCS (not UTF-8) | |
| | - if you want char to be MBCS: set tinyfd_winUtf8 to 0 | |
| | | |
| | - alternatively, tinyfiledialogs provides | |
| | functions to convert between UTF-8, UTF-16 and MBCS | |
| |____________________________________________________________________________| |
|________________________________________________________________________________|
- This is not for ios nor android (it works in termux though).
- The files can be renamed with extension ".cpp" as the code is 100% compatible C C++
(just comment out << extern "C" >> in the header file)
- Windows is fully supported from XP to 10 (maybe even older versions)
- C# & LUA via dll, see files in the folder EXTRAS
- OSX supported from 10.4 to latest (maybe even older versions)
- Do not use " and ' as the dialogs will be displayed with a warning
instead of the title, message, etc...
- There's one file filter only, it may contain several patterns.
- If no filter description is provided,
the list of patterns will become the description.
- On windows link against Comdlg32.lib and Ole32.lib
(on windows the no linking claim is a lie)
- On unix: it tries command line calls, so no such need (NO LINKING).
- On unix you need one of the following:
applescript, kdialog, zenity, matedialog, shellementary, qarma, yad,
python (2 or 3)/tkinter/python-dbus (optional), Xdialog
or curses dialogs (opens terminal if running without console).
- One of those is already included on most (if not all) desktops.
- In the absence of those it will use gdialog, gxmessage or whiptail
with a textinputbox. If nothing is found, it switches to basic console input,
it opens a console if needed (requires xterm + bash).
- for curses dialogs you must set tinyfd_allowCursesDialogs=1
- You can query the type of dialog that will be used (pass "tinyfd_query" as aTitle)
- String memory is preallocated statically for all the returned values.
- File and path names are tested before return, they should be valid.
- tinyfd_forceConsole=1; at run time, forces dialogs into console mode.
- On windows, console mode only make sense for console applications.
- On windows, console mode is not implemented for wchar_T UTF-16.
- Mutiple selects are not possible in console mode.
- The package dialog must be installed to run in curses dialogs in console mode.
It is already installed on most unix systems.
- On osx, the package dialog can be installed via
http://macappstore.org/dialog or http://macports.org
- On windows, for curses dialogs console mode,
dialog.exe should be copied somewhere on your executable path.
It can be found at the bottom of the following page:
http://andrear.altervista.org/home/cdialog.php
*/
+63
View File
@@ -0,0 +1,63 @@
#+build !js
package tinyfiledialogs
import "base:builtin"
import "core:c"
when ODIN_OS == .Darwin {
foreign import lib "./macos-arm64/tinyfiledialogs.a"
} else when ODIN_OS == .Linux {
// TODO: this is completely untested.
foreign import lib "./linux/tinyfiledialogs.a"
} else when ODIN_OS == .Windows {
foreign import lib {"./windows/tinyfiledialogs.lib", "system:User32.lib", "system:Shell32.lib", "system:Comdlg32.lib", "system:Ole32.lib"}
}
@(default_calling_convention = "c")
@(link_prefix = "tinyfd_")
foreign lib {
// contains tinyfd current version number
version: [8]c.char
// info about requirements
needs: []c.char
// 0 (default) or 1 : on unix, prints the command line calls
verbose: c.int
// 1 (default) or 0 : on unix, hide errors and warnings from called dialogs
silent: c.int
// Curses dialogs are difficult to use and counter-intuitive.
// On windows they are only ascii and still uses the unix backslash !
// 0 (default) or 1
allowCursesDialogs: c.int
// for unix & windows: 0 (graphic mode) or 1 (console mode).
// 0: try to use a graphic solution, if it fails then it uses console mode.
// 1: forces all dialogs into console mode even when an X server is present.
// if enabled, it can use the package Dialog or dialog.exe.
// on windows it only make sense for console applications
// 0 (default) or 1
forceConsole: c.int
// some systems don't set the environment variable DISPLAY even when a graphic display is present.
// set this to 1 to tell tinyfiledialogs to assume the existence of a graphic display
assumeGraphicDisplay: c.int
// if you pass "tinyfd_query" as aTitle,
// the functions will not display the dialogs
// but will return 0 for console mode, 1 for graphic mode.
// tinyfd_response is then filled with the retain solution.
// possible values for tinyfd_response are (all lowercase)
// for graphic mode:
// windows_wchar windows applescript kdialog zenity zenity3 yad matedialog
// shellementary qarma python2-tkinter python3-tkinter python-dbus
// perl-dbus gxmessage gmessage xmessage xdialog gdialog dunst
// for console mode:
// dialog whiptail basicinput no_solution
response: [1024]c.char
beep :: proc() ---
notifyPopup :: proc(aTitle: cstring, aMessage: cstring, aIconType: cstring) -> c.int ---
messageBox :: proc(aTitle: cstring, aMessage: cstring, aDialogType: cstring, aIconType: cstring, aDefaultButton: c.int) -> c.int ---
inputBox :: proc(aTitle: cstring, aMessage: cstring, aDefaultInput: cstring) -> cstring ---
saveFileDialog :: proc(aTitle: cstring, aDefaultPathAndOrFile: cstring, aNumOfFilterPatterns: c.int, aFilterPatterns: [^]cstring, aSingleFilterDescription: cstring) -> cstring ---
openFileDialog :: proc(aTitle: cstring, aDefaultPathAndOrFile: cstring, aNumOfFilterPatterns: c.int, aFilterPatterns: [^]cstring, aSingleFilterDescription: cstring, aAllowMultipleSelects: c.int) -> cstring ---
selectFolderDialog :: proc(aTitle: cstring, aDefaultPath: cstring) -> cstring ---
colorChooser :: proc(aTitle: cstring, aDefaultHexRGB: cstring, aDefaultRGB: [^]c.uchar, aoResultRGB: [^]c.uchar) -> cstring ---
}
+28
View File
@@ -0,0 +1,28 @@
package tinyfiledialogs
import "base:builtin"
import "core:c"
import win32 "core:sys/windows"
foreign import lib {"./windows/tinyfiledialogs.lib", "system:User32.lib", "system:Shell32.lib", "system:Comdlg32.lib", "system:Ole32.lib"}
@(default_calling_convention = "c")
@(link_prefix = "tinyfd_")
foreign lib {
winUtf8: c.int
utf8toMbcs :: proc(aUtf8string: cstring) -> cstring ---
utf16toMbcs :: proc(aUtf16string: [^]win32.wchar_t) -> cstring ---
mbcsTo16 :: proc(aMbcsString: cstring) -> [^]win32.wchar_t ---
mbcsTo8 :: proc(aMbcsString: cstring) -> cstring ---
utf8to16 :: proc(aUtf8string: cstring) -> [^]win32.wchar_t ---
utf16to8 :: proc(aUtf16string: [^]win32.wchar_t) -> cstring ---
// Windows only utf-16 versions
notifyPopupW :: proc(aTitle: [^]win32.wchar_t, aMessage: [^]win32.wchar_t, aIconType: [^]win32.wchar_t) -> c.int ---
messageBoxW :: proc(aTitle: [^]win32.wchar_t, aMessage: [^]win32.wchar_t, aDialogType: [^]win32.wchar_t, aIconType: [^]win32.wchar_t, aDefaultButton: c.int) -> c.int ---
inputBoxW :: proc(aTitle: [^]win32.wchar_t, aMessage: [^]win32.wchar_t, aDefaultInput: [^]win32.wchar_t) -> [^]win32.wchar_t ---
saveFileDialogW :: proc(aTitle: [^]win32.wchar_t, aDefaultPathAndOrFile: [^]win32.wchar_t, aNumOfFilterPatterns: c.int, aFilterPatterns: [^]win32.wstring, aSingleFilterDescription: [^]win32.wchar_t) -> [^]win32.wchar_t ---
openFileDialogW :: proc(aTitle: [^]win32.wchar_t, aDefaultPathAndOrFile: [^]win32.wchar_t, aNumOfFilterPatterns: c.int, aFilterPatterns: [^]win32.wstring, aSingleFilterDescription: [^]win32.wchar_t, aAllowMultipleSelects: c.int) -> [^]win32.wchar_t ---
selectFolderDialogW :: proc(aTitle: [^]win32.wchar_t, aDefaultPath: [^]win32.wchar_t) -> [^]win32.wchar_t ---
colorChooserW :: proc(aTitle: [^]win32.wchar_t, aDefaultHexRGB: [^]win32.wchar_t, aDefaultRGB: [^]c.uchar, aoResultRGB: [^]c.uchar) -> win32.wchar_t ---
}
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

+19 -3
View File
@@ -1,7 +1,5 @@
package machine
import "core:log"
// System struct, init, constants, fontset
System :: struct {
@@ -48,7 +46,7 @@ FONT_SET := [80]u8 {
}
init :: proc() -> System {
log.info("Booting chip 8 cpu")
// log.info("Booting chip 8 cpu")
// Structs are zero initialized so timers, sp etc are good.
s := System { pc = 0x200 }
@@ -68,3 +66,21 @@ run_machine :: proc(s: ^System, cycles: int) {
cycle(s)
}
}
reset_machine :: proc(s: ^System) {
s.memory = {}
s.v = {}
s.stack = {}
s.sp = 0
s.i = 0
s.pc = 0x200
s.display = {}
s.keypad = {}
s.current_key = -1
s.delay_timer = 0
s.sound_timer = 0
for v, i in FONT_SET {
s.memory[i] = v
}
}
+4 -5
View File
@@ -1,17 +1,16 @@
#+build !js
package machine
// load_rom, anything rom related
import "core:log"
import "core:os"
load_rom :: proc(s: ^System, file_path: string) -> os.Error {
load_rom :: proc(s: ^System, file_path: string) -> (int, os.Error) {
log.info("Loading rom from file")
data, read_err := os.read_entire_file(file_path, context.allocator)
if read_err != os.ERROR_NONE {
log.errorf("failed to read rom %v", read_err)
return read_err
return 0, read_err
}
defer delete(data)
@@ -21,5 +20,5 @@ load_rom :: proc(s: ^System, file_path: string) -> os.Error {
log.infof("First few bytes: %X %X %X %X", s.memory[0x200], s.memory[0x201], s.memory[0x202], s.memory[0x203])
return nil
return len(data), nil
}
+16 -10
View File
@@ -3,8 +3,8 @@ package main
import "core:log"
import "core:mem"
import emu "machine"
import sim "simulator"
import emu "../machine"
import sim "../simulator"
DEV :: #config(DEV, false)
@@ -22,16 +22,22 @@ main :: proc() {
// Init the emu 8 "cpu"
system := emu.init()
// @TODO: move this into a gui component
// load rom, hardcoded for now, will eventually be cli or gui
err := emu.load_rom(&system, "./test_roms/7-beep.ch8")
if err != nil {
panic("failed to load rom!")
s := sim.Simulator {
machine = &system,
rom_loaded = false,
paused = true,
step = false,
cpu_hz = 700,
disasm_follow = true,
}
// Initilize sim, gui etc
sim.run_simulator(&system)
sim.init(&s)
for sim.should_run() {
sim.update()
}
sim.shutdown()
when DEV {
if len(track.allocation_map) > 0 {
+126
View File
@@ -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
}
+91
View File
@@ -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, "] ")
}
+115
View File
@@ -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>
+65
View File
@@ -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
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-37
View File
@@ -1,37 +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)
// Small text area
// Control bar buttons
btn_w : f32 = 80
btn_h : f32 = rect.height - (PADDING * 2)
gap : f32 = 6
cursor : f32 = rect.x + PADDING
if btn(&cursor, rect, btn_h, btn_w, gap, "RUN") {
log.info("RUN clicked")
sim.paused = false
sim.running = true
}
if btn(&cursor, rect, btn_h, btn_w, gap, "PAUSE") {
log.info("PAUSE clicked")
sim.paused = true
sim.running = false
}
if btn(&cursor, rect, btn_h, btn_w, gap, "STEP") { log.info("STEP clicked") }
}
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)
}
+168 -52
View File
@@ -3,45 +3,80 @@ package simulator
import emu "../machine"
import rl "vendor:raylib"
// Initial window size
sim_run: bool
// Window
WINDOW_WIDTH :: 1920
WINDOW_HEIGHT :: 1080
// @TODO: If this grows lets move it into its own file
SIDEBAR_PERCENT :: 0.20
DISPLAY_PERCENT :: 0.30
// Layout proportions
// Sidebar takes 20% of screen width on each side; display takes the rest.
// The center column splits vertically: 40% display, 60% debug panel.
SIDEBAR_PERCENT :: f32(0.20)
DISPLAY_V_RATIO :: f32(0.40)
// Fixed heights
CONTROL_BAR_H :: f32(50)
STATUS_BAR_H :: f32(30)
// Layout constants
PADDING_X :: f32(10)
PADDING_Y :: f32(8)
PANEL_HEADER :: f32(24)
// Buttons
BUTTON_HEIGHT :: 30
BUTTON_WIDTH :: 120
// Fonts
BIG_FONT_SIZE :: 20
KEYPAD_FONT_SIZE :: 18
LINE_HEIGHT :: 22
Layout :: struct {
control_bar : rl.Rectangle,
left_panel : rl.Rectangle,
file_loader : rl.Rectangle,
keypad : rl.Rectangle,
display : rl.Rectangle,
right_panel : rl.Rectangle,
bottom_panel : rl.Rectangle,
cpu : rl.Rectangle,
status_bar : rl.Rectangle,
info_box : rl.Rectangle,
}
// Initialize main the gui 'window'
run_gui :: proc(sim: ^Simulator) {
init :: proc(sim: ^Simulator) {
_sim = sim
sim_run = true
// desktop
when ODIN_OS != .JS {
rl.SetConfigFlags({.WINDOW_RESIZABLE})
rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "raylib")
rl.InitAudioDevice()
rl.SetTargetFPS(60)
beep := rl.LoadSound("./beep.wav")
}
// web
when ODIN_OS == .JS {
// rl.SetConfigFlags({.VSYNC_HINT})
}
rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Octal Cookie - Chip 8 Simulator")
rl.InitAudioDevice()
// Load sound
beep := rl.LoadSound("./assets/sounds/beep.wav")
_sim.sound = beep
// Load fonts
font := rl.LoadFontEx("./assets/fonts/Inter_18pt-Regular.ttf", 18, nil, 0)
rl.SetTextureFilter(font.texture, .BILINEAR)
sim.font = font
_sim.font = font
rl.GuiLoadStyleDefault()
rl.GuiLoadStyle("./assets/raygui_styles/style_dark.rgs")
rl.GuiLoadStyle("./assets/raygui_styles/genesis.rgs")
rl.GuiSetFont(font)
rl.GuiSetStyle(.DEFAULT, cast(i32)rl.GuiDefaultProperty.TEXT_SIZE, 18)
rl.GuiSetStyle(.DEFAULT, i32(rl.GuiDefaultProperty.TEXT_SIZE), 18)
}
update :: proc() {
sim := _sim
// Draw each of the components in its own window within the main window
for !rl.WindowShouldClose() {
// Recalculate layout each frame based on current window size
// Pass these down to gui functions so they can setup their sizes?
screen_width := f32(rl.GetScreenWidth())
@@ -52,79 +87,160 @@ run_gui :: proc(sim: ^Simulator) {
layout := calc_layout(screen_width, screen_height)
rl.BeginDrawing()
rl.ClearBackground(rl.BLACK)
rl.ClearBackground(rl.Color{0x18, 0x18, 0x18, 0xFF})
// Cycle the machine to update memory etc
if (!sim.paused) {
emu.run_machine(sim.machine, 12)
// Handle delay timer
if sim.machine.delay_timer > 0 do sim.machine.delay_timer -= 1
// Current sound file is around 1s so stop
// immediately when timer is 0
if sim.machine.sound_timer > 0 {
sim.machine.sound_timer -= 1
if !rl.IsSoundPlaying(beep) do rl.PlaySound(beep)
} else {
rl.StopSound(beep)
cycles := int(sim.cpu_hz / SIM_FPS)
// Cycle the machine to update memory etc
emu.run_machine(sim.machine, cycles)
tick_timers(sim)
}
if(sim.paused && sim.step) {
// Cycle the machine to update memory etc
emu.run_machine(sim.machine, 1)
tick_timers(sim)
sim.step = false
}
// Top
// ------------------------------------------
gui_control_bar(layout.control_bar, sim)
gui_left_panel(layout.left_panel, sim)
// gui_right_panel(layout.right_panel, s.machine)
// Screen is just drawing the display buffer just needs that as arg
// Not the whole sim struct
gui_screen(layout.display, sim.machine)
// 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.UnloadSound(beep)
rl.UnloadSound(sim.sound)
rl.CloseAudioDevice()
rl.CloseWindow()
}
// @TODO: If this grows lets move it into its own file
should_run :: proc() -> bool {
when ODIN_OS != .JS {
return !rl.WindowShouldClose()
}
// web loop is controlled by JS requestAnimationFrame
return true
}
tick_timers :: proc(sim: ^Simulator) {
beep := sim.sound
if sim.machine.delay_timer > 0 do sim.machine.delay_timer -= 1
if sim.machine.sound_timer > 0 {
sim.machine.sound_timer -= 1
if !rl.IsSoundPlaying(beep) do rl.PlaySound(beep)
} else {
// only stop if actually playing
if rl.IsSoundPlaying(beep) do rl.StopSound(beep)
}
}
calc_layout :: proc(screen_width: f32, screen_height: f32) -> Layout {
// Control bar is a fixed height frozen at top of gui, all items start below it.
y_pos := CONTROL_BAR_H
x_pos := f32(0)
// Usable gui vertical space
visible_height := screen_height - CONTROL_BAR_H - STATUS_BAR_H
sidebar_width := screen_width * SIDEBAR_PERCENT
content_height := screen_height - CONTROL_BAR_H - STATUS_BAR_H
display_height := screen_height * DISPLAY_PERCENT - CONTROL_BAR_H - STATUS_BAR_H
display_width := screen_width - (sidebar_width * 2) - sidebar_width
display_height := visible_height * DISPLAY_V_RATIO
keypad_height := (y_pos + visible_height - visible_height / 2)
x_center := sidebar_width
y_center := screen_width - sidebar_width * 2
return Layout {
// Left Area
control_bar = rl.Rectangle{
x = 0,
y = 0,
width = screen_width,
height = CONTROL_BAR_H,
},
left_panel = rl.Rectangle {
file_loader = rl.Rectangle{
x = 0,
y = CONTROL_BAR_H,
y = y_pos,
width = sidebar_width,
height = content_height,
height = keypad_height - y_pos
},
keypad = rl.Rectangle{
x = 0,
y = keypad_height,
width = sidebar_width,
height = visible_height / 2
},
// ------------------------------------------
// Center Area
display = rl.Rectangle{
x = sidebar_width,
y = CONTROL_BAR_H,
width = screen_width - (sidebar_width * 2),
y = y_pos,
width = display_width,
height = display_height,
},
right_panel = rl.Rectangle {
x = screen_width - sidebar_width,
y = CONTROL_BAR_H,
width = sidebar_width,
height = content_height,
// ------------------------------------------
// Bottom Area
bottom_panel = rl.Rectangle{
x = sidebar_width,
y = y_pos + display_height,
width = screen_width - sidebar_width,
height = visible_height - display_height,
},
// ------------------------------------------
// Right Area
cpu = rl.Rectangle {
x = sidebar_width + display_width,
y = y_pos,
width = sidebar_width * 2,
height = display_height
},
// ------------------------------------------
// Bottom Area
status_bar = rl.Rectangle{
x = 0,
y = screen_height - STATUS_BAR_H,
width = screen_width, height = STATUS_BAR_H,
x = x_pos,
y = y_pos + visible_height,
width = screen_width,
height = STATUS_BAR_H,
},
// ------------------------------------------
// Info Box
info_box = rl.Rectangle{
x = (screen_width / 2 - 200),
y = (screen_height / 2 - 200),
width = 400,
height = 140
}
}
}
+31
View File
@@ -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)
}
+77
View File
@@ -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)
}
+104
View File
@@ -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 (V0VF, 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)
}
}
+245
View File
@@ -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
}
+174
View File
@@ -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
}
+51
View File
@@ -0,0 +1,51 @@
package simulator
import rl "vendor:raylib"
gui_info_box :: proc(rect: rl.Rectangle, sim: ^Simulator, screen_width: f32, screen_height: f32) {
if sim.info_box {
screen_rect := rl.Rectangle{0, 0, screen_width, screen_height}
rl.DrawRectangleRec(screen_rect, {0, 0, 0, 140})
if rl.GuiWindowBox(rect, "App Info") > 0 {
sim.info_box = false
}
content_y := rect.y + 24
center_x := rect.x + rect.width / 2
name_text : cstring = "Octal Cookie"
desc_text : cstring = "A CHIP-8 emulator and simulator written in Odin."
source_text : cstring = "https://codeberg.org/jasonhilder/octal_cookie"
name_size := rl.MeasureTextEx(sim.font, name_text, 18, 1)
desc_size := rl.MeasureTextEx(sim.font, desc_text, 18, 1)
source_size := rl.MeasureTextEx(sim.font, source_text, 18, 1)
rl.DrawTextEx(sim.font, name_text, {center_x - name_size.x / 2, content_y + 20}, 18, 1, rl.WHITE)
rl.DrawTextEx(sim.font, desc_text, {center_x - desc_size.x / 2, content_y + 46}, 18, 1, rl.WHITE)
source_pos := rl.Vector2{center_x - source_size.x / 2, content_y + 72}
rl.DrawTextEx(sim.font, source_text, source_pos, 18, 1, rl.SKYBLUE)
source_rect := rl.Rectangle{
x = source_pos.x,
y = source_pos.y,
width = source_size.x,
height = source_size.y,
}
mouse := rl.GetMousePosition()
if rl.CheckCollisionPointRec(mouse, source_rect) {
rl.SetMouseCursor(.POINTING_HAND)
rl.DrawRectangleRec( {source_pos.x, source_pos.y + source_size.y - 1, source_size.x, 1}, rl.SKYBLUE,)
if rl.IsMouseButtonPressed(.LEFT) {
rl.OpenURL("https://codeberg.org/jasonhilder/octal_cookie")
}
} else {
rl.SetMouseCursor(.DEFAULT)
}
}
}
+65
View File
@@ -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,
)
}
}
+69
View File
@@ -0,0 +1,69 @@
package simulator
import rl "vendor:raylib"
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
// 2 : 1
// 2 so, for every 1 unit of height I have 2 units of width
// twice as wide as it is tall, or half as tall as it is wide
aspect_ratio : f32 = 64.0 / 32.0
// create viewport for the ratio
view := rect
avail_space := bounds.width / bounds.height
if avail_space > aspect_ratio {
view.width = bounds.height * aspect_ratio
view.x = bounds.x + (bounds.width - view.width) * 0.5
} else {
view.height = bounds.width / aspect_ratio
view.y = bounds.y + (bounds.height - view.height) * 0.5
}
// get scale
pixel := min(int(view.width / 64), int(view.height / 32))
pixel = max(pixel, 1)
draw_w := pixel * 64
draw_h := pixel * 32
// center frame
x := i32(view.x + (view.width - f32(draw_w)) * 0.5)
y := i32(view.y + (view.height - f32(draw_h)) * 0.5)
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
if !sim.rom_loaded {
// centered drop-zone text
text: cstring = "PLEASE SELECT AND LOAD A CHIP 8 ROM"
text_width := rl.MeasureText(text, BIG_FONT_SIZE)
text_x := view.x + (view.width - f32(text_width)) * 0.6
text_y := view.y + (view.height - f32(BIG_FONT_SIZE)) * 0.5
rl.DrawTextEx(sim.font, text, {text_x,text_y}, BIG_FONT_SIZE, 1, rl.WHITE)
} else {
render_display(&s.display, x, y, i32(pixel))
}
}
@(private = "file")
render_display :: proc(display_buffer: ^[32][64]u8, offset_x, offset_y, scale: i32) {
rl.DrawRectangle(offset_x, offset_y, 64 * scale, 32 * scale, rl.BLACK)
for y in 0..<len(display_buffer) {
for x in 0..<len(display_buffer[0]) {
if display_buffer[y][x] == 0x01 {
rl.DrawRectangle(i32(x) * scale + offset_x, i32(y) * scale + offset_y, scale, scale, rl.WHITE)
}
}
}
}
@@ -3,23 +3,38 @@ package simulator
import "core:fmt"
import rl "vendor:raylib"
StatusIconShape :: enum { CIRCLE, SQUARE }
gui_status_bar :: proc(rect: rl.Rectangle, sim: ^Simulator) {
// Left to right text draws
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
cursor: f32 = rect.x + PADDING
cursor: f32 = rect.x + PADDING_X
cy := rect.y + rect.height * 0.5
if sim.running && !sim.paused {
if !sim.paused {
status_icon(&cursor, cy, rl.GREEN, .CIRCLE, "Running", sim.font)
} else {
status_icon(&cursor, cy, rl.RED, .SQUARE, "Paused", sim.font)
}
status_divider(&cursor, cy)
status_text(&cursor, cy, fmt.ctprintf("FPS: %d", rl.GetFPS()), sim.font)
}
status_text(&cursor, cy, fmt.ctprintf("Rom Loaded: %v", sim.rom_loaded), sim.font)
StatusIconShape :: enum { CIRCLE, SQUARE }
// FPS set far right
fps_text := fmt.ctprintf("FPS: %d", rl.GetFPS())
fps_width := rl.MeasureTextEx(sim.font, fps_text, f32(sim.font.baseSize), 1).x
fps_x := rect.x + rect.width - PADDING_X - fps_width
rl.DrawTextEx(
sim.font,
fps_text,
{fps_x, cy - f32(sim.font.baseSize) * 0.5},
f32(sim.font.baseSize),
1,
rl.RAYWHITE,
)
}
status_icon :: proc(cursor: ^f32, cy: f32, color: rl.Color, shape: StatusIconShape, label: cstring, font: rl.Font) {
size: f32 = 10
@@ -32,15 +47,15 @@ status_icon :: proc(cursor: ^f32, cy: f32, color: rl.Color, shape: StatusIconSha
cursor^ += size + 6
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) {
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) {
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
}
+73
View File
@@ -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
}
}
}
}
+124
View File
@@ -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,
)
}
}
+63
View File
@@ -0,0 +1,63 @@
package simulator
import "core:fmt"
import rl "vendor:raylib"
gui_tab_stack :: proc(rect: rl.Rectangle, sim: ^Simulator) {
// Panel below header
panel_rect := rl.Rectangle{
rect.x,
rect.y + PANEL_HEADER,
rect.width,
rect.height - PANEL_HEADER,
}
rl.GuiPanel(panel_rect, nil)
// Header background
rl.DrawRectangle(i32(rect.x), i32(rect.y), i32(rect.width), i32(PANEL_HEADER), rl.BLACK)
rl.DrawTextEx(sim.font, "Stack", {rect.x + 4, rect.y + 4}, 18, 1, rl.WHITE)
// SP indicator in header
sp_label := fmt.ctprintf("SP: %d", sim.machine.sp)
sp_size := rl.MeasureTextEx(sim.font, sp_label, 18, 1)
rl.DrawTextEx(sim.font, sp_label, {rect.x + rect.width - sp_size.x - 8, rect.y + 4}, 18, 1, rl.WHITE)
// Use full panel height so empty slots fill the space
total_height := max(f32(16) * LINE_HEIGHT, panel_rect.height)
content_rect := rl.Rectangle{
x = panel_rect.x,
y = panel_rect.y - PANEL_HEADER,
width = panel_rect.width - 15,
height = total_height,
}
view: rl.Rectangle
rl.GuiScrollPanel(panel_rect, nil, content_rect, &sim.stack_scroll, &view)
rl.BeginScissorMode(i32(view.x), i32(view.y), i32(view.width), i32(view.height))
defer rl.EndScissorMode()
index_x := panel_rect.x + PADDING_X
colon_x := index_x + 30
for i in 0..<16 {
y_pos := panel_rect.y + (f32(i) * LINE_HEIGHT) + sim.stack_scroll.y
is_active := i < int(sim.machine.sp)
is_top := i == int(sim.machine.sp) - 1
// Only draw a background for active slots
if is_active {
rl.DrawRectangleV(
{panel_rect.x + sim.stack_scroll.x, y_pos},
{panel_rect.width, LINE_HEIGHT},
rl.BLACK,
)
}
sp_marker := ""
if is_top do sp_marker = " <- SP"
color := rl.WHITE if is_top else (rl.BLACK if is_active else rl.WHITE)
txt_index := fmt.ctprintf("%02d", i)
txt_value := fmt.ctprintf(": 0x%04X%s", sim.machine.stack[i], sp_marker)
rl.DrawTextEx(sim.font, txt_index, {index_x + sim.stack_scroll.x, y_pos}, 18, 1, color)
rl.DrawTextEx(sim.font, txt_value, {colon_x + sim.stack_scroll.x, y_pos}, 18, 1, color)
}
}
-72
View File
@@ -1,72 +0,0 @@
package simulator
import "core:log"
import "core:strings"
import rl "vendor:raylib"
gui_left_panel :: proc(rect: rl.Rectangle, sim: ^Simulator) {
top_panel := rl.Rectangle {
rect.x,
rect.y,
rect.width,
rect.height / 2,
}
rl.DrawRectangleLinesEx(top_panel, 1, rl.DARKGRAY)
gui_file_loader(top_panel)
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,) {
if rl.IsFileDropped() {
log.info("file drop found!")
dropped_file := rl.LoadDroppedFiles()
if dropped_file.count > 1 {
path_str := string(dropped_file.paths[0])
log.info("file dropped: ", path_str)
}
rl.UnloadDroppedFiles(dropped_file)
}
}
gui_key_pad :: proc(rect: rl.Rectangle, display: [16]bool, font: rl.Font) {
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}, {"D", 14},
{"A", 10}, {"0", 0}, {"B", 11}, {"F", 15}
}
btn_width := f32(rect.width / 4)
btn_height := f32(rect.height / 4)
for val, idx in keys {
str := strings.clone_to_cstring(val.label)
ri := int(idx / 4)
ci := idx % 4
irect := rl.Rectangle{
x = btn_width * f32(ci),
y = (btn_height * f32(ri)) + rect.y,
width = btn_width,
height = btn_height,
}
if(display[val.index]) { rl.DrawRectangleRec(irect, rl.DARKGRAY) }
rl.DrawRectangleLinesEx(irect, 1, rl.GRAY)
rl.DrawTextEx(font, str, rl.Vector2{irect.x + btn_width / 2, irect.y + btn_height / 2}, 18, 1, rl.WHITE)
// Free the allocations!
delete(str)
}
}
-8
View File
@@ -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)
}
-49
View File
@@ -1,49 +0,0 @@
package simulator
import m "../machine"
import rl "vendor:raylib"
gui_screen :: proc(rect: rl.Rectangle, s: ^m.System) {
// Make sure screen is staying in its aspect ratio
chip8_aspect := f32(64.0 / 32.0)
available_aspect := rect.width / rect.height
screen_rect := rect
if available_aspect > chip8_aspect {
// Area is too wide
screen_rect.width = rect.height * chip8_aspect
screen_rect.x = rect.x + (rect.width - screen_rect.width) / 2
} else {
// Area is too tall
screen_rect.height = rect.width / chip8_aspect
screen_rect.y = rect.y + (rect.height - screen_rect.height) / 2
}
pixel_size := min(int(screen_rect.width / 64), int(screen_rect.height / 32))
pixel_size = max(pixel_size, 1)
actual_width := pixel_size * 64
actual_height := pixel_size * 32
display_x := i32(screen_rect.x) + i32(int(screen_rect.width) - actual_width) / 2
display_y := i32(screen_rect.y) + i32(int(screen_rect.height) - actual_height) / 2
// Debug borders
rl.DrawRectangleLinesEx(rect, 1, rl.DARKGRAY)
rl.DrawRectangleLinesEx(screen_rect, 2, rl.WHITE)
render_display(&s.display, display_x, display_y, i32(pixel_size))
}
@(private = "file")
render_display :: proc(display_buffer: ^[32][64]u8, offset_x, offset_y, scale: i32) {
rl.DrawRectangle(offset_x, offset_y, 64 * scale, 32 * scale, rl.BLACK)
for y in 0..<len(display_buffer) {
for x in 0..<len(display_buffer[0]) {
if display_buffer[y][x] == 0x01 {
rl.DrawRectangle(i32(x) * scale + offset_x, i32(y) * scale + offset_y, scale, scale, rl.WHITE)
}
}
}
}
+44 -11
View File
@@ -3,22 +3,55 @@ package simulator
import emu "../machine"
import rl "vendor:raylib"
@(private="package")
_sim: ^Simulator
// Embed roms
roms := #load_directory("../roms")
// CHIP-8 ROM can be at most 3584 bytes (4096 - 0x200 reserved)
// Each instruction is 2 bytes, 3584 / 2 = 1792 instructions.
MAX_INSTRUCTIONS :: 1792
SIM_FPS :: 60
DEFAULT_CPU_HZ :: 700
MIN_HZ :: 100
MAX_HZ :: 1500
Instruction :: struct {
address: u16,
raw: u16,
str: [32]u8
}
Simulator :: struct {
// Emulator
machine: ^emu.System,
running: bool,
rom_loaded: bool,
paused: bool,
cycles_per_second: int,
step: bool,
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
run_simulator :: proc(s: ^emu.System) {
sim := Simulator {
machine = s,
running = true,
paused = false,
cycles_per_second = 12
reset_sim :: proc(sim: ^Simulator) {
sim.paused = true
sim.disasm_count = 0
sim.rom_loaded = false
sim.selected_rom = ""
emu.reset_machine(sim.machine)
}
run_gui(&sim)
}