odin-lang  ·  vulkan 1.3  ·  game engine

OdinGame

engine architecture & render pipeline

engine/ — odingame.exe game/ — game.dll (hot-reload) shared/ — game_api.odin Vulkan 1.3 Dynamic Rendering Hot Reload GLSL → SPIR-V

Module Architecture

Three Odin packages communicate through a versioned ABI defined in shared/game_api.odin. The engine owns all memory — the game DLL holds only code. build.lua orchestrates shader compilation (GLSL→SPIR-V via glslc) and both package builds. Dashed lines show type imports; solid lines show runtime data flow.

graph LR
    classDef buildCls   fill:#18102e,stroke:#a855f7,stroke-width:2px,color:#e9d5ff
    classDef engineCls  fill:#0d1636,stroke:#818cf8,stroke-width:2px,color:#c7d2fe
    classDef gameCls    fill:#042318,stroke:#34d399,stroke-width:2px,color:#a7f3d0
    classDef sharedCls  fill:#1c1200,stroke:#fbbf24,stroke-width:2px,color:#fde68a
    classDef gpuCls     fill:#031828,stroke:#38bdf8,stroke-width:2px,color:#bae6fd
    classDef shaderCls  fill:#130a22,stroke:#c084fc,stroke-width:1.5px,color:#f0abfc

    Build(["build.lua"]):::buildCls

    subgraph SH ["shared/"]
        SharedAPI["game_api.odin
Engine_API · Game_API
api_version: u32"]:::sharedCls end subgraph ENG ["engine/ → odingame.exe"] Main["main.odin
Vulkan init + frame loop
GpuContext · SwapchainContext"]:::engineCls HotR["hot_reload.odin
Game_Module
dynlib.load/unload"]:::engineCls Shaders["shaders/
triangle.vert
triangle.frag"]:::shaderCls end subgraph GAMEDLL ["game/ → game.dll"] GameCode["game.odin
Game_State
game_update()"]:::gameCls end GPU["Vulkan 1.3 GPU
GpuContext · SwapchainContext
Pipeline · CommandBuffer
Semaphores · Fence"]:::gpuCls Build -->|"glslc → .spv"| Shaders Build -->|"odin build engine"| Main Build -->|"odin build game -build-mode:dll"| GameCode Shaders --> Main Main -. "imports types" .-> SharedAPI GameCode -. "imports types" .-> SharedAPI HotR -->|"dynlib.load
game_loaded.dll
(file-watch mtime)"| GameCode GameCode -->|"Engine_API fn ptrs
draw_quad · set_clear_color
log · get_dt · is_key_down"| Main Main -->|"Frame_Commands
record + submit"| GPU

Per-Frame System Flow

Every iteration: GLFW events poll, the game DLL mtime is compared for hot-reload, game_update() populates a Frame_Commands list, then Vulkan records a command buffer using dynamic rendering — no render passes or framebuffer objects. Quads use PushConstants and procedural vertex generation via gl_VertexIndex; no vertex buffers are needed.

flowchart TD
    classDef normalCls    fill:#0d1636,stroke:#818cf8,stroke-width:1.5px,color:#c7d2fe
    classDef hotCls       fill:#280810,stroke:#fb7185,stroke-width:2px,color:#fecdd3
    classDef vulkanCls    fill:#031828,stroke:#38bdf8,stroke-width:1.5px,color:#bae6fd
    classDef decisionCls  fill:#0e0c24,stroke:#a855f7,stroke-width:1.5px,color:#e9d5ff
    classDef startCls     fill:#062818,stroke:#34d399,stroke-width:2px,color:#a7f3d0
    classDef recreateCls  fill:#1c1200,stroke:#fbbf24,stroke-width:1.5px,color:#fde68a

    FrameStart(["● Frame Start"]):::startCls
    FrameStart --> Poll

    Poll["glfw.PollEvents
free_all temp_allocator
clear frame_commands.quads
Δt = now − prev_time"]:::normalCls Poll --> DLLCheck DLLCheck{"game.dll mtime
changed?"}:::decisionCls DLLCheck -- no --> GameUpdate DLLCheck -- yes --> ReadBytes ReadBytes["Read game.dll bytes
into temp memory"]:::hotCls ReadBytes --> DevWait DevWait["vk.DeviceWaitIdle
game_unload(api, memory)
— serialize state"]:::hotCls DevWait --> DynUnload DynUnload["dynlib.unload_library
clear Game_Module.api"]:::hotCls DynUnload --> WriteLoad WriteLoad["Write bytes → game_loaded.dll
dynlib.load_library
(releases lock on game.dll)"]:::hotCls WriteLoad --> BindSyms BindSyms["Bind exported symbols:
game_get_api_version
game_get_memory_size
game_load / update / unload / reload"]:::hotCls BindSyms --> VerAPI VerAPI{"API version
match?
engine == game"}:::decisionCls VerAPI -- "no → log warn" --> Poll VerAPI -- yes --> GameReload GameReload["game_reload(api, memory)
state.reload_count++
— restore live state"]:::hotCls GameReload --> GameUpdate GameUpdate["game_update(api, memory)
— reads/writes Game_State
— calls api.set_clear_color()
— calls api.draw_quad() N times
→ produces Frame_Commands"]:::normalCls GameUpdate --> WaitFence WaitFence["WaitForFences
in_flight_fence
(infinite timeout)"]:::vulkanCls WaitFence --> Acquire Acquire["AcquireNextImageKHR
wait: image_available_semaphore
→ image_index"]:::vulkanCls Acquire --> AcqR AcqR{"acquire
result?"}:::decisionCls AcqR -- SUCCESS --> RstCmd AcqR -- "SUBOPTIMAL / OUT_OF_DATE" --> RecreateA RecreateA["recreate_swapchain
+ create_graphics_pipeline
+ recreate semaphores"]:::recreateCls RecreateA --> FrameStart RstCmd["vk.ResetCommandBuffer
vk.BeginCommandBuffer"]:::vulkanCls RstCmd --> Barrier1 Barrier1["Pipeline Barrier (Sync2)
UNDEFINED
→ COLOR_ATTACHMENT_OPTIMAL
srcStage: TOP_OF_PIPE
dstStage: COLOR_ATTACHMENT_OUTPUT"]:::vulkanCls Barrier1 --> BeginRender BeginRender["CmdBeginRendering
colorAttachment: image_view
loadOp: CLEAR → clear_color
storeOp: STORE"]:::vulkanCls BeginRender --> BindPipe BindPipe["CmdBindPipeline GRAPHICS
CmdSetViewport
CmdSetScissor"]:::vulkanCls BindPipe --> QuadLoop QuadLoop["For each Quad_Command in Frame_Commands:
CmdPushConstants(rect[4], color[4])
CmdDraw(6 verts, gl_VertexIndex → NDC)
no VBO — procedural in vertex shader"]:::vulkanCls QuadLoop --> EndRender EndRender["CmdEndRendering"]:::vulkanCls EndRender --> Barrier2 Barrier2["Pipeline Barrier (Sync2)
COLOR_ATTACHMENT_OPTIMAL
→ PRESENT_SRC_KHR
srcStage: COLOR_ATTACHMENT_OUTPUT
dstStage: BOTTOM_OF_PIPE"]:::vulkanCls Barrier2 --> EndCmd EndCmd["vk.EndCommandBuffer"]:::vulkanCls EndCmd --> ResetFence ResetFence["vk.ResetFences in_flight_fence"]:::vulkanCls ResetFence --> Submit Submit["QueueSubmit (graphics queue)
wait: image_available_semaphore
stage: COLOR_ATTACHMENT_OUTPUT
signal: render_finished_semaphore[image_index]
fence: in_flight_fence"]:::vulkanCls Submit --> Present Present["QueuePresentKHR (present queue)
wait: render_finished_semaphore[image_index]
swapchain + image_index"]:::vulkanCls Present --> PresR PresR{"present
result?"}:::decisionCls PresR -- SUCCESS --> FrameStart PresR -- "SUBOPTIMAL / OUT_OF_DATE" --> RecreateB RecreateB["recreate_swapchain
+ create_graphics_pipeline
+ recreate semaphores"]:::recreateCls RecreateB --> FrameStart

Key Design Notes

State-Persistent Hot Reload

Game state lives in an engine-allocated []byte slice. The DLL owns zero heap memory — only code. On reload, game_reload(api, memory) receives the same pointer it left behind, so the live game state fully survives DLL swaps.

Shadow-Copy DLL Trick (Windows)

The engine copies game.dllgame_loaded.dll before loading. This releases the OS file lock on the original, letting the Odin compiler overwrite it while the engine is running — enabling zero-friction hot reload without a build system dance.

Dynamic Rendering (VK 1.3)

No VkRenderPass or VkFramebuffer objects exist. CmdBeginRendering takes attachment info inline. Swapchain recreation only recreates the swapchain + pipeline — no framebuffer rebuild needed.

Procedural Quad Vertex Shader

The vertex shader contains 6 hardcoded unit-quad positions. gl_VertexIndex indexes them at draw time, so CmdDraw(6, 1, 0, 0) needs no VBO. PushConstants deliver rect(xy+zw) and color per quad in NDC space.

Sync Primitives

image_available_semaphore signals when the swapchain image is safe to write. render_finished_semaphores[image_index] gates presentation. in_flight_fence (pre-signaled) prevents the CPU from outrunning the GPU.

Versioned ABI

GAME_API_VERSION = 1 is checked on every reload. A mismatch logs a warning and skips the reload — the engine keeps running with the previous DLL unloaded, preventing crashes from incompatible structs.