Skip to content

Instantly share code, notes, and snippets.

@Falconerd
Created October 7, 2025 04:55
Show Gist options
  • Select an option

  • Save Falconerd/35e7fa827dc572d18ef20bef527c5a6a to your computer and use it in GitHub Desktop.

Select an option

Save Falconerd/35e7fa827dc572d18ef20bef527c5a6a to your computer and use it in GitHub Desktop.
Odin + Raylib Hot Reload

Build Instructions

Build Cradle

odin build main.odin -file -out:build/debug.exe -debug

Build Game (This one is the hot reload one)

odin build game.odin -file -build-mode:dll -out:build/game.dll -debug

Linux/MacOS

remove .exe and swap .dll for .so or .dylib

package main
import "core:fmt"
// Types and constants from raylib are safe to use - old dynamic libs
// are kept alive until full restart.
//
// Procedures are not safe due to different raylib context between
// Cradle and Game.
import rl "vendor:raylib"
import "raylib_api"
Vec2 :: rl.Vector2
// We can keep dynamic arrays here as the context (Odin's context) is
// passed from the Cradle to Game via calling convention and thus memory
// is owned by Cradle.
Game_State :: struct {
positions: [dynamic]Vec2,
sizes: [dynamic]Vec2,
colors: [dynamic]rl.Color,
}
@export
game_memory_size :: proc() -> int {
return size_of(Game_State)
}
@export
game_init :: proc(gs: ^Game_State, rl: ^raylib_api.Raylib_API) {
append(&gs.positions, Vec2{10, 10})
append(&gs.positions, Vec2{50, 50})
append(&gs.positions, Vec2{150, 20})
append(&gs.sizes, Vec2{100, 100})
append(&gs.sizes, Vec2{15, 30})
append(&gs.sizes, Vec2{30, 800})
append(&gs.colors, rl.RED)
append(&gs.colors, rl.GREEN)
append(&gs.colors, rl.BLUE)
}
@export
game_update :: proc(gs: ^Game_State, rl: ^raylib_api.Raylib_API) {
// Write changes here, such as (uncomment these then recompile game
// without closing the program):
// gs.sizes[2].y = 80
// gs.positions[0] = rl.GetMousePosition()
}
@export
game_render :: proc(gs: ^Game_State, rl: ^raylib_api.Raylib_API) {
rl.ClearBackground(rl.BLACK)
for pos, i in gs.positions {
size := gs.sizes[i]
color := gs.colors[i]
rl.DrawRectangleV(pos, size, color)
}
}
package main
import "core:fmt"
import "core:os/os2"
import "core:os"
import "core:mem"
import "core:dynlib"
import rl "vendor:raylib"
import "raylib_api"
DLL_PATH :: "build/"
DLL_NAME :: "game"
when ODIN_OS == .Windows {
DLL_EXT :: ".dll"
} else when ODIN_OS == .Darwin {
DLL_EXT :: ".dylib"
} else {
DLL_EXT :: ".so"
}
DLL_PATH_FULL :: DLL_PATH + DLL_NAME + DLL_EXT
Game_API :: struct {
lib: dynlib.Library,
reload_count: int,
last_write_time: os.File_Time,
game_memory_size: proc() -> int,
game_init: proc(game_memory: rawptr, rl: ^raylib_api.Raylib_API),
game_update: proc(game_memory: rawptr, rl: ^raylib_api.Raylib_API),
game_render: proc(game_memory: rawptr, rl: ^raylib_api.Raylib_API),
}
game_api_reload :: proc(api: ^Game_API) -> (ok: bool) {
// Construct the path to copy the DLL
// The DLL must be copied as the OS locks it, so when we compile it
// can't be overwritten
dll_copy_path_full := fmt.tprintf("%s%s_%d%s", DLL_PATH, DLL_NAME, api.reload_count, DLL_EXT)
// NOTE: We don't unload old DLLs because they may contain memory
// being used (string literals, etc)
//
// Alternative: If you don't want to keep old DLLs until a restart,
// allocate every string using the Cradle's allocator (context)
if copy_err := os2.copy_file(dll_copy_path_full, DLL_PATH_FULL); copy_err != nil {
return false
}
// Load the symbols from the DLL
if symbol_count, symbol_init_ok := dynlib.initialize_symbols(api, dll_copy_path_full); !symbol_init_ok {
// Most likely error is 'TOO SHORT'
// as in, the file is still being written
return false
}
// Get the time the DLL was changed
if dll_last_write_time, dll_last_write_time_err := os.last_write_time_by_name(DLL_PATH_FULL); dll_last_write_time_err == nil {
api.last_write_time = dll_last_write_time
} else {
fmt.printfln("Failed to read last write time of `%s`", DLL_PATH_FULL)
return false
}
// Everything is okay
api.reload_count += 1
return true
}
main :: proc() {
rl_api := raylib_api.init()
game_api: Game_API
assert(game_api_reload(&game_api))
rl.InitWindow(800, 600, "Hot Reloading Test")
defer rl.CloseWindow()
game_memory_buffer := make([]u8, game_api.game_memory_size())
game_memory_pointer := raw_data(game_memory_buffer)
game_api.game_init(game_memory_pointer, &rl_api)
for !rl.WindowShouldClose() {
if dll_write_time, dll_write_time_err := os.last_write_time_by_name(DLL_PATH_FULL); dll_write_time_err == nil {
if dll_write_time != game_api.last_write_time {
if !game_api_reload(&game_api) {
// Don't try to call into the DLL until it's loaded
continue
}
}
}
game_api.game_update(game_memory_pointer, &rl_api)
rl.BeginDrawing()
game_api.game_render(game_memory_pointer, &rl_api)
rl.EndDrawing()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment