├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── README.md ├── long_cat.png └── round_cat.png ├── build_desktop.bat ├── build_desktop.sh ├── build_web.bat ├── build_web.sh ├── project.sublime-project └── source ├── game.odin ├── main_desktop └── main_desktop.odin ├── main_web ├── emscripten_allocator.odin ├── emscripten_logger.odin ├── index_template.html └── main_web.odin ├── utils.odin ├── utils_default.odin └── utils_web.odin /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | 12 | jobs: 13 | build_linux: 14 | name: Linux 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: laytan/setup-odin@v2 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | release: false 21 | 22 | - uses: mymindstorm/setup-emsdk@v14 23 | 24 | - uses: actions/checkout@v4 25 | 26 | - name: Build desktop 27 | run: ./build_desktop.sh 28 | 29 | - name: Build web 30 | run: ./build_web.sh 31 | 32 | build_macos: 33 | name: MacOS 34 | strategy: 35 | matrix: 36 | os: [macos-13, macos-15] 37 | runs-on: ${{matrix.os}} 38 | steps: 39 | - uses: laytan/setup-odin@v2 40 | with: 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | release: false 43 | 44 | - uses: mymindstorm/setup-emsdk@v14 45 | 46 | - uses: actions/checkout@v4 47 | 48 | - name: Build desktop 49 | run: ./build_desktop.sh 50 | 51 | - name: Build web 52 | run: ./build_web.sh 53 | 54 | build_windows: 55 | name: Windows 56 | runs-on: windows-latest 57 | steps: 58 | - uses: laytan/setup-odin@v2 59 | with: 60 | token: ${{ secrets.GITHUB_TOKEN }} 61 | release: false 62 | 63 | - uses: mymindstorm/setup-emsdk@v14 64 | 65 | - uses: actions/checkout@v4 66 | - uses: ilammy/msvc-dev-cmd@v1 67 | 68 | - name: Build desktop 69 | run: .\build_desktop.bat 70 | 71 | - name: Build web 72 | run: .\build_web.bat 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | build/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Karl Zylinski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Odin + Raylib on the web 2 | ![image](https://github.com/user-attachments/assets/a104c6f4-8789-415d-a9af-c8ff2e9458ec) 3 | 4 | Make games using Odin + Raylib that works in browser and on desktop. 5 | 6 | Live example: https://zylinski.se/odin-raylib-web/ 7 | 8 | ## Requirements 9 | 10 | - **Emscripten**. Follow instructions here: https://emscripten.org/docs/getting_started/downloads.html (the stuff under "Installation instructions using the emsdk (recommended)"). 11 | - **Recent Odin compiler**: This uses Raylib binding changes that were done on January 1, 2025. 12 | 13 | ## Getting started 14 | 15 | 1. Point `EMSCRIPTEN_SDK_DIR` in `build_web.bat/sh` to where you installed emscripten. 16 | 2. Run `build_web.bat/sh`. 17 | 3. Web game is in the `build/web` folder. 18 | 19 | > [!NOTE] 20 | > `build_web.bat` is for windows, `build_web.sh` is for Linux / macOS. 21 | 22 | > [!WARNING] 23 | > You can't run `build/web/index.html` directly due to "CORS policy" javascript errors. You can work around that by running a small python web server: 24 | > - Go to `build/web` in a console. 25 | > - Run `python -m http.server` 26 | > - Go to `localhost:8000` in your browser. 27 | > 28 | > _For those who don't have python: Emscripten comes with it. See the `python` folder in your emscripten installation directory._ 29 | 30 | Build a desktop executable using `build_desktop.bat/sh`. It will end up in the `build/desktop` folder. 31 | 32 | Put any assets (textures, sounds etc) into the `assets` folder. Emscripten will merge those into the web build. For desktop builds, the `assets` folder is copied to the `build/desktop` folder. 33 | 34 | ## What works 35 | 36 | - Use raylib, raygui, rlgl using the default `vendor:raylib` bindings. 37 | - Allocator that works with maps and SIMD (uses emcripten's `malloc`). 38 | - Temp allocator. 39 | - Logger. 40 | - `fmt.println` etc 41 | - There's a wrapper for `read_entire_file` and `write_entire_file` from `core:os` that can files from `assets` directory, even on web. See `source/utils.odin` 42 | 43 | > [!NOTE] 44 | > Files written using `write_entire_file` don't exist outside the browser. They don't survive closing the tab. But you can write a file and load it within the same session. You can use it to make your old desktop code run, even though it won't be possible to _really_ save anything. 45 | 46 | ## Debugging 47 | 48 | I recommend debugging the desktop build when you can (add `-debug` inside `build_desktop.bat/sh` and use for example [RAD Debugger](https://github.com/EpicGamesExt/raddebugger)). For web-only bugs, you can add `-g` to the the `emcc` line in the build script. This gives you better crash callstacks. It works in Chrome, not so much in Firefox. 49 | 50 | ## Sublime Text 51 | 52 | There is a Sublime project file: `project.sublime-project`. It has a build system that lets you run the web and desktop build scripts. 53 | 54 | ## Web build in my Hot Reload template 55 | 56 | My Odin + Raylib + Hot Reload template has been updated with similar capabilities: https://github.com/karl-zylinski/odin-raylib-hot-reload-game-template -- Note: It's just for making a _release web build_, no web hot reloading is supported! 57 | 58 | ## How does the web build work? 59 | 60 | Start by looking at `build_web.bat/sh` and see how it uses both the Odin compiler and the emscripten compiler (`emcc`). Raylib requires `emcc` to (among other things) translate OpenGL to WebGL calls. Also see `source/main_web/index_template.html` (used as template for `build/web/index/html`). That HTML file contains javascript that calls the entry-point procedures you'll find in `source/main_web/main_web.odin`. It's a bit special in the way that it sets our Odin stuff up within a callback that comes from emscripten (`instantiateWasm`). 61 | 62 | ## Troubleshooting 63 | 64 | ### I get `panic: wasm_allocator: initial memory could not be allocated` 65 | 66 | You probably have a global variable that allocates dynamic memory. Move that allocation into the game's `init` proc. This could also happen if initialize dynamic arrays or maps in the global file scope, like so: 67 | 68 | ``` 69 | arr := [dynamic]int { 2, 3, 4 } 70 | ``` 71 | 72 | In that case you can declare it and do the initialization in the `init` proc instead: 73 | 74 | ``` 75 | arr: [dynamic]int 76 | 77 | init :: proc() { 78 | arr = { 2, 3, 4 } 79 | } 80 | ``` 81 | 82 | This happens because the context hasn't been initialized with the correct allocator yet. 83 | 84 | The error can also happen if you import some package that does allocations in a procedure tagged with `@(init)`. Once such page is `core:math/big`. That package is in turn imported by `core:encoding/cbor`. So if you import cbor you may get this error. If you need cbor, then you you can try, as a work-around, to run the Odin compiler with the `-default-to-nil-allocator` option. That may break the `core:math/big` package, but you might not need it. 85 | 86 | ### I get `RuntimeError: memory access out of bounds` 87 | 88 | Try modifying the `build_web` script and add these flags where it runs `emcc`: 89 | ``` 90 | -sALLOW_MEMORY_GROWTH=1 -sINITIAL_HEAP=16777216 -sSTACK_SIZE=65536 91 | ``` 92 | The numbers `16777216` and `65536` above are the default values, try bigger ones and see if it helps. 93 | 94 | ### I load assets from more folders than the `assets` folder 95 | Add an additional `--preload-file folder_name` option to the build script when it runs `emcc`. Then that folder will become part of the web build. 96 | 97 | ### I can only use `#version 100` shaders 98 | The raylib libraries that come with Odin can only use version 100. That's the default for raylib. But you can recompile raylib so that shaders with version `300 es` works. That's a fairly modern version. It's mostly the same as the common `330` version. 99 | 100 | You'll need to recompile the raylib WASM binaries. That means you need to download the raylib source from here: https://github.com/raysan5/raylib 101 | 102 | When you've downloaded it you need to compile raylib with OpenGL ES3 support. Something like this: 103 | 104 | ``` 105 | make clean 106 | make PLATFORM=PLATFORM_WEB GRAPHICS=GRAPHICS_API_OPENGL_ES3 -B 107 | ``` 108 | You might also be able to use `mingw32-make` instead of `make` on Windows, if you have mingw installed. You may also be able to invoke `emcc` manually, but you'll have to look through the makefiles to see what commands you need to run. 109 | 110 | You'll need to copy the outputted wasm libs from this build to your raylib bindings, overwriting the old raylib WASM library files. 111 | 112 | When building your game, you need to modify the build script. When it runs `emcc`, add the following: `-sFULL_ES3=1`. 113 | 114 | You can now use the `300 es` version of shaders. Make sure the shaders have this at the top: 115 | ``` 116 | #version 300 es 117 | precision highp float; 118 | ``` 119 | 120 | Thanks to lucy for figuring this stuff out. 121 | 122 | ### Error: `emcc: error: build\web\index.data --from-emcc --preload assets' failed (returned 1)` 123 | You might be missing the `assets` folder. It must have at least a single file inside it. You can also remove the `--preload-file assets` from the build script. 124 | 125 | ## Questions? 126 | 127 | Talk to me on my Discord server: https://discord.gg/4FsHgtBmFK 128 | 129 | ## Acknowledgements 130 | Tyzor on the Odin Discord helped me with using `js_wasm32` instead of `freestanding_wasm32`. 131 | 132 | [Caedo's repository](https://github.com/Caedo/raylib_wasm_odin) and [Aronicu's repository](https://github.com/Aronicu/Raylib-WASM) helped me with: 133 | - The initial emscripten setup 134 | - The logger setup 135 | - The idea of using python to host a server 136 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | This folder must have a least a single file, otherwise the web build fails. You can also remove the `--preload-file assets` if you do not need the `assets` folder. -------------------------------------------------------------------------------- /assets/long_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karl-zylinski/odin-raylib-web/e30a1b0d99d868cde4969420091cba30f9d5bb99/assets/long_cat.png -------------------------------------------------------------------------------- /assets/round_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karl-zylinski/odin-raylib-web/e30a1b0d99d868cde4969420091cba30f9d5bb99/assets/round_cat.png -------------------------------------------------------------------------------- /build_desktop.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set OUT_DIR=build\desktop 4 | if not exist %OUT_DIR% mkdir %OUT_DIR% 5 | 6 | odin build source\main_desktop -vet -strict-style -out:%OUT_DIR%\game_desktop.exe 7 | IF %ERRORLEVEL% NEQ 0 exit /b 1 8 | 9 | xcopy /y /e /i assets %OUT_DIR%\assets >nul 10 | IF %ERRORLEVEL% NEQ 0 exit /b 1 11 | 12 | echo Desktop build created in %OUT_DIR% -------------------------------------------------------------------------------- /build_desktop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | OUT_DIR="build/desktop" 4 | mkdir -p $OUT_DIR 5 | odin build source/main_desktop -out:$OUT_DIR/game_desktop.bin 6 | cp -R ./assets/ ./$OUT_DIR/assets/ 7 | echo "Desktop build created in ${OUT_DIR}" -------------------------------------------------------------------------------- /build_web.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Point this to where you installed emscripten. 4 | set EMSCRIPTEN_SDK_DIR=c:\SDK\emsdk 5 | set OUT_DIR=build\web 6 | 7 | if not exist %OUT_DIR% mkdir %OUT_DIR% 8 | 9 | set EMSDK_QUIET=1 10 | call %EMSCRIPTEN_SDK_DIR%\emsdk_env.bat 11 | 12 | :: Note RAYLIB_WASM_LIB=env.o -- env.o is an internal WASM object file. You can 13 | :: see how RAYLIB_WASM_LIB is used inside /vendor/raylib/raylib.odin. 14 | :: 15 | :: The emcc call will be fed the actual raylib library file. That stuff will end 16 | :: up in env.o 17 | :: 18 | :: Note that there is a rayGUI equivalent: -define:RAYGUI_WASM_LIB=env.o 19 | odin build source\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 20 | IF %ERRORLEVEL% NEQ 0 exit /b 1 21 | 22 | for /f %%i in ('odin root') do set "ODIN_PATH=%%i" 23 | 24 | copy %ODIN_PATH%\core\sys\wasm\js\odin.js %OUT_DIR% 25 | 26 | set files=%OUT_DIR%\game.wasm.o %ODIN_PATH%\vendor\raylib\wasm\libraylib.a %ODIN_PATH%\vendor\raylib\wasm\libraygui.a 27 | 28 | :: index_template.html contains the javascript code that calls the procedures in 29 | :: source/main_web/main_web.odin 30 | set flags=-sUSE_GLFW=3 -sWASM_BIGINT -sWARN_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS --shell-file source\main_web\index_template.html --preload-file assets 31 | 32 | :: For debugging: Add `-g` to `emcc` (gives better error callstack in chrome) 33 | :: 34 | :: This uses `cmd /c` to avoid emcc stealing the whole command prompt. Otherwise 35 | :: it does not run the lines that follow it. 36 | cmd /c emcc -o %OUT_DIR%\index.html %files% %flags% 37 | 38 | del %OUT_DIR%\game.wasm.o 39 | 40 | echo Web build created in %OUT_DIR% -------------------------------------------------------------------------------- /build_web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Point this to where you installed emscripten. Optional on systems that already 4 | # have `emcc` in the path. 5 | EMSCRIPTEN_SDK_DIR="$HOME/repos/emsdk" 6 | OUT_DIR="build/web" 7 | 8 | mkdir -p $OUT_DIR 9 | 10 | export EMSDK_QUIET=1 11 | [[ -f "$EMSCRIPTEN_SDK_DIR/emsdk_env.sh" ]] && . "$EMSCRIPTEN_SDK_DIR/emsdk_env.sh" 12 | 13 | # Note RAYLIB_WASM_LIB=env.o -- env.o is an internal WASM object file. You can 14 | # see how RAYLIB_WASM_LIB is used inside /vendor/raylib/raylib.odin. 15 | # 16 | # The emcc call will be fed the actual raylib library file. That stuff will end 17 | # up in env.o 18 | # 19 | # Note that there is a rayGUI equivalent: -define:RAYGUI_WASM_LIB=env.o 20 | odin build source/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 21 | 22 | ODIN_PATH=$(odin root) 23 | 24 | cp $ODIN_PATH/core/sys/wasm/js/odin.js $OUT_DIR 25 | 26 | files="$OUT_DIR/game.wasm.o ${ODIN_PATH}/vendor/raylib/wasm/libraylib.a ${ODIN_PATH}/vendor/raylib/wasm/libraygui.a" 27 | 28 | # index_template.html contains the javascript code that calls the procedures in 29 | # source/main_web/main_web.odin 30 | flags="-sUSE_GLFW=3 -sWASM_BIGINT -sWARN_ON_UNDEFINED_SYMBOLS=0 -sASSERTIONS --shell-file source/main_web/index_template.html --preload-file assets" 31 | 32 | # For debugging: Add `-g` to `emcc` (gives better error callstack in chrome) 33 | emcc -o $OUT_DIR/index.html $files $flags 34 | 35 | rm $OUT_DIR/game.wasm.o 36 | 37 | echo "Web build created in ${OUT_DIR}" -------------------------------------------------------------------------------- /project.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | }, 7 | ], 8 | "build_systems": 9 | [ 10 | { 11 | "file_regex": "^(.+)\\(([0-9]+):([0-9]+)\\) (.+)$", 12 | "name": "Odin + Raylib on the web", 13 | "working_dir": "$project_path", 14 | 15 | "windows": { 16 | "cmd": "build_web.bat", 17 | }, 18 | "linux": { 19 | "cmd": "./build_web.sh", 20 | }, 21 | "osx": { 22 | "cmd": "./build_web.sh", 23 | }, 24 | 25 | "variants": [ 26 | { 27 | "name": "desktop + run", 28 | "windows": { 29 | "cmd": "build_desktop.bat && build\\desktop\\game_desktop.exe", 30 | }, 31 | "linux": { 32 | "shell_cmd": "./build_desktop.sh && build/desktop/game_desktop.bin", 33 | }, 34 | "osx": { 35 | "shell_cmd": "./build_desktop.sh && build/desktop/game_desktop.bin", 36 | }, 37 | 38 | }, 39 | ], 40 | } 41 | ], 42 | "settings": 43 | { 44 | "auto_complete": false, 45 | "LSP": 46 | { 47 | "odin": 48 | { 49 | "enabled": true, 50 | }, 51 | }, 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /source/game.odin: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import rl "vendor:raylib" 4 | import "core:log" 5 | import "core:fmt" 6 | import "core:c" 7 | 8 | run: bool 9 | texture: rl.Texture 10 | texture2: rl.Texture 11 | texture2_rot: f32 12 | 13 | init :: proc() { 14 | run = true 15 | rl.SetConfigFlags({.WINDOW_RESIZABLE, .VSYNC_HINT}) 16 | rl.InitWindow(1280, 720, "Odin + Raylib on the web") 17 | 18 | // Anything in `assets` folder is available to load. 19 | texture = rl.LoadTexture("assets/round_cat.png") 20 | 21 | // A different way of loading a texture: using `read_entire_file` that works 22 | // both on desktop and web. Note: You can import `core:os` and use 23 | // `os.read_entire_file`. But that won't work on web. Emscripten has a way 24 | // to bundle files into the build, and we access those using this 25 | // special `read_entire_file`. 26 | if long_cat_data, long_cat_ok := read_entire_file("assets/long_cat.png", context.temp_allocator); long_cat_ok { 27 | long_cat_img := rl.LoadImageFromMemory(".png", raw_data(long_cat_data), c.int(len(long_cat_data))) 28 | texture2 = rl.LoadTextureFromImage(long_cat_img) 29 | rl.UnloadImage(long_cat_img) 30 | } 31 | } 32 | 33 | update :: proc() { 34 | rl.BeginDrawing() 35 | rl.ClearBackground({0, 120, 153, 255}) 36 | { 37 | texture2_rot += rl.GetFrameTime()*50 38 | source_rect := rl.Rectangle { 39 | 0, 0, 40 | f32(texture2.width), f32(texture2.height), 41 | } 42 | dest_rect := rl.Rectangle { 43 | 300, 220, 44 | f32(texture2.width)*5, f32(texture2.height)*5, 45 | } 46 | rl.DrawTexturePro(texture2, source_rect, dest_rect, {dest_rect.width/2, dest_rect.height/2}, texture2_rot, rl.WHITE) 47 | } 48 | rl.DrawTextureEx(texture, rl.GetMousePosition(), 0, 5, rl.WHITE) 49 | rl.DrawRectangleRec({0, 0, 220, 130}, rl.BLACK) 50 | rl.GuiLabel({10, 10, 200, 20}, "raygui works!") 51 | 52 | if rl.GuiButton({10, 30, 200, 20}, "Print to log (see console)") { 53 | log.info("log.info works!") 54 | fmt.println("fmt.println too.") 55 | } 56 | 57 | if rl.GuiButton({10, 60, 200, 20}, "Source code (opens GitHub)") { 58 | rl.OpenURL("https://github.com/karl-zylinski/odin-raylib-web") 59 | } 60 | 61 | if rl.GuiButton({10, 90, 200, 20}, "Quit") { 62 | run = false 63 | } 64 | 65 | rl.EndDrawing() 66 | 67 | // Anything allocated using temp allocator is invalid after this. 68 | free_all(context.temp_allocator) 69 | } 70 | 71 | // In a web build, this is called when browser changes size. Remove the 72 | // `rl.SetWindowSize` call if you don't want a resizable game. 73 | parent_window_size_changed :: proc(w, h: int) { 74 | rl.SetWindowSize(c.int(w), c.int(h)) 75 | } 76 | 77 | shutdown :: proc() { 78 | rl.CloseWindow() 79 | } 80 | 81 | should_run :: proc() -> bool { 82 | when ODIN_OS != .JS { 83 | // Never run this proc in browser. It contains a 16 ms sleep on web! 84 | if rl.WindowShouldClose() { 85 | run = false 86 | } 87 | } 88 | 89 | return run 90 | } -------------------------------------------------------------------------------- /source/main_desktop/main_desktop.odin: -------------------------------------------------------------------------------- 1 | package main_desktop 2 | 3 | import "core:log" 4 | import "core:os" 5 | import "core:path/filepath" 6 | import game ".." 7 | 8 | main :: proc() { 9 | // Set working dir to dir of executable. 10 | exe_path := os.args[0] 11 | exe_dir := filepath.dir(string(exe_path), context.temp_allocator) 12 | os.set_current_directory(exe_dir) 13 | 14 | context.logger = log.create_console_logger() 15 | 16 | game.init() 17 | 18 | for game.should_run() { 19 | game.update() 20 | } 21 | 22 | game.shutdown() 23 | } -------------------------------------------------------------------------------- /source/main_web/emscripten_allocator.odin: -------------------------------------------------------------------------------- 1 | /* 2 | This allocator uses the malloc, calloc, free and realloc procs that emscripten 3 | exposes in order to allocate memory. Just like Odin's default heap allocator 4 | this uses proper alignment, so that maps and simd works. 5 | */ 6 | 7 | package main_web 8 | 9 | import "core:mem" 10 | import "core:c" 11 | import "base:intrinsics" 12 | 13 | // This will create bindings to emscripten's implementation of libc 14 | // memory allocation features. 15 | @(default_calling_convention = "c") 16 | foreign { 17 | calloc :: proc(num, size: c.size_t) -> rawptr --- 18 | free :: proc(ptr: rawptr) --- 19 | malloc :: proc(size: c.size_t) -> rawptr --- 20 | realloc :: proc(ptr: rawptr, size: c.size_t) -> rawptr --- 21 | } 22 | 23 | emscripten_allocator :: proc "contextless" () -> mem.Allocator { 24 | return mem.Allocator{emscripten_allocator_proc, nil} 25 | } 26 | 27 | emscripten_allocator_proc :: proc( 28 | allocator_data: rawptr, 29 | mode: mem.Allocator_Mode, 30 | size, alignment: int, 31 | old_memory: rawptr, 32 | old_size: int, 33 | location := #caller_location 34 | ) -> (data: []byte, err: mem.Allocator_Error) { 35 | // These aligned alloc procs are almost indentical those in 36 | // `_heap_allocator_proc` in `core:os`. Without the proper alignment you 37 | // cannot use maps and simd features. 38 | 39 | aligned_alloc :: proc(size, alignment: int, zero_memory: bool, old_ptr: rawptr = nil) -> ([]byte, mem.Allocator_Error) { 40 | a := max(alignment, align_of(rawptr)) 41 | space := size + a - 1 42 | 43 | allocated_mem: rawptr 44 | if old_ptr != nil { 45 | original_old_ptr := mem.ptr_offset((^rawptr)(old_ptr), -1)^ 46 | allocated_mem = realloc(original_old_ptr, c.size_t(space+size_of(rawptr))) 47 | } else if zero_memory { 48 | // calloc automatically zeros memory, but it takes a number + size 49 | // instead of just size. 50 | allocated_mem = calloc(c.size_t(space+size_of(rawptr)), 1) 51 | } else { 52 | allocated_mem = malloc(c.size_t(space+size_of(rawptr))) 53 | } 54 | aligned_mem := rawptr(mem.ptr_offset((^u8)(allocated_mem), size_of(rawptr))) 55 | 56 | ptr := uintptr(aligned_mem) 57 | aligned_ptr := (ptr - 1 + uintptr(a)) & -uintptr(a) 58 | diff := int(aligned_ptr - ptr) 59 | if (size + diff) > space || allocated_mem == nil { 60 | return nil, .Out_Of_Memory 61 | } 62 | 63 | aligned_mem = rawptr(aligned_ptr) 64 | mem.ptr_offset((^rawptr)(aligned_mem), -1)^ = allocated_mem 65 | 66 | return mem.byte_slice(aligned_mem, size), nil 67 | } 68 | 69 | aligned_free :: proc(p: rawptr) { 70 | if p != nil { 71 | free(mem.ptr_offset((^rawptr)(p), -1)^) 72 | } 73 | } 74 | 75 | aligned_resize :: proc(p: rawptr, old_size: int, new_size: int, new_alignment: int) -> ([]byte, mem.Allocator_Error) { 76 | if p == nil { 77 | return nil, nil 78 | } 79 | return aligned_alloc(new_size, new_alignment, true, p) 80 | } 81 | 82 | switch mode { 83 | case .Alloc: 84 | return aligned_alloc(size, alignment, true) 85 | 86 | case .Alloc_Non_Zeroed: 87 | return aligned_alloc(size, alignment, false) 88 | 89 | case .Free: 90 | aligned_free(old_memory) 91 | return nil, nil 92 | 93 | case .Resize: 94 | if old_memory == nil { 95 | return aligned_alloc(size, alignment, true) 96 | } 97 | 98 | bytes := aligned_resize(old_memory, old_size, size, alignment) or_return 99 | 100 | // realloc doesn't zero the new bytes, so we do it manually. 101 | if size > old_size { 102 | new_region := raw_data(bytes[old_size:]) 103 | intrinsics.mem_zero(new_region, size - old_size) 104 | } 105 | 106 | return bytes, nil 107 | 108 | case .Resize_Non_Zeroed: 109 | if old_memory == nil { 110 | return aligned_alloc(size, alignment, false) 111 | } 112 | 113 | return aligned_resize(old_memory, old_size, size, alignment) 114 | 115 | case .Query_Features: 116 | set := (^mem.Allocator_Mode_Set)(old_memory) 117 | if set != nil { 118 | set^ = {.Alloc, .Free, .Resize, .Query_Features} 119 | } 120 | return nil, nil 121 | 122 | case .Free_All, .Query_Info: 123 | return nil, .Mode_Not_Implemented 124 | } 125 | return nil, .Mode_Not_Implemented 126 | } 127 | -------------------------------------------------------------------------------- /source/main_web/emscripten_logger.odin: -------------------------------------------------------------------------------- 1 | /* 2 | This logger is largely a copy of the console logger in `core:log`, but it uses 3 | emscripten's `puts` proc to write into he console of the web browser. 4 | 5 | This is more or less identical to the logger in Aronicu's repository: 6 | https://github.com/Aronicu/Raylib-WASM/tree/main 7 | */ 8 | 9 | package main_web 10 | 11 | import "core:c" 12 | import "core:fmt" 13 | import "core:log" 14 | import "core:strings" 15 | 16 | Emscripten_Logger_Opts :: log.Options{.Level, .Short_File_Path, .Line} 17 | 18 | create_emscripten_logger :: proc (lowest := log.Level.Debug, opt := Emscripten_Logger_Opts) -> log.Logger { 19 | return log.Logger{data = nil, procedure = logger_proc, lowest_level = lowest, options = opt} 20 | } 21 | 22 | // This create's a binding to `puts` which will be linked in as part of the 23 | // emscripten runtime. 24 | @(default_calling_convention = "c") 25 | foreign { 26 | puts :: proc(buffer: cstring) -> c.int --- 27 | } 28 | 29 | @(private="file") 30 | logger_proc :: proc( 31 | logger_data: rawptr, 32 | level: log.Level, 33 | text: string, 34 | options: log.Options, 35 | location := #caller_location 36 | ) { 37 | b := strings.builder_make(context.temp_allocator) 38 | strings.write_string(&b, Level_Headers[level]) 39 | do_location_header(options, &b, location) 40 | fmt.sbprint(&b, text) 41 | 42 | if bc, bc_err := strings.to_cstring(&b); bc_err == nil { 43 | puts(bc) 44 | } 45 | } 46 | 47 | @(private="file") 48 | Level_Headers := [?]string { 49 | 0 ..< 10 = "[DEBUG] --- ", 50 | 10 ..< 20 = "[INFO ] --- ", 51 | 20 ..< 30 = "[WARN ] --- ", 52 | 30 ..< 40 = "[ERROR] --- ", 53 | 40 ..< 50 = "[FATAL] --- ", 54 | } 55 | 56 | @(private="file") 57 | do_location_header :: proc(opts: log.Options, buf: ^strings.Builder, location := #caller_location) { 58 | if log.Location_Header_Opts & opts == nil { 59 | return 60 | } 61 | fmt.sbprint(buf, "[") 62 | file := location.file_path 63 | if .Short_File_Path in opts { 64 | last := 0 65 | for r, i in location.file_path { 66 | if r == '/' { 67 | last = i + 1 68 | } 69 | } 70 | file = location.file_path[last:] 71 | } 72 | 73 | if log.Location_File_Opts & opts != nil { 74 | fmt.sbprint(buf, file) 75 | } 76 | if .Line in opts { 77 | if log.Location_File_Opts & opts != nil { 78 | fmt.sbprint(buf, ":") 79 | } 80 | fmt.sbprint(buf, location.line) 81 | } 82 | 83 | if .Procedure in opts { 84 | if (log.Location_File_Opts | {.Line}) & opts != nil { 85 | fmt.sbprint(buf, ":") 86 | } 87 | fmt.sbprintf(buf, "%s()", location.procedure) 88 | } 89 | 90 | fmt.sbprint(buf, "] ") 91 | } 92 | -------------------------------------------------------------------------------- /source/main_web/index_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Odin + Raylib on the web 8 | 9 | 10 | 11 | 12 | 28 | 29 | 30 | 31 | 32 | 110 | 111 | 112 | {{{ SCRIPT }}} 113 | 114 | 115 | -------------------------------------------------------------------------------- /source/main_web/main_web.odin: -------------------------------------------------------------------------------- 1 | // These procs are the ones that will be called from `index.html`, which is 2 | // generated from `index_template.html`. 3 | 4 | package main_web 5 | 6 | import "base:runtime" 7 | import "core:c" 8 | import "core:mem" 9 | import game ".." 10 | 11 | @(private="file") 12 | web_context: runtime.Context 13 | 14 | @export 15 | main_start :: proc "c" () { 16 | context = runtime.default_context() 17 | 18 | // The WASM allocator doesn't seem to work properly in combination with 19 | // emscripten. There is some kind of conflict with how the manage memory. 20 | // So this sets up an allocator that uses emscripten's malloc. 21 | context.allocator = emscripten_allocator() 22 | runtime.init_global_temporary_allocator(1*mem.Megabyte) 23 | 24 | // Since we now use js_wasm32 we should be able to remove this and use 25 | // context.logger = log.create_console_logger(). However, that one produces 26 | // extra newlines on web. So it's a bug in that core lib. 27 | context.logger = create_emscripten_logger() 28 | 29 | web_context = context 30 | 31 | game.init() 32 | } 33 | 34 | @export 35 | main_update :: proc "c" () -> bool { 36 | context = web_context 37 | game.update() 38 | return game.should_run() 39 | } 40 | 41 | @export 42 | main_end :: proc "c" () { 43 | context = web_context 44 | game.shutdown() 45 | } 46 | 47 | @export 48 | web_window_size_changed :: proc "c" (w: c.int, h: c.int) { 49 | context = web_context 50 | game.parent_window_size_changed(int(w), int(h)) 51 | } -------------------------------------------------------------------------------- /source/utils.odin: -------------------------------------------------------------------------------- 1 | // Wraps os.read_entire_file and os.write_entire_file, but they also work with emscripten. 2 | 3 | package game 4 | 5 | @(require_results) 6 | read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) { 7 | return _read_entire_file(name, allocator, loc) 8 | } 9 | 10 | write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) { 11 | return _write_entire_file(name, data, truncate) 12 | } -------------------------------------------------------------------------------- /source/utils_default.odin: -------------------------------------------------------------------------------- 1 | #+build !wasm32 2 | #+build !wasm64p32 3 | 4 | package game 5 | 6 | import "core:os" 7 | 8 | _read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) { 9 | return os.read_entire_file(name, allocator, loc) 10 | } 11 | 12 | _write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) { 13 | return os.write_entire_file(name, data, truncate) 14 | } -------------------------------------------------------------------------------- /source/utils_web.odin: -------------------------------------------------------------------------------- 1 | // Implementations of `read_entire_file` and `write_entire_file` using the libc 2 | // stuff emscripten exposes. You can read the files that get bundled by 3 | // `--preload-file assets` in `build_web` script. 4 | 5 | #+build wasm32, wasm64p32 6 | 7 | package game 8 | 9 | import "base:runtime" 10 | import "core:log" 11 | import "core:c" 12 | import "core:strings" 13 | 14 | // These will be linked in by emscripten. 15 | @(default_calling_convention = "c") 16 | foreign { 17 | fopen :: proc(filename, mode: cstring) -> ^FILE --- 18 | fseek :: proc(stream: ^FILE, offset: c.long, whence: Whence) -> c.int --- 19 | ftell :: proc(stream: ^FILE) -> c.long --- 20 | fclose :: proc(stream: ^FILE) -> c.int --- 21 | fread :: proc(ptr: rawptr, size: c.size_t, nmemb: c.size_t, stream: ^FILE) -> c.size_t --- 22 | fwrite :: proc(ptr: rawptr, size: c.size_t, nmemb: c.size_t, stream: ^FILE) -> c.size_t --- 23 | } 24 | 25 | @(private="file") 26 | FILE :: struct {} 27 | 28 | Whence :: enum c.int { 29 | SET, 30 | CUR, 31 | END, 32 | } 33 | 34 | // Similar to raylib's LoadFileData 35 | _read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) { 36 | if name == "" { 37 | log.error("No file name provided") 38 | return 39 | } 40 | 41 | file := fopen(strings.clone_to_cstring(name, context.temp_allocator), "rb") 42 | 43 | if file == nil { 44 | log.errorf("Failed to open file %v", name) 45 | return 46 | } 47 | 48 | defer fclose(file) 49 | 50 | fseek(file, 0, .END) 51 | size := ftell(file) 52 | fseek(file, 0, .SET) 53 | 54 | if size <= 0 { 55 | log.errorf("Failed to read file %v", name) 56 | return 57 | } 58 | 59 | data_err: runtime.Allocator_Error 60 | data, data_err = make([]byte, size, allocator, loc) 61 | 62 | if data_err != nil { 63 | log.errorf("Error allocating memory: %v", data_err) 64 | return 65 | } 66 | 67 | read_size := fread(raw_data(data), 1, c.size_t(size), file) 68 | 69 | if read_size != c.size_t(size) { 70 | log.warnf("File %v partially loaded (%i bytes out of %i)", name, read_size, size) 71 | } 72 | 73 | log.debugf("Successfully loaded %v", name) 74 | return data, true 75 | } 76 | 77 | // Similar to raylib's SaveFileData. 78 | // 79 | // Note: This can save during the current session, but I don't think you can 80 | // save any data between sessions. So when you close the tab your saved files 81 | // are gone. Perhaps you could communicate back to emscripten and save a cookie. 82 | // Or communicate with a server and tell it to save data. 83 | _write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) { 84 | if name == "" { 85 | log.error("No file name provided") 86 | return 87 | } 88 | 89 | file := fopen(strings.clone_to_cstring(name, context.temp_allocator), truncate ? "wb" : "ab") 90 | defer fclose(file) 91 | 92 | if file == nil { 93 | log.errorf("Failed to open '%v' for writing", name) 94 | return 95 | } 96 | 97 | bytes_written := fwrite(raw_data(data), 1, len(data), file) 98 | 99 | if bytes_written == 0 { 100 | log.errorf("Failed to write file %v", name) 101 | return 102 | } else if bytes_written != len(data) { 103 | log.errorf("File partially written, wrote %v out of %v bytes", bytes_written, len(data)) 104 | return 105 | } 106 | 107 | log.debugf("File written successfully: %v", name) 108 | return true 109 | } --------------------------------------------------------------------------------