├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── assets └── round_cat.png ├── .gitignore ├── project.sublime-project ├── source ├── utils.odin ├── shader.glsl ├── main_web │ ├── main_web.odin │ └── emscripten_allocator.odin ├── main_release │ └── main_release.odin ├── web │ ├── emscripten_file_io.odin │ └── index_template.html ├── main_hot_reload │ └── main_hot_reload.odin └── game.odin ├── LICENSE ├── .github └── workflows │ └── build.yml └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug.allowBreakpointsEverywhere": true, 3 | } -------------------------------------------------------------------------------- /assets/round_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karl-zylinski/odin-sokol-hot-reload-template/HEAD/assets/round_cat.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | project.sublime-workspace 3 | game_hot_reload.exe 4 | game_hot_reload.bin 5 | linux 6 | log.txt 7 | dylib/ 8 | *.rdi 9 | *.pdb 10 | sokol-shdc 11 | source/sokol 12 | sokol_dll* 13 | 14 | # generated shaders are prefixed with gen__ so they can be ignored 15 | gen__*.odin -------------------------------------------------------------------------------- /project.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | }, 7 | ], 8 | "build_systems": 9 | [ 10 | { 11 | "name": "Odin + Sokol + Hot Reload template", 12 | "working_dir": "$project_path", 13 | "shell_cmd": "python build.py -hot-reload -run -debug", 14 | 15 | // This makes sublime able to jump to build errors. 16 | "file_regex": "^(.+)\\(([0-9]+):([0-9]+)\\) (.+)$", 17 | 18 | "variants": [ 19 | { 20 | "name": "release", 21 | "shell_cmd": "python build.py -release -run", 22 | }, 23 | { 24 | "name": "web", 25 | "shell_cmd": "python build.py -web", 26 | }, 27 | ], 28 | } 29 | ], 30 | "settings": 31 | { 32 | "LSP": 33 | { 34 | "odin": 35 | { 36 | "enabled": true, 37 | }, 38 | }, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /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 | import "core:os" 6 | import "web" 7 | 8 | _ :: os 9 | _ :: web 10 | 11 | IS_WEB :: ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 12 | 13 | @(require_results) 14 | read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) { 15 | when IS_WEB { 16 | return web.read_entire_file(name, allocator, loc) 17 | } else { 18 | return os.read_entire_file(name, allocator, loc) 19 | } 20 | } 21 | 22 | write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) { 23 | when IS_WEB { 24 | return web.write_entire_file(name, data, truncate) 25 | } else { 26 | return os.write_entire_file(name, data, truncate) 27 | } 28 | } -------------------------------------------------------------------------------- /source/shader.glsl: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // Shader code for texcube-sapp sample. 3 | // 4 | // NOTE: This source file also uses the '#pragma sokol' form of the 5 | // custom tags. 6 | //------------------------------------------------------------------------------ 7 | @header package game 8 | @header import sg "sokol/gfx" 9 | @ctype mat4 Mat4 10 | 11 | @vs vs 12 | layout(binding=0) uniform vs_params { 13 | mat4 mvp; 14 | }; 15 | 16 | in vec4 pos; 17 | in vec4 color0; 18 | in vec2 texcoord0; 19 | 20 | out vec4 color; 21 | out vec2 uv; 22 | 23 | void main() { 24 | gl_Position = mvp * pos; 25 | color = color0; 26 | uv = texcoord0; 27 | } 28 | @end 29 | 30 | @fs fs 31 | layout(binding=0) uniform texture2D tex; 32 | layout(binding=0) uniform sampler smp; 33 | 34 | in vec4 color; 35 | in vec2 uv; 36 | out vec4 frag_color; 37 | 38 | void main() { 39 | frag_color = texture(sampler2D(tex, smp), uv) * color; 40 | } 41 | @end 42 | 43 | @program texcube vs fs 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 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. -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "", 4 | "args": [], 5 | "tasks": [ 6 | { 7 | "label": "Build Debug", 8 | "type": "shell", 9 | "command": "python ${workspaceFolder}/build.py -debug", 10 | "group": "build" 11 | }, 12 | { 13 | "label": "Build Release", 14 | "type": "shell", 15 | "command": "python ${workspaceFolder}/build.py -release", 16 | "group": "build" 17 | }, 18 | { 19 | "label": "Build Web", 20 | "type": "shell", 21 | "command": "python ${workspaceFolder}/build.py -web", 22 | "group": "build" 23 | }, 24 | { 25 | "label": "Build Hot Reload", 26 | "type": "shell", 27 | "command": "python ${workspaceFolder}/build.py -hot-reload", 28 | "presentation": { 29 | "echo": true, 30 | "reveal": "always", 31 | "focus": false, 32 | "panel": "shared", 33 | "showReuseMessage": false, 34 | "clear": true 35 | }, 36 | "group": { 37 | "kind": "build", 38 | "isDefault": true 39 | }, 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /source/main_web/main_web.odin: -------------------------------------------------------------------------------- 1 | /* 2 | Web build entry point. This code is executed by the javascript in 3 | build/web/index.html (created from source/web/index_template.html). 4 | */ 5 | 6 | package main_web 7 | 8 | import "core:log" 9 | import "base:runtime" 10 | 11 | import game ".." 12 | import sapp "../sokol/app" 13 | 14 | main :: proc() { 15 | // The WASM allocator doesn't work properly in combination with emscripten. 16 | // This sets up an allocator that uses emscripten's malloc. 17 | context.allocator = emscripten_allocator() 18 | 19 | // Make temp allocator use new `context.allocator` by re-initing it. 20 | runtime.init_global_temporary_allocator(1*runtime.Megabyte) 21 | 22 | context.logger = log.create_console_logger(lowest = .Info, opt = {.Level, .Short_File_Path, .Line, .Procedure}) 23 | custom_context = context 24 | 25 | app_desc := game.game_app_default_desc() 26 | app_desc.init_cb = init 27 | app_desc.frame_cb = frame 28 | app_desc.cleanup_cb = cleanup 29 | app_desc.event_cb = event 30 | 31 | // On web this will not block. Any line after this one will run immediately! 32 | // Do any on-shutdown stuff in the `cleanup` proc. 33 | sapp.run(app_desc) 34 | } 35 | 36 | custom_context: runtime.Context 37 | 38 | init :: proc "c" () { 39 | context = custom_context 40 | game.game_init() 41 | } 42 | 43 | frame :: proc "c" () { 44 | context = custom_context 45 | game.game_frame() 46 | } 47 | 48 | event :: proc "c" (e: ^sapp.Event) { 49 | context = custom_context 50 | game.game_event(e) 51 | } 52 | 53 | // Most web programs will never "quit". The tab will just close. But if you make 54 | // a web program that runs `sapp.quit()`, then this will run. 55 | cleanup :: proc "c" () { 56 | context = custom_context 57 | game.game_cleanup() 58 | log.destroy_console_logger(context.logger) 59 | 60 | // This runs any procedure tagged with `@fini`. 61 | runtime._cleanup_runtime() 62 | } 63 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // Windows configs (only difference from linux/mac is "type" and "program") 5 | { 6 | "type": "cppvsdbg", 7 | "request": "launch", 8 | "preLaunchTask": "Build Hot Reload", 9 | "name": "Run Hot Reload (Windows)", 10 | "args": [], 11 | "cwd": "${workspaceFolder}", 12 | "program": "${workspaceFolder}/game_hot_reload.exe", 13 | }, 14 | { 15 | "type": "cppvsdbg", 16 | "request": "launch", 17 | "preLaunchTask": "Build Debug", 18 | "name": "Run Debug (Windows)", 19 | "program": "${workspaceFolder}/build/debug/game_debug.exe", 20 | "args": [], 21 | "cwd": "${workspaceFolder}" 22 | }, 23 | { 24 | "type": "cppvsdbg", 25 | "request": "launch", 26 | "preLaunchTask": "Build Release", 27 | "name": "Run Release (Windows)", 28 | "program": "${workspaceFolder}/build/release/game_release.exe", 29 | "args": [], 30 | "cwd": "${workspaceFolder}" 31 | }, 32 | 33 | // Linux / Mac configs 34 | { 35 | "type": "lldb", 36 | "request": "launch", 37 | "preLaunchTask": "Build Hot Reload", 38 | "name": "Run Hot Reload (Linux / Mac)", 39 | "args": [], 40 | "cwd": "${workspaceFolder}", 41 | "program": "${workspaceFolder}/game_hot_reload.bin", 42 | }, 43 | { 44 | "type": "lldb", 45 | "request": "launch", 46 | "preLaunchTask": "Build Debug", 47 | "name": "Run Debug (Linux / Mac)", 48 | "args": [], 49 | "cwd": "${workspaceFolder}", 50 | "program": "${workspaceFolder}/build/debug/game_debug.bin", 51 | }, 52 | { 53 | "type": "lldb", 54 | "request": "launch", 55 | "preLaunchTask": "Build Release", 56 | "name": "Run Release (Linux / Mac)", 57 | "args": [], 58 | "cwd": "${workspaceFolder}", 59 | "program": "${workspaceFolder}/build/release/game_release.bin", 60 | }, 61 | ] 62 | } -------------------------------------------------------------------------------- /source/main_release/main_release.odin: -------------------------------------------------------------------------------- 1 | /* 2 | For making a release exe that does not use hot reload. 3 | 4 | Note how this just uses a `game` package to call the game code. No DLL is loaded. 5 | */ 6 | 7 | package main_release 8 | 9 | import "core:log" 10 | import "core:os" 11 | import "core:os/os2" 12 | import "base:runtime" 13 | import "core:mem" 14 | import game ".." 15 | import sapp "../sokol/app" 16 | 17 | _ :: mem 18 | 19 | USE_TRACKING_ALLOCATOR :: #config(USE_TRACKING_ALLOCATOR, false) 20 | 21 | main :: proc() { 22 | if exe_dir, exe_dir_err := os2.get_executable_directory(context.temp_allocator); exe_dir_err == nil { 23 | os2.set_working_directory(exe_dir) 24 | } 25 | 26 | mode: int = 0 27 | when ODIN_OS == .Linux || ODIN_OS == .Darwin { 28 | mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH 29 | } 30 | 31 | logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode) 32 | 33 | if logh_err == os.ERROR_NONE { 34 | os.stdout = logh 35 | os.stderr = logh 36 | } 37 | 38 | logger_alloc := context.allocator 39 | logger := logh_err == os.ERROR_NONE ? log.create_file_logger(logh, allocator = logger_alloc) : log.create_console_logger(allocator = logger_alloc) 40 | context.logger = logger 41 | custom_context = context 42 | 43 | when USE_TRACKING_ALLOCATOR { 44 | default_allocator := context.allocator 45 | tracking_allocator: mem.Tracking_Allocator 46 | mem.tracking_allocator_init(&tracking_allocator, default_allocator) 47 | context.allocator = mem.tracking_allocator(&tracking_allocator) 48 | } 49 | 50 | app_desc := game.game_app_default_desc() 51 | 52 | app_desc.init_cb = init 53 | app_desc.frame_cb = frame 54 | app_desc.cleanup_cb = cleanup 55 | app_desc.event_cb = event 56 | 57 | sapp.run(app_desc) 58 | 59 | free_all(context.temp_allocator) 60 | 61 | when USE_TRACKING_ALLOCATOR { 62 | for _, value in tracking_allocator.allocation_map { 63 | log.errorf("%v: Leaked %v bytes\n", value.location, value.size) 64 | } 65 | 66 | mem.tracking_allocator_destroy(&tracking_allocator) 67 | } 68 | 69 | if logh_err == os.ERROR_NONE { 70 | log.destroy_file_logger(logger, logger_alloc) 71 | } 72 | } 73 | 74 | custom_context: runtime.Context 75 | 76 | init :: proc "c" () { 77 | context = custom_context 78 | game.game_init() 79 | } 80 | 81 | frame :: proc "c" () { 82 | context = custom_context 83 | game.game_frame() 84 | } 85 | 86 | event :: proc "c" (e: ^sapp.Event) { 87 | context = custom_context 88 | game.game_event(e) 89 | } 90 | 91 | cleanup :: proc "c" () { 92 | context = custom_context 93 | game.game_cleanup() 94 | } 95 | 96 | // make game use good GPU on laptops etc 97 | 98 | @(export) 99 | NvOptimusEnablement: u32 = 1 100 | 101 | @(export) 102 | AmdPowerXpressRequestHighPerformance: i32 = 1 -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: 0 20 * * * 9 | 10 | 11 | jobs: 12 | build_linux: 13 | name: Linux 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: laytan/setup-odin@v2 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | release: false 20 | 21 | - uses: mymindstorm/setup-emsdk@v14 22 | 23 | - uses: actions/checkout@v4 24 | 25 | - uses: awalsh128/cache-apt-pkgs-action@latest 26 | with: 27 | packages: libglu1-mesa-dev mesa-common-dev xorg-dev libasound2-dev 28 | version: 1.2 29 | 30 | - name: Build hot reload 31 | run: ./build.py -hot-reload 32 | 33 | - name: Build release 34 | run: ./build.py -release 35 | 36 | - name: Build web 37 | run: ./build.py -web 38 | 39 | - name: Build hot reload (debug) 40 | run: ./build.py -hot-reload -debug 41 | 42 | - name: Build release (debug) 43 | run: ./build.py -release -debug 44 | 45 | - name: Build web (debug) 46 | run: ./build.py -web -debug 47 | 48 | build_macos: 49 | name: MacOS 50 | strategy: 51 | matrix: 52 | os: [macos-15-intel, macos-15] 53 | runs-on: ${{matrix.os}} 54 | steps: 55 | - uses: laytan/setup-odin@v2 56 | with: 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | release: false 59 | 60 | - uses: mymindstorm/setup-emsdk@v14 61 | 62 | - uses: actions/checkout@v4 63 | 64 | - name: Build hot reload 65 | run: ./build.py -hot-reload 66 | 67 | - name: Build release 68 | run: ./build.py -release 69 | 70 | - name: Build web 71 | run: ./build.py -web 72 | 73 | - name: Build hot reload (debug) 74 | run: ./build.py -hot-reload -debug 75 | 76 | - name: Build release (debug) 77 | run: ./build.py -release -debug 78 | 79 | - name: Build web (debug) 80 | run: ./build.py -web -debug 81 | 82 | build_windows: 83 | name: Windows 84 | runs-on: windows-latest 85 | steps: 86 | - uses: laytan/setup-odin@v2 87 | with: 88 | token: ${{ secrets.GITHUB_TOKEN }} 89 | release: false 90 | 91 | - uses: mymindstorm/setup-emsdk@v14 92 | 93 | - uses: actions/setup-python@v5 94 | 95 | - uses: actions/checkout@v4 96 | 97 | - uses: ilammy/msvc-dev-cmd@v1 98 | 99 | - name: Build hot reload 100 | run: python build.py -hot-reload 101 | 102 | - name: Build release 103 | run: python build.py -release 104 | 105 | - name: Build web 106 | run: python build.py -web 107 | 108 | - name: Build hot reload (debug) 109 | run: python build.py -hot-reload -debug 110 | 111 | - name: Build release (debug) 112 | run: python build.py -release -debug 113 | 114 | - name: Build web (debug) 115 | run: python build.py -web -debug 116 | -------------------------------------------------------------------------------- /source/web/emscripten_file_io.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 web_support 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 | FILE :: struct{} 26 | 27 | Whence :: enum c.int { 28 | SET, 29 | CUR, 30 | END, 31 | } 32 | 33 | // Similar to raylib's LoadFileData 34 | read_entire_file :: proc(name: string, allocator := context.allocator, loc := #caller_location) -> (data: []byte, success: bool) { 35 | if name == "" { 36 | log.error("No file name provided") 37 | return 38 | } 39 | 40 | file := fopen(strings.clone_to_cstring(name, context.temp_allocator), "rb") 41 | 42 | if file == nil { 43 | log.errorf("Failed to open file %v", name) 44 | return 45 | } 46 | 47 | defer fclose(file) 48 | 49 | fseek(file, 0, .END) 50 | size := ftell(file) 51 | fseek(file, 0, .SET) 52 | 53 | if size <= 0 { 54 | log.errorf("Failed to read file %v", name) 55 | return 56 | } 57 | 58 | data_err: runtime.Allocator_Error 59 | data, data_err = make([]byte, size, allocator, loc) 60 | 61 | if data_err != nil { 62 | log.errorf("Error allocating memory: %v", data_err) 63 | return 64 | } 65 | 66 | read_size := fread(raw_data(data), 1, c.size_t(size), file) 67 | 68 | if read_size != c.size_t(size) { 69 | log.warnf("File %v partially loaded (%i bytes out of %i)", name, read_size, size) 70 | } 71 | 72 | log.debugf("Successfully loaded %v", name) 73 | return data, true 74 | } 75 | 76 | // Similar to raylib's SaveFileData. 77 | // 78 | // Note: This can save during the current session, but I don't think you can 79 | // save any data between sessions. So when you close the tab your saved files 80 | // are gone. Perhaps you could communicate back to emscripten and save a cookie. 81 | // Or communicate with a server and tell it to save data. 82 | write_entire_file :: proc(name: string, data: []byte, truncate := true) -> (success: bool) { 83 | if name == "" { 84 | log.error("No file name provided") 85 | return 86 | } 87 | 88 | file := fopen(strings.clone_to_cstring(name, context.temp_allocator), truncate ? "wb" : "ab") 89 | defer fclose(file) 90 | 91 | if file == nil { 92 | log.errorf("Failed to open '%v' for writing", name) 93 | return 94 | } 95 | 96 | bytes_written := fwrite(raw_data(data), 1, len(data), file) 97 | 98 | if bytes_written == 0 { 99 | log.errorf("Failed to write file %v", name) 100 | return 101 | } else if bytes_written != len(data) { 102 | log.errorf("File partially written, wrote %v out of %v bytes", bytes_written, len(data)) 103 | return 104 | } 105 | 106 | log.debugf("File written successfully: %v", name) 107 | return true 108 | } -------------------------------------------------------------------------------- /source/web/index_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Odin + Sokol + Hot Reload template 8 | 9 | 10 | 11 | 12 | 55 | 56 | 57 | 58 | 59 | 112 | 113 | 114 | {{{ SCRIPT }}} 115 | 116 | 117 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Odin + Sokol + Hot Reload template 2 | 3 | Hot reload gameplay code when making games using Odin + Sokol. Also comes with web build support (no hot reload on web, it's just for web release builds). 4 | 5 | Supported platforms: Windows, Linux and Mac. It's possible to do the web build from all of them. 6 | 7 | ![ezgif-660dd8cd5add20](https://github.com/user-attachments/assets/676b48f0-74e3-4ffa-9098-a9956510aacb) 8 | 9 | Demo and technical overview video: https://www.youtube.com/watch?v=0wNjfgZlDyw 10 | 11 | ## Requirements 12 | 13 | - [Odin compiler](https://odin-lang.org/) (must be in PATH) 14 | - [Python 3](https://www.python.org/) (for the build script) 15 | - [Emscripten](https://emscripten.org/) (optional, for web build support) 16 | 17 | ## Setup 18 | 19 | Run `build.py -update-sokol`. It will download the Sokol bindings and try to build the Sokol C libraries. It also downloads the Sokol Shader compiler. 20 | 21 | The above may fail if no C compiler is available. For example, on Windows you may need to use the `x64 Native Tools Command Prompt for VS20XX`. You can re-run the compilation using `build.py -compile-sokol`. This will avoid re-downloading Sokol, which `-update-sokol` does. 22 | 23 | > [!NOTE] 24 | > `-update-sokol` always does `-compile-sokol` automatically. 25 | 26 | > [!WARNING] 27 | > `-update-sokol` deletes the `sokol-shdc` and `source/sokol` directories. 28 | 29 | If you want web build support, then you either need `emcc` in your path _or_ you can point to the emscripten installation directory by adding `-emsdk-path path/to/emscripten`. You'll have to run `-compile-sokol` with these things present for it to compile the web (WASM) Sokol libraries. 30 | 31 | ## Hot reloading 32 | 33 | 1. Make sure you've done the [setup](#setup) 34 | 2. Run `build.py -hot-reload -run` 35 | 3. A game with just a spinning cube should start 36 | 4. Leave the game running, change a some line in `source/game.odin`. For example, you can modify the line `g.rx += 60 * dt` to use the value `500` instead of `60`. 37 | 5. Re-run `build.py -hot-reload -run`. The game DLL will re-compile and get reloaded. The cube will spin faster. 38 | 39 | > [!NOTE] 40 | > It doesn't matter if you use `-run` on step 5). If the hot reload executable is already running, then it won't try to re-start it. It will just re-build the game DLL and reload that. 41 | 42 | ## Web build 43 | 44 | 1. Make sure you've done the [setup](#setup). Pay attention to the stuff about `emcc` and `-emsdk-path`. 45 | 2. Run `build.py -web`. You may also need to add `-emsdk-path path/to/emscripten`. 46 | 3. Web build is in `build/web` 47 | 48 | > [!NOTE] 49 | > You may not be able to start the `index.html` in there due to javascript CORS errors. If you run the game from a local web server then it will work: 50 | > - Navigate to `build/web` in a terminal 51 | > - Run `python -m http.server` 52 | > - Go to `localhost:8000` in a browser to play your game. 53 | 54 | Check the web developer tools console for any additional errors. Chrome tends to have better error messages than Firefox. 55 | 56 | ## Native release builds 57 | 58 | `build.py -release` makes a native release build of your game (no hot reloading). 59 | 60 | ## Debugging 61 | 62 | Add `-debug` when running `build.py` to create debuggable binaries. 63 | 64 | ## Updating Sokol 65 | 66 | `build.py -update-sokol` downloads the lastest Odin Sokol bindings and latest Sokol shader compiler. 67 | 68 | > [!WARNING] 69 | > This will completely replace everything in the `sokol-shdc` and `source/sokol` directories. 70 | 71 | `build.py -compile-sokol` recompiles the sokol C and WASM libraries. 72 | 73 | > [!NOTE] 74 | > `-update-sokol` automatically does `-compile-sokol`. 75 | > You can also add `-update-sokol` or `-compile-sokol` when building the game. For example you can do `build.py -hot-reload -update-sokol` to update Sokol before compiling the hot reload executable. 76 | 77 | ## Common issues 78 | 79 | ### The build script crashes due to missing libraries 80 | 81 | - Make sure you're using a terminal that has access to a C compiler. 82 | - Re-run `build.py -compile-sokol`. If you want web (WASM) support, then make sure to have `emcc` in the PATH or use `-emsdk-path path/to/emscripten` to point out your emscripten installation. 83 | 84 | ### I'm on an old mac with no metal support 85 | 86 | - Add `-gl` when running `build.py` to force OpenGL 87 | - Remove the `set -e` lines from `source/sokol/build_clibs_macos.sh` and `source/sokol/build_clibs_macos_dylib.sh` and re-run `build.py -compile-sokol`. This will make those scripts not crash when it fails to compile some metal-related Sokol libraries. 88 | 89 | ### I get `panic: wasm_allocator: initial memory could not be allocated` 90 | 91 | You probably have a global variable that allocates dynamic memory. Move that allocation into the `game_init` proc. This could also happen if initialize dynamic arrays or maps in the global file scope, like so: 92 | 93 | ``` 94 | arr := [dynamic]int { 2, 3, 4 } 95 | ``` 96 | 97 | In that case you can declare it and do the initialization in the `init` proc instead: 98 | 99 | ``` 100 | arr: [dynamic]int 101 | 102 | main :: proc() { 103 | arr = { 2, 3, 4 } 104 | 105 | // bla bla 106 | } 107 | ``` 108 | 109 | This happens because the context hasn't been initialized with the correct allocator yet. 110 | 111 | ### I get `RuntimeError: memory access out of bounds` 112 | 113 | Try modifying the `build.py` script and add these flags where it runs `emcc`: 114 | ``` 115 | -sALLOW_MEMORY_GROWTH=1 -sINITIAL_HEAP=16777216 -sSTACK_SIZE=65536 116 | ``` 117 | The numbers above are the default values, try bigger ones and see if it helps. 118 | 119 | ### Error: `emcc: error: build\web\index.data --from-emcc --preload assets' failed (returned 1)` 120 | You might be missing the `assets` folder. It must have at least a single file inside it. You can also remove `--preload assets` from the `build.py` script. 121 | -------------------------------------------------------------------------------- /source/main_hot_reload/main_hot_reload.odin: -------------------------------------------------------------------------------- 1 | /* 2 | Development game exe. Loads build/hot_reload/game.dll and reloads it whenever it 3 | changes. 4 | 5 | Uses sokol/app to open the window. The init, frame, event and cleanup callbacks 6 | of the app run procedures inside the current game DLL. 7 | */ 8 | 9 | package main 10 | 11 | import "core:dynlib" 12 | import "core:fmt" 13 | import "core:os" 14 | import "core:os/os2" 15 | import "core:log" 16 | import "core:mem" 17 | import "base:runtime" 18 | 19 | import sapp "../sokol/app" 20 | 21 | when ODIN_OS == .Windows { 22 | DLL_EXT :: ".dll" 23 | } else when ODIN_OS == .Darwin { 24 | DLL_EXT :: ".dylib" 25 | } else { 26 | DLL_EXT :: ".so" 27 | } 28 | 29 | GAME_DLL_DIR :: "build/hot_reload/" 30 | GAME_DLL_PATH :: GAME_DLL_DIR + "game" + DLL_EXT 31 | 32 | // We copy the DLL because using it directly would lock it, which would prevent 33 | // the compiler from writing to it. 34 | copy_dll :: proc(to: string) -> bool { 35 | copy_err := os2.copy_file(to, GAME_DLL_PATH) 36 | return copy_err == nil 37 | } 38 | 39 | Game_API :: struct { 40 | lib: dynlib.Library, 41 | app_default_desc: proc() -> sapp.Desc, 42 | init: proc(), 43 | frame: proc(), 44 | event: proc(e: ^sapp.Event), 45 | cleanup: proc(), 46 | memory: proc() -> rawptr, 47 | memory_size: proc() -> int, 48 | hot_reloaded: proc(mem: rawptr), 49 | force_restart: proc() -> bool, 50 | modification_time: os.File_Time, 51 | api_version: int, 52 | } 53 | 54 | load_game_api :: proc(api_version: int) -> (api: Game_API, ok: bool) { 55 | mod_time, mod_time_error := os.last_write_time_by_name(GAME_DLL_PATH) 56 | if mod_time_error != os.ERROR_NONE { 57 | fmt.printfln( 58 | "Failed getting last write time of " + GAME_DLL_PATH + ", error code: {1}", 59 | mod_time_error, 60 | ) 61 | return 62 | } 63 | 64 | game_dll_name := fmt.tprintf(GAME_DLL_DIR + "game_{0}" + DLL_EXT, api_version) 65 | copy_dll(game_dll_name) or_return 66 | 67 | // This proc matches the names of the fields in Game_API to symbols in the 68 | // game DLL. It actually looks for symbols starting with `game_`, which is 69 | // why the argument `"game_"` is there. 70 | _, ok = dynlib.initialize_symbols(&api, game_dll_name, "game_", "lib") 71 | if !ok { 72 | fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error()) 73 | } 74 | 75 | api.api_version = api_version 76 | api.modification_time = mod_time 77 | ok = true 78 | 79 | return 80 | } 81 | 82 | unload_game_api :: proc(api: ^Game_API) { 83 | if api.lib != nil { 84 | if !dynlib.unload_library(api.lib) { 85 | fmt.printfln("Failed unloading lib: {0}", dynlib.last_error()) 86 | } 87 | } 88 | 89 | if os.remove(fmt.tprintf(GAME_DLL_DIR + "game_{0}" + DLL_EXT, api.api_version)) != nil { 90 | fmt.printfln("Failed to remove {0}game_{1}" + DLL_EXT + " copy", GAME_DLL_DIR, api.api_version) 91 | } 92 | } 93 | 94 | game_api: Game_API 95 | game_api_version: int 96 | 97 | custom_context: runtime.Context 98 | 99 | init :: proc "c" () { 100 | context = custom_context 101 | game_api.init() 102 | } 103 | 104 | frame :: proc "c" () { 105 | context = custom_context 106 | game_api.frame() 107 | 108 | reload: bool 109 | game_dll_mod, game_dll_mod_err := os.last_write_time_by_name(GAME_DLL_PATH) 110 | 111 | if game_dll_mod_err == os.ERROR_NONE && game_api.modification_time != game_dll_mod { 112 | reload = true 113 | } 114 | 115 | force_restart := game_api.force_restart() 116 | 117 | if reload || force_restart { 118 | new_game_api, new_game_api_ok := load_game_api(game_api_version) 119 | 120 | if new_game_api_ok { 121 | force_restart = force_restart || game_api.memory_size() != new_game_api.memory_size() 122 | 123 | if !force_restart { 124 | // This does the normal hot reload 125 | 126 | // Note that we don't unload the old game APIs because that 127 | // would unload the DLL. The DLL can contain stored info 128 | // such as string literals. The old DLLs are only unloaded 129 | // on a full reset or on shutdown. 130 | append(&old_game_apis, game_api) 131 | game_memory := game_api.memory() 132 | game_api = new_game_api 133 | game_api.hot_reloaded(game_memory) 134 | } else { 135 | // This does a full reset. That's basically like opening and 136 | // closing the game, without having to restart the executable. 137 | // 138 | // You end up in here if the game requests a full reset OR 139 | // if the size of the game memory has changed. That would 140 | // probably lead to a crash anyways. 141 | 142 | game_api.cleanup() 143 | reset_tracking_allocator(&tracking_allocator) 144 | 145 | for &g in old_game_apis { 146 | unload_game_api(&g) 147 | } 148 | 149 | clear(&old_game_apis) 150 | unload_game_api(&game_api) 151 | game_api = new_game_api 152 | game_api.init() 153 | } 154 | 155 | game_api_version += 1 156 | } 157 | } 158 | } 159 | 160 | reset_tracking_allocator :: proc(a: ^mem.Tracking_Allocator) -> bool { 161 | err := false 162 | 163 | for _, value in a.allocation_map { 164 | fmt.printf("%v: Leaked %v bytes\n", value.location, value.size) 165 | err = true 166 | } 167 | 168 | mem.tracking_allocator_clear(a) 169 | return err 170 | } 171 | 172 | event :: proc "c" (e: ^sapp.Event) { 173 | context = custom_context 174 | game_api.event(e) 175 | } 176 | 177 | tracking_allocator: mem.Tracking_Allocator 178 | 179 | cleanup :: proc "c" () { 180 | context = custom_context 181 | game_api.cleanup() 182 | } 183 | 184 | old_game_apis: [dynamic]Game_API 185 | 186 | main :: proc() { 187 | if exe_dir, exe_dir_err := os2.get_executable_directory(context.temp_allocator); exe_dir_err == nil { 188 | os2.set_working_directory(exe_dir) 189 | } 190 | 191 | context.logger = log.create_console_logger() 192 | 193 | default_allocator := context.allocator 194 | mem.tracking_allocator_init(&tracking_allocator, default_allocator) 195 | context.allocator = mem.tracking_allocator(&tracking_allocator) 196 | 197 | custom_context = context 198 | 199 | game_api_ok: bool 200 | game_api, game_api_ok = load_game_api(game_api_version) 201 | 202 | if !game_api_ok { 203 | fmt.println("Failed to load Game API") 204 | return 205 | } 206 | 207 | game_api_version += 1 208 | old_game_apis = make([dynamic]Game_API, default_allocator) 209 | 210 | app_desc := game_api.app_default_desc() 211 | 212 | app_desc.init_cb = init 213 | app_desc.frame_cb = frame 214 | app_desc.cleanup_cb = cleanup 215 | app_desc.event_cb = event 216 | 217 | sapp.run(app_desc) 218 | 219 | free_all(context.temp_allocator) 220 | 221 | if reset_tracking_allocator(&tracking_allocator) { 222 | // You can add something here to inform the user that the program leaked 223 | // memory. In many cases a terminal window will close on shutdown so the 224 | // user could miss it. 225 | } 226 | 227 | for &g in old_game_apis { 228 | unload_game_api(&g) 229 | } 230 | 231 | delete(old_game_apis) 232 | 233 | unload_game_api(&game_api) 234 | mem.tracking_allocator_destroy(&tracking_allocator) 235 | } 236 | 237 | // Make game use good GPU on laptops. 238 | 239 | @(export) 240 | NvOptimusEnablement: u32 = 1 241 | 242 | @(export) 243 | AmdPowerXpressRequestHighPerformance: i32 = 1 244 | -------------------------------------------------------------------------------- /source/game.odin: -------------------------------------------------------------------------------- 1 | /* 2 | This file is the starting point of your game. 3 | 4 | Some importants procedures: 5 | - game_init: Initializes sokol_gfx and sets up the game state. 6 | - game_frame: Called one per frame, do your game logic and rendering in here. 7 | - game_cleanup: Called on shutdown of game, cleanup memory etc. 8 | 9 | The hot reload compiles the contents of this folder into a game DLL. A host 10 | application loads that DLL and calls the procedures of the DLL. 11 | 12 | Special procedures that help facilitate the hot reload: 13 | - game_memory: Run just before a hot reload. The hot reload host application can 14 | that way keep a pointer to the game's memory and feed it to the new game DLL 15 | after the hot reload is complete. 16 | - game_hot_reloaded: Sets the `g` global variable in the new game DLL. The value 17 | comes from the value the host application got from game_memory before the 18 | hot reload. 19 | 20 | When release or web builds are made, then this whole package is just 21 | treated as a normal Odin package. No DLL is created. 22 | 23 | The hot applications use sokol_app to open the window. They use the settings 24 | returned by the `game_app_default_desc` procedure. 25 | */ 26 | 27 | package game 28 | 29 | import "core:math/linalg" 30 | import "core:image/png" 31 | import "core:log" 32 | import "core:slice" 33 | import sapp "sokol/app" 34 | import sg "sokol/gfx" 35 | import sglue "sokol/glue" 36 | import slog "sokol/log" 37 | 38 | Game_Memory :: struct { 39 | pip: sg.Pipeline, 40 | bind: sg.Bindings, 41 | rx, ry: f32, 42 | } 43 | 44 | Mat4 :: matrix[4,4]f32 45 | Vec3 :: [3]f32 46 | g: ^Game_Memory 47 | 48 | Vertex :: struct { 49 | x, y, z: f32, 50 | color: u32, 51 | u, v: u16, 52 | } 53 | 54 | @export 55 | game_app_default_desc :: proc() -> sapp.Desc { 56 | return { 57 | width = 1280, 58 | height = 720, 59 | sample_count = 4, 60 | window_title = "Odin + Sokol hot reload template", 61 | icon = { sokol_default = true }, 62 | logger = { func = slog.func }, 63 | html5 = { 64 | update_document_title = true, 65 | }, 66 | } 67 | } 68 | 69 | @export 70 | game_init :: proc() { 71 | g = new(Game_Memory) 72 | 73 | game_hot_reloaded(g) 74 | 75 | sg.setup({ 76 | environment = sglue.environment(), 77 | logger = { func = slog.func }, 78 | }) 79 | 80 | // The remainder of this proc just sets up a sample cube and loads the 81 | // texture to put on the cube's sides. 82 | // 83 | // The cube is from https://github.com/floooh/sokol-odin/blob/main/examples/cube/main.odin 84 | 85 | /* 86 | Cube vertex buffer with packed vertex formats for color and texture coords. 87 | Note that a vertex format which must be portable across all 88 | backends must only use the normalized integer formats 89 | (BYTE4N, UBYTE4N, SHORT2N, SHORT4N), which can be converted 90 | to floating point formats in the vertex shader inputs. 91 | */ 92 | 93 | vertices := [?]Vertex { 94 | // pos color uvs 95 | { -1.0, -1.0, -1.0, 0xFF0000FF, 0, 0 }, 96 | { 1.0, -1.0, -1.0, 0xFF0000FF, 32767, 0 }, 97 | { 1.0, 1.0, -1.0, 0xFF0000FF, 32767, 32767 }, 98 | { -1.0, 1.0, -1.0, 0xFF0000FF, 0, 32767 }, 99 | 100 | { -1.0, -1.0, 1.0, 0xFF00FF00, 0, 0 }, 101 | { 1.0, -1.0, 1.0, 0xFF00FF00, 32767, 0 }, 102 | { 1.0, 1.0, 1.0, 0xFF00FF00, 32767, 32767 }, 103 | { -1.0, 1.0, 1.0, 0xFF00FF00, 0, 32767 }, 104 | 105 | { -1.0, -1.0, -1.0, 0xFFFF0000, 0, 0 }, 106 | { -1.0, 1.0, -1.0, 0xFFFF0000, 32767, 0 }, 107 | { -1.0, 1.0, 1.0, 0xFFFF0000, 32767, 32767 }, 108 | { -1.0, -1.0, 1.0, 0xFFFF0000, 0, 32767 }, 109 | 110 | { 1.0, -1.0, -1.0, 0xFFFF007F, 0, 0 }, 111 | { 1.0, 1.0, -1.0, 0xFFFF007F, 32767, 0 }, 112 | { 1.0, 1.0, 1.0, 0xFFFF007F, 32767, 32767 }, 113 | { 1.0, -1.0, 1.0, 0xFFFF007F, 0, 32767 }, 114 | 115 | { -1.0, -1.0, -1.0, 0xFFFF7F00, 0, 0 }, 116 | { -1.0, -1.0, 1.0, 0xFFFF7F00, 32767, 0 }, 117 | { 1.0, -1.0, 1.0, 0xFFFF7F00, 32767, 32767 }, 118 | { 1.0, -1.0, -1.0, 0xFFFF7F00, 0, 32767 }, 119 | 120 | { -1.0, 1.0, -1.0, 0xFF007FFF, 0, 0 }, 121 | { -1.0, 1.0, 1.0, 0xFF007FFF, 32767, 0 }, 122 | { 1.0, 1.0, 1.0, 0xFF007FFF, 32767, 32767 }, 123 | { 1.0, 1.0, -1.0, 0xFF007FFF, 0, 32767 }, 124 | } 125 | g.bind.vertex_buffers[0] = sg.make_buffer({ 126 | data = { ptr = &vertices, size = size_of(vertices) }, 127 | }) 128 | 129 | // create an index buffer for the cube 130 | indices := [?]u16 { 131 | 0, 1, 2, 0, 2, 3, 132 | 6, 5, 4, 7, 6, 4, 133 | 8, 9, 10, 8, 10, 11, 134 | 14, 13, 12, 15, 14, 12, 135 | 16, 17, 18, 16, 18, 19, 136 | 22, 21, 20, 23, 22, 20, 137 | } 138 | g.bind.index_buffer = sg.make_buffer({ 139 | usage = { 140 | index_buffer = true, 141 | }, 142 | data = { ptr = &indices, size = size_of(indices) }, 143 | }) 144 | 145 | if img_data, img_data_ok := read_entire_file("assets/round_cat.png", context.temp_allocator); img_data_ok { 146 | if img, img_err := png.load_from_bytes(img_data, allocator = context.temp_allocator); img_err == nil { 147 | sg_img := sg.make_image({ 148 | width = i32(img.width), 149 | height = i32(img.height), 150 | data = { 151 | mip_levels = { 152 | 0 = { 153 | ptr = raw_data(img.pixels.buf), 154 | size = uint(slice.size(img.pixels.buf[:])), 155 | }, 156 | }, 157 | }, 158 | }) 159 | 160 | g.bind.views[VIEW_tex] = sg.make_view({ 161 | texture = sg.Texture_View_Desc({image = sg_img}), 162 | }) 163 | } else { 164 | log.error(img_err) 165 | } 166 | } else { 167 | log.error("Failed loading texture") 168 | } 169 | 170 | // a sampler with default options to sample the above image as texture 171 | g.bind.samplers[SMP_smp] = sg.make_sampler({}) 172 | 173 | // shader and pipeline object 174 | g.pip = sg.make_pipeline({ 175 | shader = sg.make_shader(texcube_shader_desc(sg.query_backend())), 176 | layout = { 177 | attrs = { 178 | ATTR_texcube_pos = { format = .FLOAT3 }, 179 | ATTR_texcube_color0 = { format = .UBYTE4N }, 180 | ATTR_texcube_texcoord0 = { format = .SHORT2N }, 181 | }, 182 | }, 183 | index_type = .UINT16, 184 | cull_mode = .BACK, 185 | depth = { 186 | compare = .LESS_EQUAL, 187 | write_enabled = true, 188 | }, 189 | }) 190 | } 191 | 192 | @export 193 | game_frame :: proc() { 194 | dt := f32(sapp.frame_duration()) 195 | g.rx += 60 * dt 196 | g.ry += 120 * dt 197 | 198 | // vertex shader uniform with model-view-projection matrix 199 | vs_params := Vs_Params { 200 | mvp = compute_mvp(g.rx, g.ry), 201 | } 202 | 203 | pass_action := sg.Pass_Action { 204 | colors = { 205 | 0 = { load_action = .CLEAR, clear_value = { 0.41, 0.68, 0.83, 1 } }, 206 | }, 207 | } 208 | 209 | sg.begin_pass({ action = pass_action, swapchain = sglue.swapchain() }) 210 | sg.apply_pipeline(g.pip) 211 | sg.apply_bindings(g.bind) 212 | sg.apply_uniforms(UB_vs_params, { ptr = &vs_params, size = size_of(vs_params) }) 213 | 214 | // 36 is the number of indices 215 | sg.draw(0, 36, 1) 216 | 217 | sg.end_pass() 218 | sg.commit() 219 | 220 | free_all(context.temp_allocator) 221 | } 222 | 223 | compute_mvp :: proc (rx, ry: f32) -> Mat4 { 224 | proj := linalg.matrix4_perspective(60.0 * linalg.RAD_PER_DEG, sapp.widthf() / sapp.heightf(), 0.01, 10.0) 225 | view := linalg.matrix4_look_at_f32({0.0, -1.5, -6.0}, {}, {0.0, 1.0, 0.0}) 226 | view_proj := proj * view 227 | rxm := linalg.matrix4_rotate_f32(rx * linalg.RAD_PER_DEG, {1.0, 0.0, 0.0}) 228 | rym := linalg.matrix4_rotate_f32(ry * linalg.RAD_PER_DEG, {0.0, 1.0, 0.0}) 229 | model := rxm * rym 230 | return view_proj * model 231 | } 232 | 233 | force_reset: bool 234 | 235 | @export 236 | game_event :: proc(e: ^sapp.Event) { 237 | #partial switch e.type { 238 | case .KEY_DOWN: 239 | if e.key_code == .F6 { 240 | force_reset = true 241 | } 242 | } 243 | } 244 | 245 | @export 246 | game_cleanup :: proc() { 247 | sg.shutdown() 248 | free(g) 249 | } 250 | 251 | @(export) 252 | game_memory :: proc() -> rawptr { 253 | return g 254 | } 255 | 256 | @(export) 257 | game_memory_size :: proc() -> int { 258 | return size_of(Game_Memory) 259 | } 260 | 261 | @(export) 262 | game_hot_reloaded :: proc(mem: rawptr) { 263 | g = (^Game_Memory)(mem) 264 | 265 | // Here you can also set your own global variables. A good idea is to make 266 | // your global variables into pointers that point to something inside 267 | // `g`. Then that state carries over between hot reloads. 268 | } 269 | 270 | @(export) 271 | game_force_restart :: proc() -> bool { 272 | return force_reset 273 | } 274 | 275 | --------------------------------------------------------------------------------