LEARN ODIN PROGRAMMING LANGUAGE
Learn Odin Programming Language: From Fundamentals to Mastery
Goal: Deeply understand the Odin programming language—its philosophy, unique features, and where it shines over other systems languages—through hands-on projects that exercise every core concept.
What is Odin?
Odin is a systems programming language designed as a practical, joyful alternative to C. Created by Bill Hall (gingerBill) in 2016, it provides quality-of-life improvements over C while maintaining simplicity. Unlike Rust’s complexity or Zig’s aggressive compile-time features, Odin takes a more conservative, pragmatic approach.
The Philosophy
“Odin is not a ‘Big Agenda’ language. It doesn’t try to enforce a new way of programming onto you.”
Odin solves actual problems with actual solutions. It’s designed for high performance, modern systems, and data-oriented programming.
Real-World Validation
JangaFX uses Odin to develop EmberGen—a real-time volumetric fluid simulator used by Bethesda, CAPCOM, Warner Bros, Weta Digital, and over 200 game studios. This isn’t a toy language—it powers production software.
Why Odin Over Other Languages?
Odin vs C
| Aspect | C | Odin |
|---|---|---|
| Memory Safety | None | Bounds checking, tracking allocator |
| Build System | External (Make, CMake) | Built-in |
| Generics | Macros (unsafe) | Parametric polymorphism |
| Error Handling | errno, return codes | Multiple returns + or_return |
| Data Layout | Manual | Built-in SOA, SIMD |
| Modern Types | None | Unions, bit_sets, distinct types |
Odin vs Rust
| Aspect | Rust | Odin |
|---|---|---|
| Learning Curve | Steep (borrow checker) | Gentle (C-like) |
| Compile Time | Slow | Fast |
| Complexity | High | Intentionally low |
| Memory Model | Ownership | Manual with allocators |
| Error Handling | Result<T, E> | Multiple returns |
Odin vs Zig
| Aspect | Zig | Odin |
|---|---|---|
| Comptime | Pervasive | Conservative |
| Philosophy | Replace C entirely | Improve C pragmatically |
| Build System | Zig is the build system | Built-in, simpler |
| Allocators | Explicit everywhere | Context-based (implicit) |
Where Odin Shines
- Game Development: Built-in graphics bindings, array programming, swizzling
- Real-Time Graphics: SOA, SIMD, matrices, quaternions out of the box
- Data-Oriented Design: First-class SOA support, cache-friendly by design
- Rapid Prototyping: Simple syntax, fast compilation, hot reloading support
- Productivity: Less boilerplate than C, less complexity than Rust/C++
Core Concept Analysis
1. The Context System
Odin’s most unique feature. Every procedure implicitly receives a context parameter containing:
- The current allocator
- The temporary allocator
- Logger
- User data
// Conceptual view of what happens
my_procedure :: proc() {
// context is implicitly available
ptr := new(MyStruct) // Uses context.allocator
temp := make([]int, 100, context.temp_allocator)
}
Why it matters: Libraries can be intercepted and modified without recompilation. Change allocators, add logging—all without modifying library code.
2. Memory Management Philosophy
Odin uses manual memory management but makes it comfortable:
Allocator Hierarchy:
┌─────────────────────────────────────────────────────────┐
│ Tracking Allocator │
│ (Wraps any allocator, tracks leaks, bad frees) │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Heap Allocator │ │ Arena Allocator │ │
│ │ (General use) │ │ (Bulk/temp) │ │
│ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Temporary Allocator │
│ (Arena-based, freed each frame/scope) │
└─────────────────────────────────────────────────────────┘
3. Data-Oriented Design Features
Array Programming (operations on entire arrays):
a := [3]f32{1, 2, 3}
b := [3]f32{4, 5, 6}
c := a + b // {5, 7, 9}
d := a * 2 // {2, 4, 6}
Swizzling (reorder vector components):
v := [4]f32{1, 2, 3, 4}
v.xy // {1, 2}
v.zyx // {3, 2, 1}
v.rgba // Same as xyzw
SOA (Structure of Arrays):
// AoS (traditional)
Entity :: struct { x, y, z: f32; health: int }
entities: [1000]Entity
// SOA (cache-friendly)
#soa entities: [1000]Entity
// Becomes: x: [1000]f32, y: [1000]f32, z: [1000]f32, health: [1000]int
SIMD Vectors:
Vec4 :: #simd[4]f32
a: Vec4 = {1, 2, 3, 4}
b: Vec4 = {5, 6, 7, 8}
c := a + b // Hardware SIMD operation
4. Error Handling
Multiple return values + or_return:
read_file :: proc(path: string) -> (data: []byte, ok: bool) {
// ...
}
// Usage with or_return (like Rust's ? or Zig's try)
process :: proc() -> bool {
data := read_file("config.txt") or_return
// If read_file returns false, process returns false immediately
return true
}
5. Type System Features
Distinct Types (like newtypes):
Meters :: distinct f32
Feet :: distinct f32
// Cannot accidentally mix Meters and Feet!
Tagged Unions:
Token :: union {
Identifier: string,
Number: f64,
Operator: rune,
}
Bit Sets:
Direction :: enum { North, South, East, West }
Directions :: bit_set[Direction]
allowed: Directions = {.North, .South}
if .North in allowed { /* ... */ }
Project List
Projects are ordered from fundamentals to advanced, each building on previous concepts.
Project 1: Memory Arena Allocator
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C, Rust, Zig
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Memory Management / Allocators
- Software or Tool: Custom implementation
- Main Book: “Understanding the Odin Programming Language” by Karl Zylinski
What you’ll build: A custom arena allocator from scratch that implements Odin’s allocator interface, with support for reset, free_all, and memory tracking.
Why it teaches Odin: Memory management is the foundation of systems programming. Odin’s allocator interface is one of its killer features. Building your own teaches you how the entire ecosystem works—every core library uses this interface.
Core challenges you’ll face:
- Implementing the Allocator interface → maps to understanding Odin’s memory model
- Managing a contiguous memory block → maps to low-level memory layout
- Supporting alignment requirements → maps to CPU cache optimization
- Integrating with the context system → maps to Odin’s implicit context
Key Concepts:
- Allocator Interface: Odin Overview - Allocators
- Arena Allocators: “Understanding the Odin Programming Language” Ch. 8
- Context System: Karl Zylinski - Temporary Allocator
- Memory Alignment: “Computer Systems: A Programmer’s Perspective” Ch. 3
Difficulty: Intermediate Time estimate: Weekend Prerequisites: Basic programming concepts, understanding of pointers
Real world outcome:
$ odin run arena_demo
Arena Allocator Demo
--------------------
Created arena with 1MB capacity
Allocating 1000 structs...
Allocated: 1000 × Entity (48 bytes each)
Total used: 48,000 bytes
Alignment: 8-byte aligned ✓
Allocating strings...
"Hello, Odin!" at 0x7f8b4c048000
"Arena allocators are fast!" at 0x7f8b4c04800d
Reset arena (instant free)...
Used: 0 bytes
Capacity: 1,048,576 bytes
Stress test: 100,000 allocations...
Arena time: 0.8ms
Heap time: 45.2ms
Speedup: 56x faster!
Implementation Hints:
The Odin allocator interface:
// What you need to implement
Allocator :: struct {
procedure: Allocator_Proc,
data: rawptr,
}
Allocator_Proc :: proc(
allocator_data: rawptr,
mode: Allocator_Mode,
size, alignment: int,
old_memory: rawptr,
old_size: int,
location: Source_Code_Location
) -> ([]byte, Allocator_Error)
Arena structure concept:
Arena:
┌─────────────────────────────────────────────────────────┐
│ buffer ([]byte) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ used data │ used data │ used data │ free space │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ offset (current position) end │
└─────────────────────────────────────────────────────────┘
Questions to guide implementation:
- How do you handle allocation requests? (Bump the offset)
- How do you handle alignment? (Round up offset to alignment boundary)
- What happens on free? (Nothing for arena—free_all resets offset to 0)
- How do you integrate with context? (Set context.allocator)
Learning milestones:
- Basic allocation works → You understand the allocator interface
- Alignment is correct → You understand memory layout
- Reset is instant → You understand arena efficiency
- Works with core library → You understand context integration
Project 2: Vector Math Library with SIMD
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C (with intrinsics), Rust
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: SIMD / Math / Performance
- Software or Tool: Odin’s built-in SIMD
- Main Book: “Computer Graphics from Scratch” by Gabriel Gambetta
What you’ll build: A 3D math library with Vec2, Vec3, Vec4, Mat4, and Quaternion types using Odin’s built-in SIMD and array programming, plus operations like dot, cross, normalize, and matrix multiplication.
Why it teaches Odin: This showcases Odin’s array programming, swizzling, SIMD vectors, and built-in math types. These features are why Odin excels at graphics and games—they’re first-class language features, not libraries.
Core challenges you’ll face:
- Using #simd vectors efficiently → maps to hardware SIMD understanding
- Implementing quaternion operations → maps to using Odin’s built-in quaternion type
- Matrix operations with proper layout → maps to column-major matrices for SIMD
- Leveraging swizzling → maps to elegant vector manipulation
Key Concepts:
- SIMD in Odin: Odin SIMD Package
- Array Programming: Odin Overview
- Linear Algebra for Games: “Computer Graphics from Scratch” Ch. 2-4
- Quaternions: “3D Math Primer for Graphics and Game Development” Ch. 8
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Basic linear algebra (vectors, matrices)
Real world outcome:
$ odin run math_lib_demo
Vector Operations
-----------------
a = {1.0, 2.0, 3.0}
b = {4.0, 5.0, 6.0}
a + b = {5.0, 7.0, 9.0}
a · b = 32.0
a × b = {-3.0, 6.0, -3.0}
|a| = 3.742
Swizzling Demo
--------------
v = {1.0, 2.0, 3.0, 4.0}
v.xy = {1.0, 2.0}
v.zyx = {3.0, 2.0, 1.0}
v.wwww = {4.0, 4.0, 4.0, 4.0}
Quaternion Demo
---------------
q1 = rotation(45°, Y_AXIS)
q2 = rotation(30°, X_AXIS)
q_combined = q1 * q2
Rotating point {1, 0, 0}: {0.707, 0.354, 0.612}
Matrix Demo
-----------
M = perspective(fov=60°, aspect=16/9, near=0.1, far=100)
V = look_at(eye={0,5,10}, target={0,0,0}, up={0,1,0})
MVP = M * V
SIMD Benchmark (1M operations)
------------------------------
Scalar dot product: 8.2ms
SIMD dot product: 1.1ms
Speedup: 7.5x
Implementation Hints:
Odin already has excellent built-in support:
// Built-in types you can use directly
import "core:math/linalg"
// But for learning, build your own:
Vec3 :: [3]f32 // Array programming works on this!
Vec4 :: #simd[4]f32 // Hardware SIMD
// Array programming just works:
add :: proc(a, b: Vec3) -> Vec3 {
return a + b // Component-wise, auto-vectorized
}
// Swizzling is built-in:
cross :: proc(a, b: Vec3) -> Vec3 {
return a.yzx * b.zxy - a.zxy * b.yzx
}
Matrix representation (column-major for SIMD efficiency):
Mat4 :: struct {
columns: [4]Vec4,
}
// Matrix-vector multiply leverages SIMD
mul_mv :: proc(m: Mat4, v: Vec4) -> Vec4 {
return m.columns[0] * v.x +
m.columns[1] * v.y +
m.columns[2] * v.z +
m.columns[3] * v.w
}
Learning milestones:
- Vector ops work → You understand array programming
- Swizzling feels natural → You understand Odin’s expressiveness
- Matrix multiply is fast → You understand SIMD benefits
- Quaternions work → You understand Odin’s numerical types
Project 3: JSON Parser with Tagged Unions
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: Rust, Go
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Parsing / Type System
- Software or Tool: Custom implementation
- Main Book: “Crafting Interpreters” by Robert Nystrom
What you’ll build: A complete JSON parser using Odin’s tagged unions for the AST, or_return for error handling, and dynamic arrays for collections.
Why it teaches Odin: Tagged unions are Odin’s way of representing “one of many” types safely. Combined with or_return for error propagation, this project teaches idiomatic Odin error handling and type-safe data representation.
Core challenges you’ll face:
- Designing the Value union → maps to understanding tagged unions
- Error propagation with or_return → maps to idiomatic error handling
- Memory management for dynamic data → maps to allocator awareness
- String handling and escapes → maps to Odin string types
Key Concepts:
- Tagged Unions: Odin Overview - Unions
- Error Handling: Odin FAQ
- Parsing Techniques: “Crafting Interpreters” Ch. 4-6
- Dynamic Arrays: Odin Overview - Dynamic Arrays
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Understanding of JSON format, basic parsing concepts
Real world outcome:
$ odin run json_parser
Parsing test.json...
{
"name": "Odin",
"version": 1.0,
"features": ["fast", "simple", "powerful"],
"config": {
"debug": true,
"workers": 4
}
}
Parsed successfully!
AST Structure:
Object {
"name" → String("Odin")
"version" → Number(1.0)
"features" → Array [
String("fast")
String("simple")
String("powerful")
]
"config" → Object {
"debug" → Bool(true)
"workers" → Number(4)
}
}
Type-safe access demo:
name = "Odin"
worker_count = 4
first_feature = "fast"
Error handling demo:
Input: {"broken": }
Error at line 1, col 12: Expected value, got '}'
Implementation Hints:
The JSON Value as a tagged union:
Json_Value :: union {
Null,
Bool,
Number,
String,
Array,
Object,
}
Null :: struct {}
Bool :: distinct bool
Number :: distinct f64
String :: distinct string
Array :: distinct [dynamic]Json_Value
Object :: distinct map[string]Json_Value
Error handling with or_return:
Parse_Error :: struct {
message: string,
line, column: int,
}
parse_value :: proc(p: ^Parser) -> (Json_Value, bool) {
skip_whitespace(p)
switch peek(p) {
case 'n': return parse_null(p) or_return
case 't', 'f': return parse_bool(p) or_return
case '"': return parse_string(p) or_return
case '[': return parse_array(p) or_return
case '{': return parse_object(p) or_return
case '0'..='9', '-': return parse_number(p) or_return
case:
set_error(p, "Unexpected character")
return {}, false
}
}
Pattern matching on unions:
print_value :: proc(v: Json_Value, indent: int) {
switch val in v {
case String:
fmt.printf("\"%s\"", val)
case Number:
fmt.printf("%v", val)
case Array:
fmt.println("[")
for elem in val {
print_value(elem, indent + 2)
}
fmt.println("]")
// ... etc
}
}
Learning milestones:
- Can parse primitives → You understand basic parsing and unions
- Nested structures work → You understand recursive descent
- Errors are clear → You understand or_return idiom
- Memory is managed → You understand allocator integration
Project 4: 2D Game with Raylib
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C, Zig
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Game Development / Graphics
- Software or Tool: Raylib (built-in Odin bindings)
- Main Book: “Game Programming Patterns” by Robert Nystrom
What you’ll build: A complete 2D game (like a platformer or top-down shooter) using Odin’s official Raylib bindings, demonstrating game loops, input handling, sprite rendering, and basic physics.
Why it teaches Odin: Odin was literally made for game development. The official Raylib bindings showcase how Odin’s features (defer, context, array programming) make game code cleaner than C while being just as fast.
Core challenges you’ll face:
- Game loop structure → maps to Odin program structure
- Resource management with defer → maps to RAII-like patterns in Odin
- Entity management → maps to struct design and slices
- Input and collision → maps to practical Odin coding
Key Concepts:
- Raylib in Odin: Raylib Odin Bindings
- Game Loops: “Game Programming Patterns” Ch. 9
- 2D Physics: Basic AABB collision
- Defer for Cleanup: Odin Overview
Resources for key challenges:
- Karl Zylinski - No-engine gamedev using Odin + Raylib
- CAT & ONION - Commercial Odin+Raylib game
Difficulty: Intermediate Time estimate: 2 weeks Prerequisites: Basic game development concepts, Project 2 (Vector Math)
Real world outcome:
$ odin run platformer -o:speed
[Window opens showing a 2D platformer game]
Features demonstrated:
- Smooth player movement with acceleration/friction
- Gravity and jumping physics
- Tilemap rendering with camera following
- Enemy AI with state machines
- Particle effects for dust/explosions
- Sound effects and music
- Pause menu with settings
Controls:
- Arrow keys / WASD: Move
- Space: Jump
- Escape: Pause
Debug overlay (F1):
FPS: 144
Entities: 47
Draw calls: 23
Memory: 2.3 MB
Implementation Hints:
Basic Raylib game structure:
import rl "vendor:raylib"
main :: proc() {
rl.InitWindow(800, 600, "My Odin Game")
defer rl.CloseWindow() // Cleanup when main exits
rl.SetTargetFPS(60)
player := Player{ pos = {400, 300} }
for !rl.WindowShouldClose() {
// Update
dt := rl.GetFrameTime()
update_player(&player, dt)
// Draw
rl.BeginDrawing()
defer rl.EndDrawing() // Always called, even on error
rl.ClearBackground(rl.RAYWHITE)
draw_player(player)
}
}
Entity structure:
Player :: struct {
pos: [2]f32,
vel: [2]f32,
on_ground: bool,
facing_right: bool,
sprite: rl.Texture2D,
animation_frame: int,
}
update_player :: proc(p: ^Player, dt: f32) {
// Input
move_input: f32 = 0
if rl.IsKeyDown(.RIGHT) do move_input += 1
if rl.IsKeyDown(.LEFT) do move_input -= 1
// Horizontal movement
p.vel.x = move_to(p.vel.x, move_input * MAX_SPEED, ACCEL * dt)
// Gravity
p.vel.y += GRAVITY * dt
// Jump
if p.on_ground && rl.IsKeyPressed(.SPACE) {
p.vel.y = -JUMP_FORCE
}
// Apply velocity
p.pos += p.vel * dt
}
Learning milestones:
- Window opens, player renders → You understand Raylib basics
- Smooth movement works → You understand game physics
- Collisions work → You understand game logic
- Game is fun to play → You’ve made a real game in Odin!
Project 5: Entity Component System (ECS) with SOA
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C++, Rust
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 3: Advanced
- Knowledge Area: Game Architecture / Data-Oriented Design
- Software or Tool: Custom implementation with #soa
- Main Book: “Data-Oriented Design” by Richard Fabian
What you’ll build: A data-oriented Entity Component System using Odin’s #soa attribute, demonstrating cache-friendly game architecture with systems that process components efficiently.
Why it teaches Odin: The #soa attribute is one of Odin’s most powerful features. This project shows why Odin is uniquely suited for data-oriented design—the compiler transforms your data layout automatically while keeping the same programming interface.
Core challenges you’ll face:
- Understanding AoS vs SoA trade-offs → maps to cache-friendly design
- Using #soa with slices and dynamic arrays → maps to Odin’s SOA support
- Designing component queries → maps to bit_sets for component masks
- Efficient iteration patterns → maps to data-oriented thinking
Key Concepts:
- SOA in Odin: Odin Overview - SOA
- Data-Oriented Design: “Data-Oriented Design” by Richard Fabian
- ECS Architecture: “Game Programming Patterns” Ch. 14
- Cache Performance: “Computer Systems: A Programmer’s Perspective” Ch. 6
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 4 (Game with Raylib), understanding of data locality
Real world outcome:
$ odin run ecs_demo
ECS Demo - 100,000 Entities
---------------------------
Memory Layout Comparison:
AoS (Array of Structs): Components interleaved
SoA (Struct of Arrays): Components contiguous
Entity struct size: 64 bytes
AoS total: 6.4 MB
SoA total: 6.4 MB (same, but layout differs)
System: MovementSystem (Position + Velocity)
Entities with components: 100,000
AoS iteration: 12.3 ms
SoA iteration: 2.1 ms ← 5.8x faster!
System: RenderSystem (Position + Sprite)
Entities with components: 85,000
AoS iteration: 15.7 ms
SoA iteration: 3.2 ms ← 4.9x faster!
System: PhysicsSystem (Position + Velocity + Collider)
Entities with components: 50,000
AoS iteration: 8.4 ms
SoA iteration: 1.8 ms ← 4.7x faster!
Why SoA is faster:
- CPU loads entire cache lines (64 bytes)
- SoA: 16 positions loaded per cache line
- AoS: Only 1 entity per cache line (other data wasted)
Running visual demo with 10,000 entities...
[Window opens showing thousands of moving/colliding entities]
Implementation Hints:
Traditional AoS (what you’d write in C):
Entity :: struct {
position: [2]f32,
velocity: [2]f32,
health: f32,
sprite_id: u32,
flags: u32,
// ... more components
}
entities: [dynamic]Entity
Odin’s #soa magic:
Entity :: struct {
position: [2]f32,
velocity: [2]f32,
health: f32,
sprite_id: u32,
flags: u32,
}
// Just add #soa!
#soa entities: [dynamic]Entity
// The compiler transforms this to:
// positions: [dynamic][2]f32
// velocities: [dynamic][2]f32
// healths: [dynamic]f32
// sprite_ids: [dynamic]u32
// flags: [dynamic]u32
// But you still access it the same way:
for &e in entities {
e.position += e.velocity * dt
}
// Or access columns directly:
for i in 0..<len(entities) {
entities.position[i] += entities.velocity[i] * dt
}
Component queries with bit_sets:
Component :: enum {
Position,
Velocity,
Sprite,
Collider,
Health,
AI,
}
ComponentMask :: bit_set[Component]
Entity :: struct {
id: u32,
mask: ComponentMask,
}
// Query entities with specific components
for e in entities {
if {.Position, .Velocity} <= e.mask {
// This entity has Position AND Velocity
}
}
Learning milestones:
- SOA transformation works → You understand #soa mechanics
- Performance difference is measurable → You understand cache effects
- Component queries work → You understand bit_sets
- Systems iterate efficiently → You understand data-oriented design
Project 6: Software Rasterizer
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C, Rust
- Coolness Level: Level 5: Pure Magic
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 4: Expert
- Knowledge Area: Graphics / Rendering / Math
- Software or Tool: Custom software renderer
- Main Book: “Computer Graphics from Scratch” by Gabriel Gambetta
What you’ll build: A 3D software rasterizer from scratch—no GPU, just CPU and math. Transform vertices, rasterize triangles, implement a z-buffer, texture mapping, and basic lighting.
Why it teaches Odin: This project uses everything: SIMD for vertex transforms, array programming for pixel operations, SOA for vertex buffers, and manual memory management for framebuffers. It’s the ultimate Odin workout.
Core challenges you’ll face:
- 3D math pipeline (model→world→view→clip→screen) → maps to matrix operations
- Triangle rasterization → maps to algorithm implementation
- Z-buffer depth testing → maps to memory management
- Texture sampling → maps to array indexing and interpolation
Key Concepts:
- Rasterization Pipeline: “Computer Graphics from Scratch” Part II
- 3D Transformations: “3D Math Primer for Graphics and Game Development”
- Barycentric Coordinates: “Fundamentals of Computer Graphics” Ch. 8
- Fixed-Point Math: For performance optimization
Difficulty: Expert Time estimate: 3-4 weeks Prerequisites: Project 2 (Vector Math), linear algebra, trigonometry
Real world outcome:
$ odin run rasterizer -o:speed
Software Rasterizer Demo
------------------------
Resolution: 800x600
Target FPS: 30
Loading mesh: teapot.obj (6,320 triangles)
Loading texture: checkerboard.png
Rendering pipeline:
1. Vertex Transform (SIMD): 0.8ms
2. Triangle Setup: 0.3ms
3. Rasterization: 12.4ms
4. Fragment Shading: 8.2ms
Total: 21.7ms (46 FPS)
[Window shows rotating 3D teapot with lighting and textures]
Press keys to toggle:
W - Wireframe mode
T - Textured/flat shading
L - Lighting on/off
Z - Show z-buffer
Stats (F1):
Triangles drawn: 4,892
Triangles culled: 1,428
Pixels shaded: 287,432
Cache efficiency: 89%
Implementation Hints:
Framebuffer structure:
Framebuffer :: struct {
width, height: int,
color: []u32, // RGBA pixels
depth: []f32, // Z-buffer
}
create_framebuffer :: proc(w, h: int) -> Framebuffer {
size := w * h
return Framebuffer{
width = w,
height = h,
color = make([]u32, size),
depth = make([]f32, size),
}
}
clear :: proc(fb: ^Framebuffer, color: u32) {
for &c in fb.color { c = color }
for &d in fb.depth { d = 1.0 } // Far plane
}
Vertex transformation with SIMD:
transform_vertices :: proc(vertices: []Vertex, mvp: Mat4) -> []Vec4 {
result := make([]Vec4, len(vertices))
for i, v in vertices {
// Transform position
pos := Vec4{v.position.x, v.position.y, v.position.z, 1}
result[i] = mvp * pos
// Perspective divide
result[i].xyz /= result[i].w
}
return result
}
Triangle rasterization (scanline):
draw_triangle :: proc(fb: ^Framebuffer, v0, v1, v2: Vertex) {
// Compute bounding box
min_x := max(0, int(min(v0.x, v1.x, v2.x)))
max_x := min(fb.width-1, int(max(v0.x, v1.x, v2.x)))
// ... same for y
// Rasterize
for y in min_y..=max_y {
for x in min_x..=max_x {
// Compute barycentric coordinates
w0, w1, w2 := barycentric(x, y, v0, v1, v2)
if w0 >= 0 && w1 >= 0 && w2 >= 0 {
// Inside triangle
// Interpolate depth
z := w0 * v0.z + w1 * v1.z + w2 * v2.z
idx := y * fb.width + x
if z < fb.depth[idx] {
fb.depth[idx] = z
fb.color[idx] = shade_fragment(w0, w1, w2, v0, v1, v2)
}
}
}
}
}
Learning milestones:
- Triangles render → You understand rasterization
- Z-buffer works → You understand depth testing
- Textures map correctly → You understand UV interpolation
- Performance is acceptable → You understand optimization
Project 7: Hot-Reloading Game Engine
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C (with dlopen)
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 3: Advanced
- Knowledge Area: Dynamic Linking / Game Development
- Software or Tool: Odin’s -build-mode:shared
- Main Book: “Game Engine Architecture” by Jason Gregory
What you’ll build: A game engine split into a host executable and game DLL, where you can modify game code and see changes instantly without restarting—like Handmade Hero or Jonathan Blow’s Jai demos.
Why it teaches Odin: Odin has first-class support for building shared libraries. This project teaches foreign imports, DLL loading, and how to structure code for hot-reload—essential for productive game development.
Core challenges you’ll face:
- Splitting engine and game code → maps to module design
- Preserving game state across reloads → maps to memory layout discipline
- Loading/unloading shared libraries → maps to foreign system interaction
- Handling function pointer changes → maps to ABI stability
Key Concepts:
- Shared Libraries in Odin: Odin compiler documentation
- Hot Reloading: “Game Engine Architecture” Ch. 15
- Memory Layout for Hot Reload: Keep state in host, code in DLL
- File Watching: Detect source changes
Resources for key challenges:
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 4 (Raylib Game), understanding of shared libraries
Real world outcome:
$ odin build host -out:host.exe
$ odin build game -build-mode:shared -out:game.dll
$ ./host.exe
[Host] Loading game.dll...
[Host] Found game_init at 0x7f8a2c000100
[Host] Found game_update at 0x7f8a2c000200
[Host] Found game_render at 0x7f8a2c000300
[Game] Initialized! State at 0x7f8a30000000
[Window shows game running]
# In another terminal, edit game code...
$ vim game/game.odin
# Change player speed from 200 to 500
$ odin build game -build-mode:shared -out:game_new.dll
[Host] Detected game.dll change!
[Host] Unloading old game.dll...
[Host] Loading game_new.dll...
[Host] Game state preserved at 0x7f8a30000000
[Host] Hot reload complete in 0.3s
[Game immediately reflects new player speed - no restart!]
Implementation Hints:
Project structure:
project/
├── host/
│ ├── host.odin # Loads DLL, owns state, runs loop
│ └── platform.odin # Window, input, rendering setup
├── game/
│ ├── game.odin # Game logic (hot-reloaded)
│ └── entities.odin # Entity definitions
└── shared/
└── game_api.odin # Interface between host and game
Game API (shared between host and game):
// shared/game_api.odin
Game_State :: struct {
player_pos: [2]f32,
player_vel: [2]f32,
score: int,
entities: [dynamic]Entity,
// ... all persistent state
}
Game_API :: struct {
init: proc(state: ^Game_State, memory: rawptr),
update: proc(state: ^Game_State, dt: f32),
render: proc(state: ^Game_State),
}
Host loading the DLL:
// host/host.odin
import "core:dynlib"
load_game :: proc() -> (Game_API, dynlib.Library) {
lib, ok := dynlib.load_library("game.dll")
if !ok {
fmt.eprintln("Failed to load game.dll")
return {}, nil
}
api := Game_API{
init = dynlib.symbol_address(lib, "game_init"),
update = dynlib.symbol_address(lib, "game_update"),
render = dynlib.symbol_address(lib, "game_render"),
}
return api, lib
}
reload_game :: proc(old_lib: dynlib.Library, state: ^Game_State) -> (Game_API, dynlib.Library) {
dynlib.unload_library(old_lib)
return load_game()
// State pointer remains valid - memory owned by host!
}
Game exports:
// game/game.odin
@(export)
game_init :: proc(state: ^Game_State, memory: rawptr) {
state.player_pos = {400, 300}
}
@(export)
game_update :: proc(state: ^Game_State, dt: f32) {
// This code can be changed while running!
speed: f32 = 200 // Change this, rebuild, see instant update
if rl.IsKeyDown(.RIGHT) { state.player_pos.x += speed * dt }
if rl.IsKeyDown(.LEFT) { state.player_pos.x -= speed * dt }
}
@(export)
game_render :: proc(state: ^Game_State) {
rl.DrawCircleV(state.player_pos, 20, rl.RED)
}
Learning milestones:
- DLL loads and runs → You understand dynamic linking
- State persists across reload → You understand memory ownership
- Changes appear instantly → You’ve built hot reload!
- Development feels magical → You understand why this matters
Project 8: Network Protocol with bit_sets
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C, Rust
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Networking / Binary Protocols
- Software or Tool: Odin’s core:net
- Main Book: “TCP/IP Illustrated, Volume 1” by W. Richard Stevens
What you’ll build: A custom binary network protocol for a multiplayer game or chat application, using Odin’s bit_sets for flags, distinct types for message IDs, and unions for message payloads.
Why it teaches Odin: Network code needs precise binary layouts, type safety, and efficient encoding. Odin’s bit_sets, distinct types, and manual memory control make this cleaner than C while being just as efficient.
Core challenges you’ll face:
- Binary serialization with exact layouts → maps to struct packing and #packed
- Message type discrimination → maps to tagged unions for protocols
- Flags and bitfields → maps to bit_set for protocol flags
- Non-blocking I/O → maps to Odin’s socket API
Key Concepts:
- Odin Networking: core:net package
- Binary Protocols: “TCP/IP Illustrated” Ch. 3
- Bit Sets: Odin Overview
- Struct Packing: #packed attribute
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Basic networking concepts, Project 3 (JSON Parser for unions)
Real world outcome:
$ odin run server &
[Server] Listening on 0.0.0.0:7777
$ odin run client
[Client] Connected to localhost:7777
# Client terminal:
> /join general
[Server] Joined channel: general (5 users)
> Hello everyone!
[You] Hello everyone!
[Bob] Hey! Welcome!
[Alice] o/
> /dm Alice Hey, want to play?
[DM to Alice] Hey, want to play?
[DM from Alice] Sure! Let's go
> /status playing
[Server] Status updated to: playing
# Protocol analysis (wireshark):
Packet: 12 bytes
Header (4 bytes):
Magic: 0x4F44 ("OD")
Flags: [Compressed, Encrypted]
Seq: 42
Payload (8 bytes):
MsgType: ChatMessage (0x03)
Channel: 1
Length: 15
Content: "Hello everyone!"
Implementation Hints:
Protocol message types with unions:
Message_Type :: enum u8 {
Handshake = 0x01,
Chat = 0x02,
Join = 0x03,
Leave = 0x04,
Status = 0x05,
DirectMessage = 0x06,
}
Message_Flags :: enum u8 {
Compressed,
Encrypted,
Priority,
Reliable,
}
Packet_Header :: struct #packed {
magic: [2]u8, // "OD"
flags: bit_set[Message_Flags; u8],
sequence: u16,
payload_len: u16,
}
Message :: struct {
header: Packet_Header,
payload: Message_Payload,
}
Message_Payload :: union {
Handshake_Msg,
Chat_Msg,
Join_Msg,
Leave_Msg,
Status_Msg,
Direct_Msg,
}
Chat_Msg :: struct {
channel_id: Channel_ID, // distinct type!
content: string,
}
Channel_ID :: distinct u16
User_ID :: distinct u32
Serialization:
serialize_message :: proc(msg: Message, buffer: []u8) -> int {
offset := 0
// Write header (fixed size, just memcpy)
mem.copy(&buffer[offset], &msg.header, size_of(Packet_Header))
offset += size_of(Packet_Header)
// Write payload based on type
switch payload in msg.payload {
case Chat_Msg:
buffer[offset] = u8(Message_Type.Chat)
offset += 1
// ... serialize chat message
case Join_Msg:
// ...
}
return offset
}
Using bit_sets for flags:
// Check flags
if .Compressed in packet.header.flags {
data = decompress(data)
}
if .Encrypted in packet.header.flags {
data = decrypt(data)
}
// Set flags
outgoing.header.flags = {.Reliable, .Priority}
Learning milestones:
- Client connects to server → You understand sockets
- Messages serialize/deserialize → You understand binary formats
- Multiple message types work → You understand protocol design
- bit_sets make flags easy → You understand Odin’s type system
Project 9: Custom Profiler with Tracking Allocator
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C++, Rust
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Profiling / Memory Analysis
- Software or Tool: Odin’s tracking allocator + custom
- Main Book: “The Art of Debugging” by Matloff & Salzman
What you’ll build: A profiling tool that wraps Odin’s tracking allocator, records all allocations with call stacks, measures timing of code sections, and generates reports—like a mini Valgrind/Instruments.
Why it teaches Odin: The tracking allocator is Odin’s answer to memory safety—it doesn’t prevent bugs at compile time like Rust, but it catches them at runtime with precise diagnostics. Understanding this is key to productive Odin development.
Core challenges you’ll face:
- Wrapping allocators → maps to allocator composition
- Capturing source locations → maps to #location intrinsic
- Timing code sections → maps to defer for measurement
- Generating useful reports → maps to data aggregation
Key Concepts:
- Tracking Allocator: mem.tracking_allocator
- Source Locations: #location intrinsic
- High-Resolution Timing: core:time
- Allocator Composition: Karl Zylinski - Tracking Allocator
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 1 (Arena Allocator), understanding of call stacks
Real world outcome:
$ odin run game -define:PROFILING=true
[Profiler] Recording enabled
[Game runs for 30 seconds, then exits]
=== MEMORY REPORT ===
Leak Summary:
Total leaked: 2,456 bytes across 3 allocations
Leaks:
1. 2048 bytes at game/entities.odin:47 (create_enemy)
└─ Called from game/spawner.odin:123 (spawn_wave)
└─ Called from game/game.odin:89 (update)
Note: Allocated 15 times, freed 14 times
2. 256 bytes at game/particles.odin:28 (emit_particle)
└─ Allocated 1,247 times, freed 1,246 times
3. 152 bytes at core:strings/builder.odin:34
└─ Called from game/ui.odin:67 (draw_score)
Bad Frees:
1. Double free at game/cleanup.odin:12
Original allocation: game/entities.odin:47
=== TIMING REPORT ===
Section Calls Total Avg Max
---------------------------------------------------------
game_update 1,800 892ms 0.50ms 2.3ms
└─ physics_step 1,800 423ms 0.24ms 1.1ms
└─ entity_update 45,000 312ms 0.007ms 0.2ms
└─ collision_detect 1,800 145ms 0.08ms 0.9ms
game_render 1,800 756ms 0.42ms 1.8ms
└─ draw_entities 45,000 534ms 0.012ms 0.3ms
└─ draw_particles 127,000 198ms 0.002ms 0.1ms
Hot spots (by total time):
1. draw_entities: 534ms (35.4%)
2. physics_step: 423ms (28.0%)
3. entity_update: 312ms (20.7%)
Implementation Hints:
Profiler structure:
Allocation_Record :: struct {
ptr: rawptr,
size: int,
location: runtime.Source_Code_Location,
timestamp: time.Time,
freed: bool,
}
Timing_Record :: struct {
name: string,
start: time.Time,
total_time: time.Duration,
call_count: int,
max_time: time.Duration,
}
Profiler :: struct {
allocations: map[rawptr]Allocation_Record,
timings: map[string]^Timing_Record,
timing_stack: [dynamic]^Timing_Record,
base_allocator: mem.Allocator,
}
Wrapping the allocator:
profiler_allocator :: proc(profiler: ^Profiler) -> mem.Allocator {
return mem.Allocator{
procedure = profiler_allocator_proc,
data = profiler,
}
}
profiler_allocator_proc :: proc(
data: rawptr,
mode: mem.Allocator_Mode,
size, alignment: int,
old_ptr: rawptr,
old_size: int,
loc: runtime.Source_Code_Location,
) -> ([]byte, mem.Allocator_Error) {
profiler := cast(^Profiler)data
// Call base allocator
result, err := profiler.base_allocator.procedure(
profiler.base_allocator.data,
mode, size, alignment, old_ptr, old_size, loc
)
// Record based on mode
switch mode {
case .Alloc:
profiler.allocations[raw_data(result)] = Allocation_Record{
ptr = raw_data(result),
size = size,
location = loc,
timestamp = time.now(),
}
case .Free:
if record, ok := &profiler.allocations[old_ptr]; ok {
record.freed = true
} else {
log.warnf("Bad free at %v", loc)
}
}
return result, err
}
Timing with defer:
@(deferred_out=end_timing)
begin_timing :: proc(profiler: ^Profiler, name: string) -> ^Timing_Record {
record := profiler.timings[name]
if record == nil {
record = new(Timing_Record)
record.name = name
profiler.timings[name] = record
}
record.start = time.now()
append(&profiler.timing_stack, record)
return record
}
end_timing :: proc(record: ^Timing_Record) {
duration := time.since(record.start)
record.total_time += duration
record.call_count += 1
if duration > record.max_time {
record.max_time = duration
}
}
// Usage (automatically calls end_timing when scope exits)
update :: proc() {
begin_timing(profiler, "update") // defer end_timing happens here
// ... game logic
}
Learning milestones:
- Allocations tracked → You understand allocator wrapping
- Leaks detected → You understand the tracking pattern
- Timings recorded → You understand deferred cleanup
- Reports are useful → You understand practical profiling
Project 10: Vulkan Renderer
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C, C++, Rust
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 5: Master
- Knowledge Area: Graphics / GPU Programming
- Software or Tool: Odin’s vendor:vulkan
- Main Book: “Vulkan Programming Guide” by Graham Sellers
What you’ll build: A Vulkan renderer from scratch—device initialization, swapchain, render passes, pipelines, buffers, and drawing a 3D scene with textures and lighting.
Why it teaches Odin: Odin has official Vulkan bindings. This project shows how Odin handles complex C APIs, manages GPU memory (with allocators!), and how data-oriented design applies to graphics programming.
Core challenges you’ll face:
- Vulkan initialization boilerplate → maps to handling C APIs in Odin
- Memory management for GPU resources → maps to custom allocators
- Pipeline creation → maps to complex struct initialization
- Synchronization → maps to understanding GPU execution
Key Concepts:
- Vulkan in Odin: vendor:vulkan
- Vulkan Fundamentals: “Vulkan Programming Guide” or vulkan-tutorial.com
- GPU Memory: VkBuffer, VkImage, VkDeviceMemory
- Render Passes: Framebuffers, attachments, subpasses
Resources for key challenges:
Difficulty: Master Time estimate: 4-6 weeks Prerequisites: Project 6 (Software Rasterizer), understanding of GPU concepts
Real world outcome:
$ odin run vulkan_renderer -o:speed
[Vulkan] Instance created
[Vulkan] Physical device: NVIDIA GeForce RTX 3080
[Vulkan] Logical device created
[Vulkan] Swapchain: 3 images, 1920x1080, VK_FORMAT_B8G8R8A8_SRGB
[Vulkan] Render pass created
[Vulkan] Graphics pipeline created
[Vulkan] Loading mesh: sponza.obj (262,144 vertices)
[Vulkan] Vertex buffer: 12 MB (GPU memory)
[Vulkan] Loading textures: 24 textures, 89 MB total
[Window opens with real-time 3D rendering]
Controls:
WASD - Move camera
Mouse - Look around
F1 - Toggle wireframe
F2 - Toggle frustum culling
Stats:
FPS: 144 (6.9ms frame time)
Draw calls: 156
Triangles: 87,456
GPU memory: 142 MB
Vulkan features used:
✓ Dynamic rendering
✓ Descriptor indexing
✓ Buffer device address
✓ Timeline semaphores
Implementation Hints:
Vulkan with Odin bindings:
import vk "vendor:vulkan"
Renderer :: struct {
instance: vk.Instance,
device: vk.Device,
physical_device: vk.PhysicalDevice,
surface: vk.SurfaceKHR,
swapchain: vk.SwapchainKHR,
render_pass: vk.RenderPass,
pipeline: vk.Pipeline,
command_pool: vk.CommandPool,
// ...
}
init_vulkan :: proc(r: ^Renderer) -> bool {
// Application info
app_info := vk.ApplicationInfo{
sType = .APPLICATION_INFO,
pApplicationName = "Odin Vulkan",
applicationVersion = vk.MAKE_VERSION(1, 0, 0),
pEngineName = "Odin Engine",
engineVersion = vk.MAKE_VERSION(1, 0, 0),
apiVersion = vk.API_VERSION_1_3,
}
// Instance creation
create_info := vk.InstanceCreateInfo{
sType = .INSTANCE_CREATE_INFO,
pApplicationInfo = &app_info,
enabledLayerCount = len(validation_layers),
ppEnabledLayerNames = raw_data(validation_layers),
enabledExtensionCount = len(extensions),
ppEnabledExtensionNames = raw_data(extensions),
}
result := vk.CreateInstance(&create_info, nil, &r.instance)
if result != .SUCCESS {
log.errorf("Failed to create Vulkan instance: %v", result)
return false
}
return true
}
Memory allocation for GPU:
// Custom GPU allocator (simplified)
GPU_Allocator :: struct {
device: vk.Device,
memory_props: vk.PhysicalDeviceMemoryProperties,
allocations: [dynamic]GPU_Allocation,
}
create_buffer :: proc(
alloc: ^GPU_Allocator,
size: vk.DeviceSize,
usage: vk.BufferUsageFlags,
) -> (vk.Buffer, vk.DeviceMemory) {
buffer_info := vk.BufferCreateInfo{
sType = .BUFFER_CREATE_INFO,
size = size,
usage = usage,
sharingMode = .EXCLUSIVE,
}
buffer: vk.Buffer
vk.CreateBuffer(alloc.device, &buffer_info, nil, &buffer)
// Get memory requirements
mem_requirements: vk.MemoryRequirements
vk.GetBufferMemoryRequirements(alloc.device, buffer, &mem_requirements)
// Allocate
memory := allocate_gpu_memory(alloc, mem_requirements, {.HOST_VISIBLE, .HOST_COHERENT})
vk.BindBufferMemory(alloc.device, buffer, memory, 0)
return buffer, memory
}
Learning milestones:
- Triangle renders → You understand Vulkan basics
- 3D mesh with textures → You understand resource management
- Smooth camera movement → You understand frame synchronization
- Complex scene renders → You understand Vulkan at a practical level
Project 11: Real-Time Particle System
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C++, Rust
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Graphics / Simulation / Data-Oriented Design
- Software or Tool: Raylib or custom renderer
- Main Book: “Real-Time Rendering” by Akenine-Möller et al.
What you’ll build: A high-performance particle system with 100,000+ particles, using SOA layouts, SIMD updates, and GPU instancing—like a mini EmberGen.
Why it teaches Odin: This is exactly what Odin was designed for. EmberGen (built in Odin) is a particle/fluid simulator. This project uses SOA, SIMD, and data-oriented design to achieve real-time performance that would be painful in other languages.
Core challenges you’ll face:
- SOA particle storage → maps to #soa for cache efficiency
- SIMD particle updates → maps to vectorized physics
- GPU instancing → maps to efficient rendering
- Emitter systems → maps to procedural generation
Key Concepts:
- SOA Particles: Odin’s #soa for contiguous data
- SIMD Physics: Vectorized position/velocity updates
- Particle Rendering: Billboarding, soft particles
- Lifetime Management: Object pooling
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 4 or 10 (Graphics), Project 5 (ECS/SOA)
Real world outcome:
$ odin run particles -o:speed
Particle System Demo
--------------------
Max particles: 500,000
Active emitters: 5
[Window shows spectacular particle effects]
Emitter Types (1-5 to toggle):
1. Fire/Flames
2. Smoke
3. Sparks/Debris
4. Magic/Energy
5. Rain/Weather
Stats (F1):
Active particles: 234,567
Particles/second: 50,000
Update time: 1.2ms (SIMD)
Render time: 2.1ms (instanced)
Total: 3.3ms (303 FPS)
Memory:
Particle pool: 48 MB (#soa layout)
GPU buffer: 12 MB (instance data)
Comparison:
AoS update: 8.5ms
SoA update: 1.2ms ← 7x faster!
Implementation Hints:
SOA particle storage:
Particle :: struct {
position: [3]f32,
velocity: [3]f32,
color: [4]f32,
size: f32,
life: f32,
max_life: f32,
}
Particle_System :: struct {
// #soa transforms AoS to SoA automatically!
#soa particles: [MAX_PARTICLES]Particle,
active_count: int,
free_list: [dynamic]int,
}
SIMD particle update:
update_particles :: proc(ps: ^Particle_System, dt: f32) {
gravity := [3]f32{0, -9.8, 0}
// Process in SIMD-friendly chunks
for i in 0..<ps.active_count {
// These access contiguous memory (SoA layout)
ps.particles.velocity[i] += gravity * dt
ps.particles.position[i] += ps.particles.velocity[i] * dt
ps.particles.life[i] -= dt
// Kill dead particles
if ps.particles.life[i] <= 0 {
kill_particle(ps, i)
}
}
}
The compiler generates SIMD-optimized code because:
positionsare contiguous in memoryvelocitiesare contiguous in memory- Operations on arrays auto-vectorize
GPU instancing:
// Instance data sent to GPU
Instance_Data :: struct #packed {
position: [3]f32,
color: [4]f32,
size: f32,
}
prepare_instance_data :: proc(ps: ^Particle_System, buffer: []Instance_Data) {
for i in 0..<ps.active_count {
buffer[i] = Instance_Data{
position = ps.particles.position[i],
color = ps.particles.color[i],
size = ps.particles.size[i],
}
}
}
Learning milestones:
- Particles spawn and die → You understand object pooling
- 100k particles run smoothly → You understand SOA benefits
- Effects look good → You understand particle aesthetics
- Performance matches EmberGen claims → You understand Odin’s strengths
Project 12: Audio Synthesizer
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C, Rust
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Audio / DSP / Real-Time
- Software or Tool: miniaudio (Odin bindings)
- Main Book: “The Audio Programming Book” by Boulanger & Lazzarini
What you’ll build: A real-time audio synthesizer with oscillators, filters, envelopes, and effects—like a mini software synth that responds to MIDI or keyboard input.
Why it teaches Odin: Audio programming requires consistent real-time performance with no garbage collection pauses. Odin’s predictable memory management and SIMD support make it ideal for DSP work.
Core challenges you’ll face:
- Lock-free audio callbacks → maps to real-time constraints
- Oscillator math (sin, saw, square) → maps to Odin math functions
- Filter implementation → maps to state management
- MIDI handling → maps to binary protocol parsing
Key Concepts:
- miniaudio in Odin: vendor:miniaudio
- Digital Signal Processing: “The Audio Programming Book” Ch. 1-5
- Lock-Free Programming: For audio thread safety
- SIMD for Audio: Processing multiple samples at once
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Understanding of audio concepts, basic signal processing
Real world outcome:
$ odin run synth
Odin Synthesizer v0.1
---------------------
Audio: 44100 Hz, 256 sample buffer (5.8ms latency)
MIDI: Connected to "Arturia KeyStep"
[Window shows virtual keyboard and visualizers]
Oscillators:
OSC1: Saw wave, +0 cents
OSC2: Square wave, -12 semitones (sub)
Filter:
Type: Low-pass (Moog ladder)
Cutoff: 800 Hz
Resonance: 0.7
Envelope:
Attack: 10ms
Decay: 200ms
Sustain: 0.6
Release: 500ms
Effects:
✓ Reverb (hall, 0.3 wet)
✓ Delay (300ms, 0.4 feedback)
✓ Chorus (rate: 1.5 Hz)
CPU: 4.2%
Playing: C4, E4, G4 (C major chord)
Implementation Hints:
Oscillator with SIMD:
Oscillator :: struct {
phase: f32,
frequency: f32,
sample_rate: f32,
}
// Generate samples (can be SIMD-optimized)
generate_saw :: proc(osc: ^Oscillator, buffer: []f32) {
phase_inc := osc.frequency / osc.sample_rate
for &sample in buffer {
// Saw wave: goes from -1 to 1
sample = 2.0 * osc.phase - 1.0
// Advance phase
osc.phase += phase_inc
if osc.phase >= 1.0 {
osc.phase -= 1.0
}
}
}
// SIMD version for 4 samples at a time
generate_saw_simd :: proc(osc: ^Oscillator, buffer: []f32) {
phase_inc := osc.frequency / osc.sample_rate
phases := #simd[4]f32{
osc.phase,
osc.phase + phase_inc,
osc.phase + phase_inc * 2,
osc.phase + phase_inc * 3,
}
phase_inc_4 := #simd[4]f32{phase_inc * 4, phase_inc * 4, phase_inc * 4, phase_inc * 4}
for i := 0; i < len(buffer); i += 4 {
// Compute 4 samples at once
samples := phases * 2.0 - 1.0
// Store
(cast(^#simd[4]f32)&buffer[i])^ = samples
// Advance
phases += phase_inc_4
phases = phases - math.floor(phases) // Wrap
}
osc.phase = phases[0]
}
Audio callback (real-time safe):
import ma "vendor:miniaudio"
audio_callback :: proc "c" (
device: ^ma.device,
output: rawptr,
input: rawptr,
frame_count: u32,
) {
context = runtime.default_context() // Needed for Odin runtime in C callback
synth := cast(^Synthesizer)device.pUserData
buffer := slice.from_ptr(cast([^]f32)output, int(frame_count) * 2)
// Generate audio (must be lock-free, no allocations!)
synth_generate(synth, buffer)
}
Learning milestones:
- Sine wave plays → You understand audio basics
- Multiple oscillators mix → You understand additive synthesis
- Filter sweeps work → You understand DSP state
- MIDI triggers notes → You understand real-time input
Project 13: Build System / Task Runner
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: Go, Rust
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 2: Intermediate
- Knowledge Area: Developer Tools / Automation
- Software or Tool: Odin’s os and os/os2 packages
- Main Book: “The Pragmatic Programmer” by Hunt & Thomas
What you’ll build: A custom build system / task runner (like Make or Just) written in Odin, with parallel execution, dependency tracking, and file watching for auto-rebuild.
Why it teaches Odin: This project uses Odin’s process spawning, file I/O, and concurrency. It’s also meta—you’re building a tool to build Odin projects, learning how Odin’s own build system works.
Core challenges you’ll face:
- Parsing build definitions → maps to file parsing in Odin
- Dependency graph execution → maps to algorithms in Odin
- Parallel task execution → maps to Odin’s thread package
- File watching → maps to OS-specific APIs
Key Concepts:
- Process Execution: core:os, core:os/os2
- Threading: core:thread
- File System: core:os, core:path/filepath
- Dependency Graphs: Topological sorting
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Basic understanding of build systems
Real world outcome:
$ odin run oddo # Our build tool is called "oddo"
# Create a build file
$ cat build.oddo
project "my_game"
task build {
depends: [assets]
run: "odin build src -out:game.exe"
}
task assets {
sources: ["assets/**/*.png", "assets/**/*.wav"]
run: "asset_packer pack assets/ assets.pak"
}
task run {
depends: [build]
run: "./game.exe"
}
task clean {
run: "rm -rf build/ game.exe assets.pak"
}
task watch {
depends: [build]
watch: ["src/**/*.odin"]
on_change: [build, run]
}
$ oddo build
[oddo] Running task: assets
[oddo] Packing 47 assets...
[oddo] Running task: build
[oddo] Compiling src/ -> game.exe
[oddo] Done in 1.2s
$ oddo watch
[oddo] Watching src/**/*.odin
[oddo] Initial build...
[oddo] Waiting for changes...
# Edit a file in another terminal
[oddo] Changed: src/game.odin
[oddo] Rebuilding...
[oddo] Running game.exe...
$ oddo -j4 all # Parallel execution
[oddo] Running 4 tasks in parallel...
[████████████████████] 100% (4/4 tasks)
Implementation Hints:
Task structure:
Task :: struct {
name: string,
depends: [dynamic]string,
sources: [dynamic]string, // Glob patterns
run: string,
watch: [dynamic]string,
on_change: [dynamic]string,
}
Build_File :: struct {
project: string,
tasks: map[string]Task,
}
Running tasks with dependency order:
run_task :: proc(build: ^Build_File, task_name: string, visited: ^map[string]bool) {
if visited[task_name] {
return
}
visited[task_name] = true
task := build.tasks[task_name]
// Run dependencies first
for dep in task.depends {
run_task(build, dep, visited)
}
// Check if sources are newer than outputs (incremental build)
if !needs_rebuild(task) {
fmt.printfln("[oddo] %s: up to date", task_name)
return
}
// Run the task
fmt.printfln("[oddo] Running task: %s", task_name)
result := os.system(task.run)
if result != 0 {
fmt.eprintfln("[oddo] Task %s failed with code %d", task_name, result)
os.exit(1)
}
}
File watching (simplified):
watch_files :: proc(patterns: []string, on_change: proc()) {
last_modified := get_mtimes(patterns)
for {
time.sleep(500 * time.Millisecond)
current := get_mtimes(patterns)
if current != last_modified {
on_change()
last_modified = current
}
}
}
get_mtimes :: proc(patterns: []string) -> map[string]time.Time {
result := make(map[string]time.Time)
for pattern in patterns {
matches, _ := filepath.glob(pattern)
for path in matches {
info, _ := os.stat(path)
result[path] = info.modification_time
}
}
return result
}
Learning milestones:
- Tasks run in order → You understand dependency resolution
- Parallel execution works → You understand Odin threading
- Watch mode works → You understand file system APIs
- It’s useful for real projects → You’ve built a practical tool
Project 14: WASM Game for the Browser
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: Rust, C
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Web / Graphics / Cross-Platform
- Software or Tool: Odin WASM target + WebGL
- Main Book: “WebAssembly in Action” by Gerard Gallant
What you’ll build: A game that compiles to WebAssembly and runs in the browser with WebGL graphics, demonstrating Odin’s cross-platform capabilities.
Why it teaches Odin: Odin supports WASM as a compilation target. This project shows how Odin code can run anywhere—native or web—and how to handle the browser’s JavaScript interop.
Core challenges you’ll face:
- WASM-specific constraints → maps to no file I/O, no threads
- WebGL bindings → maps to Odin’s vendor:wasm/webgl
- JavaScript interop → maps to foreign procedures
- Memory management → maps to no system allocator
Key Concepts:
- WASM in Odin: Compiler -target:wasm32 or wasm64-freestanding
- WebGL: vendor:wasm/webgl
- JavaScript Interop: @(export) and foreign imports
- Arena-Only Memory: No heap in WASM
Resources for key challenges:
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Project 4 (Raylib Game), basic web development
Real world outcome:
$ odin build game -target:wasm32 -out:game.wasm
$ ls -la game.wasm
-rw-r--r-- 1 user user 245K Dec 21 10:30 game.wasm
$ python -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000
# Open http://localhost:8000 in browser
[Browser shows full game running at 60 FPS]
Console:
[WASM] Loaded game.wasm (245 KB)
[WASM] Memory: 16 MB linear memory
[WASM] WebGL 2.0 context created
[WASM] Audio context initialized
[GAME] Starting...
[GAME] Running at 60 FPS
# Works on mobile too!
# Share link with anyone - no install needed
Implementation Hints:
Project structure:
wasm_game/
├── src/
│ ├── main.odin # Game logic (platform-agnostic)
│ ├── platform_wasm.odin # WASM-specific platform layer
│ └── platform_native.odin # Native platform layer
├── web/
│ ├── index.html
│ └── loader.js # Loads and runs the WASM
└── build.sh
WASM platform layer:
// src/platform_wasm.odin
//+build wasm32, wasm64
package game
import "vendor:wasm/webgl"
import "vendor:wasm/js"
// JavaScript functions we can call
foreign import "env" {
js_console_log :: proc(ptr: [^]u8, len: int) ---
}
// Functions we export to JavaScript
@(export)
game_init :: proc() {
// Initialize WebGL
webgl.SetupState()
// ...
}
@(export)
game_update :: proc(dt: f32) {
update_game(dt)
}
@(export)
game_render :: proc() {
webgl.Clear(webgl.COLOR_BUFFER_BIT)
render_game()
webgl.SwapBuffers()
}
JavaScript loader:
// web/loader.js
async function loadGame() {
const response = await fetch('game.wasm');
const bytes = await response.arrayBuffer();
const memory = new WebAssembly.Memory({ initial: 256 }); // 16MB
const importObject = {
env: {
memory: memory,
js_console_log: (ptr, len) => {
const bytes = new Uint8Array(memory.buffer, ptr, len);
const string = new TextDecoder().decode(bytes);
console.log(string);
}
}
};
const { instance } = await WebAssembly.instantiate(bytes, importObject);
instance.exports.game_init();
function frame(timestamp) {
instance.exports.game_update(1/60);
instance.exports.game_render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
loadGame();
Learning milestones:
- WASM compiles → You understand the target
- WebGL renders → You understand browser graphics
- Game plays in browser → You understand the full stack
- Works on mobile → You understand web portability
Project 15: Mini Game Engine
- File: LEARN_ODIN_PROGRAMMING_LANGUAGE.md
- Main Programming Language: Odin
- Alternative Programming Languages: C++, Rust
- Coolness Level: Level 5: Pure Magic
- Business Potential: 5. The “Industry Disruptor”
- Difficulty: Level 5: Master
- Knowledge Area: All of the Above
- Software or Tool: All previous projects combined
- Main Book: “Game Engine Architecture” by Jason Gregory
What you’ll build: A complete 2D/3D game engine combining all previous concepts: renderer, ECS, audio, physics, hot-reload, profiling, and asset pipeline.
Why it teaches Odin: This is the capstone project. Game engines require everything Odin excels at: performance, memory control, data-oriented design, and low-level access. This proves you’ve mastered the language.
Core challenges you’ll face:
- Integrating all subsystems → maps to system architecture
- Clean API design → maps to Odin idioms and patterns
- Performance optimization → maps to profiling and tuning
- Making it usable → maps to ergonomics and polish
Key Concepts:
- Engine Architecture: “Game Engine Architecture”
- All Previous Concepts: Memory, graphics, audio, ECS, etc.
- API Design: Making it pleasant to use
- Documentation: Odin’s built-in doc system
Difficulty: Master Time estimate: 2-3 months Prerequisites: All previous projects (or equivalent experience)
Real world outcome:
$ odin run my_engine_editor
Odin Game Engine v0.1
---------------------
[Editor window opens with scene view, inspector, and console]
Engine Features:
✓ Vulkan/OpenGL/Metal renderer
✓ Entity Component System (SOA-based)
✓ Physics (2D + 3D)
✓ Audio engine with effects
✓ Hot-reload for game code
✓ Asset pipeline (models, textures, audio)
✓ WASM export for web
✓ Built-in profiler
✓ Scripting via Odin DLLs
Create a new game:
1. Write game code using engine API
2. Hot-reload changes instantly
3. Build for native or web
Example game.odin:
-----------------
package game
import engine "my_engine"
@(export)
game_init :: proc(ctx: ^engine.Context) {
player := engine.create_entity(ctx)
engine.add_component(ctx, player, engine.Transform{{0, 0, 0}, {0, 0, 0}, {1, 1, 1}})
engine.add_component(ctx, player, engine.Sprite{"player.png"})
engine.add_component(ctx, player, engine.Rigidbody{mass = 1.0})
}
@(export)
game_update :: proc(ctx: ^engine.Context, dt: f32) {
input := engine.get_input(ctx)
for entity in engine.query(ctx, {Transform, Player}) {
if input.key_down[.SPACE] {
engine.apply_force(ctx, entity, {0, 500, 0})
}
}
}
Implementation Hints:
Engine architecture:
Engine/
├── core/
│ ├── allocators.odin # Custom allocators
│ ├── context.odin # Engine context
│ └── math.odin # Vector/matrix types
├── ecs/
│ ├── world.odin # Entity storage (#soa)
│ ├── components.odin # Built-in components
│ └── systems.odin # System execution
├── graphics/
│ ├── renderer.odin # Abstraction layer
│ ├── vulkan/ # Vulkan backend
│ ├── opengl/ # OpenGL backend
│ └── webgl/ # WebGL backend (WASM)
├── audio/
│ ├── audio.odin # Audio engine
│ └── effects.odin # DSP effects
├── physics/
│ ├── physics2d.odin # 2D physics
│ └── physics3d.odin # 3D physics
├── assets/
│ ├── loader.odin # Asset loading
│ └── packer.odin # Asset packing
└── tools/
├── profiler.odin # Built-in profiler
├── hot_reload.odin # DLL hot-reload
└── editor.odin # Editor UI
Engine context (passed to all systems):
Engine_Context :: struct {
// Allocators
frame_allocator: mem.Arena_Allocator,
persistent_allocator: mem.Tracking_Allocator,
// Subsystems
world: ^ECS_World,
renderer: ^Renderer,
audio: ^Audio_Engine,
physics: ^Physics_World,
input: ^Input_State,
// Frame info
dt: f32,
time: f64,
frame: u64,
// Hot reload
game_dll: dynlib.Library,
game_api: Game_API,
}
The engine loop:
engine_run :: proc(ctx: ^Engine_Context) {
for !ctx.should_quit {
// Frame timing
frame_start := time.now()
ctx.dt = compute_dt(ctx)
ctx.frame += 1
// Reset frame allocator
mem.arena_free_all(&ctx.frame_allocator)
// Check for hot reload
if should_reload_game(ctx) {
reload_game(ctx)
}
// Update
input_update(ctx.input)
ctx.game_api.update(ctx, ctx.dt)
physics_step(ctx.physics, ctx.dt)
ecs_run_systems(ctx.world, ctx.dt)
audio_update(ctx.audio)
// Render
renderer_begin_frame(ctx.renderer)
ctx.game_api.render(ctx)
ecs_render_systems(ctx.world, ctx.renderer)
renderer_end_frame(ctx.renderer)
// Profiler
profiler_end_frame(ctx)
}
}
Learning milestones:
- All subsystems work → You’ve integrated complex systems
- Games can be built with it → It’s actually usable
- Performance is good → You’ve optimized properly
- Others can use it → You’ve written good APIs
Project Comparison Table
| Project | Difficulty | Time | Key Odin Features | Fun Factor |
|---|---|---|---|---|
| 1. Arena Allocator | Intermediate | Weekend | Allocators, context | ⭐⭐⭐ |
| 2. Vector Math SIMD | Intermediate | 1 week | SIMD, array programming | ⭐⭐⭐⭐ |
| 3. JSON Parser | Intermediate | 1 week | Unions, or_return | ⭐⭐⭐ |
| 4. 2D Game Raylib | Intermediate | 2 weeks | defer, vendor libs | ⭐⭐⭐⭐⭐ |
| 5. ECS with SOA | Advanced | 2 weeks | #soa, bit_sets | ⭐⭐⭐⭐ |
| 6. Software Rasterizer | Expert | 3-4 weeks | SIMD, memory | ⭐⭐⭐⭐⭐ |
| 7. Hot-Reload Engine | Advanced | 2 weeks | DLLs, @export | ⭐⭐⭐⭐⭐ |
| 8. Network Protocol | Advanced | 2 weeks | bit_sets, distinct | ⭐⭐⭐⭐ |
| 9. Custom Profiler | Advanced | 1-2 weeks | Tracking allocator | ⭐⭐⭐⭐ |
| 10. Vulkan Renderer | Master | 4-6 weeks | Vendor bindings | ⭐⭐⭐⭐⭐ |
| 11. Particle System | Advanced | 2 weeks | #soa, SIMD | ⭐⭐⭐⭐⭐ |
| 12. Audio Synth | Advanced | 2-3 weeks | Real-time, SIMD | ⭐⭐⭐⭐ |
| 13. Build System | Intermediate | 1-2 weeks | os, threads | ⭐⭐⭐ |
| 14. WASM Game | Advanced | 2-3 weeks | WASM target | ⭐⭐⭐⭐⭐ |
| 15. Mini Engine | Master | 2-3 months | Everything | ⭐⭐⭐⭐⭐ |
Recommended Learning Path
Phase 1: Foundations (3-4 weeks)
- Project 1: Arena Allocator - Understand Odin’s memory model
- Project 2: Vector Math - Learn array programming and SIMD
- Project 3: JSON Parser - Master unions and error handling
Phase 2: Game Development (4-6 weeks)
- Project 4: 2D Game - Build something playable
- Project 5: ECS with SOA - Learn data-oriented design
- Project 7: Hot-Reload - Accelerate development workflow
Phase 3: Advanced Graphics (6-8 weeks)
- Project 6: Software Rasterizer - Understand graphics fundamentals
- Project 10: Vulkan Renderer - Master GPU programming
- Project 11: Particle System - Combine SOA + rendering
Phase 4: Specialization (4-6 weeks)
Choose based on interest:
- Audio: Project 12 (Synthesizer)
- Web: Project 14 (WASM Game)
- Tools: Project 9 (Profiler) + Project 13 (Build System)
- Networking: Project 8 (Protocol)
Phase 5: Mastery (2-3 months)
- Project 15: Mini Game Engine - Combine everything
Total estimated time: 6-12 months (depending on pace and prior experience)
Essential Resources
Official Resources
Books
- “Understanding the Odin Programming Language” by Karl Zylinski - The definitive Odin book
- “Game Programming Patterns” by Robert Nystrom - Architecture patterns
- “Data-Oriented Design” by Richard Fabian - Philosophy behind Odin
- “Computer Graphics from Scratch” by Gabriel Gambetta - Graphics fundamentals
Community
- Odin Discord - Active community
- Handmade Network - Odin - Blog posts and discussions
- awesome-odin - Curated resources
Tutorials
- Karl Zylinski’s Blog - Excellent Odin articles
- Introduction to Odin
- Trying Odin (Interactive)
Real-World Odin Code
- EmberGen Showcase - Production software in Odin
- CAT & ONION - Commercial game in Odin
- Odin + Raylib Template
Summary
| # | Project | Main Language |
|---|---|---|
| 1 | Memory Arena Allocator | Odin |
| 2 | Vector Math Library with SIMD | Odin |
| 3 | JSON Parser with Tagged Unions | Odin |
| 4 | 2D Game with Raylib | Odin |
| 5 | Entity Component System with SOA | Odin |
| 6 | Software Rasterizer | Odin |
| 7 | Hot-Reloading Game Engine | Odin |
| 8 | Network Protocol with bit_sets | Odin |
| 9 | Custom Profiler with Tracking Allocator | Odin |
| 10 | Vulkan Renderer | Odin |
| 11 | Real-Time Particle System | Odin |
| 12 | Audio Synthesizer | Odin |
| 13 | Build System / Task Runner | Odin |
| 14 | WASM Game for the Browser | Odin |
| 15 | Mini Game Engine | Odin |
Sources
- Odin Programming Language Official Site
- Odin GitHub Repository
- Introduction to Odin - Karl Zylinski
- Odin: A Programming Language Made for Me
- Understanding the Odin Programming Language (Book)
- Odin Programming Language Review
- Why Odin Deserves a Place Beside C, Zig, and Rust
- EmberGen Showcase
- JangaFX - EmberGen
- Odin FAQ
- Odin Overview
- Temporary Allocator - Karl Zylinski
- No-engine gamedev using Odin + Raylib
- awesome-odin on GitHub
- Exceptions and Why Odin Will Never Have Them
- Marketing the Odin Programming Language is Weird