Compare commits

...

4 Commits

Author SHA1 Message Date
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
8 changed files with 496 additions and 61 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
APP_NAME := my_app
SRC := ./src
SRC := ./src/main_desktop
.PHONY: all dev release clean
+17 -4
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,9 +22,22 @@ main :: proc() {
// Init the emu 8 "cpu"
system := emu.init()
s := sim.Simulator {
machine = &system,
rom_loaded = false,
paused = true,
step = false,
cpu_hz = 700,
disasm_follow = true,
}
// Initilize sim, gui etc
sim.run_simulator(&system)
sim.init(&s)
for sim.should_run() {
sim.update(&s)
}
sim.shutdown(&s)
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, "] ")
}
+114
View File
@@ -0,0 +1,114 @@
<!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 {
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>
+68
View File
@@ -0,0 +1,68 @@
// 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(1*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
// Init the emu 8 "cpu"
system := emu.init()
s := sim.Simulator {
machine = &system,
rom_loaded = false,
paused = true,
step = false,
cpu_hz = 700,
disasm_follow = true,
}
sim.init(&s)
}
@export
main_update :: proc "c" () -> bool {
context = web_context
// TODO
sim.update()
return sim.should_run()
}
@export
main_end :: proc "c" () {
context = web_context
// TODO
sim.shutdown()
}
@export
web_window_size_changed :: proc "c" (w: c.int, h: c.int) {
context = web_context
// TODO
game.parent_window_size_changed(int(w), int(h))
}
+76 -56
View File
@@ -3,6 +3,9 @@ package simulator
import emu "../machine"
import rl "vendor:raylib"
// Globals
run: bool
// Window
WINDOW_WIDTH :: 1920
WINDOW_HEIGHT :: 1080
@@ -38,13 +41,17 @@ Layout :: struct {
info_box : rl.Rectangle,
}
// Initialize main the gui 'window'
run_gui :: proc(sim: ^Simulator) {
init :: proc(sim: ^Simulator) {
run = true
rl.SetConfigFlags({.WINDOW_RESIZABLE})
rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Octal Cookie - Chip 8 Simulator")
rl.InitAudioDevice()
rl.SetTargetFPS(60)
// 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)
@@ -56,71 +63,84 @@ run_gui :: proc(sim: ^Simulator) {
rl.GuiSetFont(font)
rl.GuiSetStyle(.DEFAULT, i32(rl.GuiDefaultProperty.TEXT_SIZE), 18)
}
// 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())
screen_height := f32(rl.GetScreenHeight())
sidebar_width := screen_width * 0.20
update :: proc(sim: ^Simulator) {
// 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
layout := calc_layout(screen_width, screen_height)
// set all the layout structs dynamically with the screen size
layout := calc_layout(screen_width, screen_height)
rl.BeginDrawing()
rl.ClearBackground(rl.Color{0x18, 0x18, 0x18, 0xFF})
rl.BeginDrawing()
rl.ClearBackground(rl.Color{0x18, 0x18, 0x18, 0xFF})
cycles := int(sim.cpu_hz / SIM_FPS)
if (!sim.paused) {
// Cycle the machine to update memory etc
emu.run_machine(sim.machine, cycles)
tick_timers(sim, beep)
}
if(sim.paused && sim.step) {
// Cycle the machine to update memory etc
emu.run_machine(sim.machine, 1)
tick_timers(sim, beep)
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()
cycles := int(sim.cpu_hz / SIM_FPS)
if (!sim.paused) {
// 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)
// 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()
}
shutdown :: proc(sim: ^Simulator) {
rl.UnloadFont(sim.font)
rl.UnloadSound(beep)
rl.UnloadSound(sim.sound)
rl.CloseAudioDevice()
rl.CloseWindow()
}
tick_timers :: proc(sim: ^Simulator, beep: rl.Sound) {
should_run :: proc() -> bool {
when ODIN_OS != .JS {
if rl.WindowShouldClose() {
run = false
}
}
return run
}
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
+3
View File
@@ -29,6 +29,7 @@ Simulator :: struct {
cpu_hz: f32,
info_box: bool,
// GUI
sound: rl.Sound,
font: rl.Font,
active_tab: i32,
mem_scroll: rl.Vector2,
@@ -44,6 +45,7 @@ Simulator :: struct {
}
// Requires an initilized emulatore System Struct
/*
run_simulator :: proc(s: ^emu.System) {
sim := Simulator {
machine = s,
@@ -56,3 +58,4 @@ run_simulator :: proc(s: ^emu.System) {
run_gui(&sim)
}
*/