← Back to all projects

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

  1. Game Development: Built-in graphics bindings, array programming, swizzling
  2. Real-Time Graphics: SOA, SIMD, matrices, quaternions out of the box
  3. Data-Oriented Design: First-class SOA support, cache-friendly by design
  4. Rapid Prototyping: Simple syntax, fast compilation, hot reloading support
  5. 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:

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:

  1. Basic allocation works → You understand the allocator interface
  2. Alignment is correct → You understand memory layout
  3. Reset is instant → You understand arena efficiency
  4. 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:

  1. Vector ops work → You understand array programming
  2. Swizzling feels natural → You understand Odin’s expressiveness
  3. Matrix multiply is fast → You understand SIMD benefits
  4. 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:

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:

  1. Can parse primitives → You understand basic parsing and unions
  2. Nested structures work → You understand recursive descent
  3. Errors are clear → You understand or_return idiom
  4. 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:

Resources for key challenges:

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:

  1. Window opens, player renders → You understand Raylib basics
  2. Smooth movement works → You understand game physics
  3. Collisions work → You understand game logic
  4. 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:

  1. SOA transformation works → You understand #soa mechanics
  2. Performance difference is measurable → You understand cache effects
  3. Component queries work → You understand bit_sets
  4. 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:

  1. Triangles render → You understand rasterization
  2. Z-buffer works → You understand depth testing
  3. Textures map correctly → You understand UV interpolation
  4. 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:

  1. DLL loads and runs → You understand dynamic linking
  2. State persists across reload → You understand memory ownership
  3. Changes appear instantly → You’ve built hot reload!
  4. 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:

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:

  1. Client connects to server → You understand sockets
  2. Messages serialize/deserialize → You understand binary formats
  3. Multiple message types work → You understand protocol design
  4. 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:

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:

  1. Allocations tracked → You understand allocator wrapping
  2. Leaks detected → You understand the tracking pattern
  3. Timings recorded → You understand deferred cleanup
  4. 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:

  1. Triangle renders → You understand Vulkan basics
  2. 3D mesh with textures → You understand resource management
  3. Smooth camera movement → You understand frame synchronization
  4. 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:

  • positions are contiguous in memory
  • velocities are 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:

  1. Particles spawn and die → You understand object pooling
  2. 100k particles run smoothly → You understand SOA benefits
  3. Effects look good → You understand particle aesthetics
  4. 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:

  1. Sine wave plays → You understand audio basics
  2. Multiple oscillators mix → You understand additive synthesis
  3. Filter sweeps work → You understand DSP state
  4. 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:

  1. Tasks run in order → You understand dependency resolution
  2. Parallel execution works → You understand Odin threading
  3. Watch mode works → You understand file system APIs
  4. 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:

  1. WASM compiles → You understand the target
  2. WebGL renders → You understand browser graphics
  3. Game plays in browser → You understand the full stack
  4. 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:

  1. All subsystems work → You’ve integrated complex systems
  2. Games can be built with it → It’s actually usable
  3. Performance is good → You’ve optimized properly
  4. 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 ⭐⭐⭐⭐⭐

Phase 1: Foundations (3-4 weeks)

  1. Project 1: Arena Allocator - Understand Odin’s memory model
  2. Project 2: Vector Math - Learn array programming and SIMD
  3. Project 3: JSON Parser - Master unions and error handling

Phase 2: Game Development (4-6 weeks)

  1. Project 4: 2D Game - Build something playable
  2. Project 5: ECS with SOA - Learn data-oriented design
  3. Project 7: Hot-Reload - Accelerate development workflow

Phase 3: Advanced Graphics (6-8 weeks)

  1. Project 6: Software Rasterizer - Understand graphics fundamentals
  2. Project 10: Vulkan Renderer - Master GPU programming
  3. 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)

  1. 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

Tutorials

Real-World Odin Code


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