├── .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 | 
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 |
--------------------------------------------------------------------------------