diff --git a/odin/sdl-opengl-rendering/flake.nix b/odin/sdl-opengl-rendering/flake.nix
index e5eaa3a..b358b50 100644
--- a/odin/sdl-opengl-rendering/flake.nix
+++ b/odin/sdl-opengl-rendering/flake.nix
@@ -46,11 +46,19 @@
       system:
       let
         pkgs = inputs.nixpkgs.legacyPackages.${system}.extend inputs.self.overlays.default;
-        raylib = pkgs.raylib.override { platform = "SDL"; };
+        cross = import inputs.nixpkgs {
+          localSystem = system;
+          crossSystem.config = "x86_64-w64-mingw32";
+        };
+
         inherit (pkgs) lib;
       in
       {
-        packages = { inherit (pkgs) odin; };
+        packages = {
+          inherit (pkgs) odin;
+          gcc = cross.callPackage (import ./gcc.nix) { };
+          libgl = cross.libGL;
+        };
 
         # {{{ Shell
         devShell = pkgs.mkShell rec {
@@ -65,18 +73,19 @@
             pkgs.gdb # Debugger
             pkgs.seer # Debugger GUI
             pkgs.valgrind # Detect memory leaks
+            pkgs.renderdoc # Graphics debugger
           ];
 
           buildInputs = [
-            # pkgs.libGL
-            # pkgs.libxkbcommon
-            # pkgs.xorg.libXi
-            # pkgs.xorg.libX11
-            # pkgs.xorg.libXrandr
-            # pkgs.xorg.libXinerama
-            # pkgs.xorg.libXcursor
-            # pkgs.wayland
             pkgs.sdl3
+
+            pkgs.xorg.libX11
+            pkgs.xorg.libXScrnSaver
+            pkgs.xorg.libXcursor
+            pkgs.xorg.libXext
+            pkgs.xorg.libXfixes
+            pkgs.xorg.libXi
+            pkgs.xorg.libXrandr
           ];
 
           LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath buildInputs;
diff --git a/odin/sdl-opengl-rendering/src/main.odin b/odin/sdl-opengl-rendering/src/main.odin
index 015f77b..9eb33fb 100644
--- a/odin/sdl-opengl-rendering/src/main.odin
+++ b/odin/sdl-opengl-rendering/src/main.odin
@@ -4,17 +4,16 @@ import "base:runtime"
 import "core:c"
 import "core:fmt"
 import "core:log"
+import "core:math"
 import "vendor:OpenGL"
 import "vendor:sdl3"
 
 State :: struct {
-	window:              ^sdl3.Window,
-	program:             u32,
-	vertex_pos_location: u32,
-	vao:                 u32,
-	vbo:                 u32,
-	ibo:                 u32,
-	wireframe:           bool,
+	tick:      u32,
+	window:    ^sdl3.Window,
+	program:   u32,
+	rect_vao:  VAO,
+	wireframe: bool,
 }
 
 init :: proc() -> (state: State, ok: bool) {
@@ -67,6 +66,11 @@ init :: proc() -> (state: State, ok: bool) {
 	)
 	// }}}
 
+	sdl3.SetAppMetadata(
+		"odin-rendering-experiments",
+		"<hash-here>",
+		"dev.moonythm.odin-rendering-experiments",
+	) or_return
 	sdl3.Init(sdl3.InitFlags{.VIDEO}) or_return
 	sdl3.GL_SetAttribute(.CONTEXT_MAJOR_VERSION, GL_MAJOR) or_return
 	sdl3.GL_SetAttribute(.CONTEXT_MINOR_VERSION, GL_MINOR) or_return
@@ -87,60 +91,24 @@ init :: proc() -> (state: State, ok: bool) {
 	(gl_ctx != nil) or_return
 
 	OpenGL.load_up_to(GL_MAJOR, GL_MINOR, sdl3.gl_set_proc_address)
+	OpenGL.ClearColor(0, 0, 0, 1)
+	OpenGL.Enable(OpenGL.DEPTH_TEST)
 
 	state.program = OpenGL.load_shaders_source(
 		#load("./vert.glsl"),
 		#load("./frag.glsl"),
 	) or_return
-	// log.debug("Doing Opengl stuff")
 
-	vertex_pos_location := OpenGL.GetAttribLocation(state.program, "aPos")
-	(vertex_pos_location != -1) or_return
-	state.vertex_pos_location = u32(vertex_pos_location)
+	state.rect_vao = create_vao({{-1, -1}, {1, -1}, {1, 1}, {-1, 1}}, {0, 1, 2, 3}) or_return
 
-	OpenGL.ClearColor(0, 0, 0, 1)
-
-	// VBO data
-	vertex_data := 2 * [?]f32{-0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5}
-
-	// IBO data
-	index_data := [?]u32{0, 1, 2, 3}
-
-	OpenGL.GenVertexArrays(1, &state.vao)
-	OpenGL.BindVertexArray(state.vao)
-
-	//Create VBO
-	OpenGL.GenBuffers(1, &state.vbo)
-	OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, state.vbo)
-	OpenGL.BufferData(OpenGL.ARRAY_BUFFER, 2 * 4 * size_of(f32), &vertex_data, OpenGL.STATIC_DRAW)
-
-	OpenGL.EnableVertexAttribArray(state.vertex_pos_location)
-	OpenGL.VertexAttribPointer(
-		state.vertex_pos_location,
-		2,
-		OpenGL.FLOAT,
-		false,
-		2 * size_of(f32),
-		0,
-	)
-
-	OpenGL.GenBuffers(1, auto_cast &state.ibo)
-	OpenGL.BindBuffer(OpenGL.ELEMENT_ARRAY_BUFFER, state.ibo)
-	OpenGL.BufferData(
-		OpenGL.ELEMENT_ARRAY_BUFFER,
-		4 * size_of(u32),
-		&index_data,
-		OpenGL.STATIC_DRAW,
-	)
-
-	OpenGL.BindVertexArray(0)
-	OpenGL.DisableVertexAttribArray(state.vertex_pos_location)
+	init_command_queue()
 
 	return state, true
 }
 
-render :: proc(state: State) {
-	OpenGL.Clear(OpenGL.COLOR_BUFFER_BIT)
+render :: proc(state: ^State) {
+	state.tick += 1
+	OpenGL.Clear(OpenGL.COLOR_BUFFER_BIT | OpenGL.DEPTH_BUFFER_BIT)
 
 	OpenGL.UseProgram(state.program)
 	defer OpenGL.UseProgram(0)
@@ -151,8 +119,23 @@ render :: proc(state: State) {
 		OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, OpenGL.FILL)
 	}
 
-	OpenGL.BindVertexArray(state.vao)
-	OpenGL.DrawElements(OpenGL.TRIANGLE_FAN, 4, OpenGL.UNSIGNED_INT, nil)
+	draw_rect({-0.5, 0}, {0.75, 0.5}, {1, 0, 0, 1}, z = 0.5)
+	draw_rect({0.5, 0.25}, {0.3, 0.5}, {0, 1, 0, 1}, z = -0.5)
+
+	count := ℝ(32)
+	for x in ℝ(0) ..< count {
+		for y in ℝ(0) ..< count {
+			i := x * count + y
+			pos :=
+				ℝ²{x + math.sin_f32(2 * math.π * (ℝ(state.tick) + i) / 60), y} *
+					2 /
+					ℝ(count) -
+				1
+			draw_rect(Rect{pos, 1 / ℝ(count), 0, {(pos.x + 1) / 2, (pos.y + 1) / 2, 1, 1}})
+		}
+	}
+
+	render_queue(state)
 }
 
 close :: proc(state: State) {
@@ -194,11 +177,12 @@ main :: proc() {
 				case 'w':
 					state.wireframe = !state.wireframe
 				}
-				log.debug(event.text)
 			}
 		}
 
-		render(state)
+		render(&state)
 		sdl3.GL_SwapWindow(state.window)
+
+		free_all(context.temp_allocator)
 	}
 }
diff --git a/odin/sdl-opengl-rendering/src/shape.odin b/odin/sdl-opengl-rendering/src/shape.odin
new file mode 100644
index 0000000..2ccc6d3
--- /dev/null
+++ b/odin/sdl-opengl-rendering/src/shape.odin
@@ -0,0 +1,195 @@
+package visuals
+
+import "core:log"
+import "vendor:OpenGL"
+
+// {{{ Math constants
+ℝ :: f32
+ℝ² :: [2]ℝ
+ℝ³ :: [3]ℝ
+ℕ :: uint
+Mat3 :: matrix[3, 3]ℝ
+Color :: [4]ℝ
+// }}}
+// {{{ Command queues
+Rect :: struct {
+	top_left:   ℝ²,
+	dimensions: ℝ²,
+	z:          ℝ,
+	fill:       Color,
+}
+
+Circle :: struct {
+	center: ℝ²,
+	radius: ℝ,
+}
+
+Shape :: union {
+	Rect,
+	Circle,
+}
+
+Command_Queue :: struct {
+	rects: [dynamic]Rect,
+}
+
+queue: ^Command_Queue
+
+init_command_queue :: proc() {
+	queue = new(Command_Queue)
+	queue^ = Command_Queue {
+		rects = make([dynamic]Rect),
+	}
+}
+
+draw_rect_args :: proc(top_left: ℝ², dimensions: ℝ², color: Color, z: ℝ = 0) {
+	draw_rect_struct(Rect{top_left, dimensions, z, color})
+}
+
+draw_rect_struct :: proc(rect: Rect) {
+	append(&queue.rects, rect)
+}
+
+draw_rect :: proc {
+	draw_rect_args,
+	draw_rect_struct,
+}
+
+// }}}
+
+// {{{ VAO & consts
+VAO :: struct {
+	vao:                  u32,
+	ibo:                  u32,
+	vertex_pos_buffer:    u32,
+	instance_fill_buffer: u32,
+	instance_mat_buffer:  u32,
+	index_count:          ℕ,
+}
+
+INSTANCES :: 1024 // The number of instances to allocate space in the buffer for
+VERTEX_POS_LOCATION :: 0
+INSTANCE_FILL_LOCATION :: 1
+INSTANCE_MAT_LOCATION :: 2
+// }}}
+// {{{ Create VAO
+create_vao :: proc(vertices: []ℝ², indices: []u32) -> (out: VAO, ok: bool) {
+	out.index_count = len(indices)
+
+	// Create VAO
+	OpenGL.GenVertexArrays(1, &out.vao)
+	OpenGL.BindVertexArray(out.vao)
+
+	// Create instance fill VBO
+	OpenGL.GenBuffers(1, auto_cast &out.instance_fill_buffer)
+	OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, out.instance_fill_buffer)
+
+	OpenGL.EnableVertexAttribArray(INSTANCE_FILL_LOCATION)
+	OpenGL.VertexAttribPointer(INSTANCE_FILL_LOCATION, 4, OpenGL.FLOAT, false, size_of(Color), 0)
+	OpenGL.VertexAttribDivisor(INSTANCE_FILL_LOCATION, 1)
+
+	// Create instance mat VBO
+	OpenGL.GenBuffers(1, auto_cast &out.instance_mat_buffer)
+	OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, out.instance_mat_buffer)
+
+	for i in u32(0) ..< 3 {
+		OpenGL.EnableVertexAttribArray(INSTANCE_MAT_LOCATION + i)
+		vec3_size :: size_of(ℝ³)
+		OpenGL.VertexAttribPointer(
+			INSTANCE_MAT_LOCATION + i,
+			3,
+			OpenGL.FLOAT,
+			false,
+			3 * vec3_size,
+			uintptr(i * vec3_size),
+		)
+		OpenGL.VertexAttribDivisor(INSTANCE_MAT_LOCATION + i, 1)
+	}
+
+	//Create position VBO
+	OpenGL.GenBuffers(1, &out.vertex_pos_buffer)
+	OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, out.vertex_pos_buffer)
+	OpenGL.BufferData(
+		OpenGL.ARRAY_BUFFER,
+		len(vertices) * 2 * size_of(ℝ),
+		raw_data(vertices),
+		OpenGL.STATIC_DRAW,
+	)
+
+	// Put the data into the VBO
+	OpenGL.EnableVertexAttribArray(VERTEX_POS_LOCATION)
+	OpenGL.VertexAttribPointer(VERTEX_POS_LOCATION, 2, OpenGL.FLOAT, false, 2 * size_of(ℝ), 0)
+
+	// Create the IBO
+	OpenGL.GenBuffers(1, auto_cast &out.ibo)
+	OpenGL.BindBuffer(OpenGL.ELEMENT_ARRAY_BUFFER, out.ibo)
+	OpenGL.BufferData(
+		OpenGL.ELEMENT_ARRAY_BUFFER,
+		len(indices) * size_of(u32),
+		raw_data(indices),
+		OpenGL.STATIC_DRAW,
+	)
+
+	OpenGL.BindVertexArray(0)
+
+	return out, true
+}
+// }}}
+// {{{ Set rect transforms
+set_rect_transforms :: proc(vao: ^VAO, rects: []Rect) -> ℕ {
+	log.assert(len(rects) <= INSTANCES, "Attempting to send too many rects to the GPU")
+	matrices := new([INSTANCES]Mat3, context.temp_allocator)
+	fills := new([INSTANCES]Color, context.temp_allocator)
+
+	for rect, i in rects {
+		// This matrix must transform the rect [-1, 1]² into the desired rect
+		mat: Mat3
+
+		center := rect.top_left + rect.dimensions / 2
+		mat[0, 0] = rect.dimensions.x / 2
+		mat[1, 1] = rect.dimensions.y / 2
+		mat[2].xy = center.xy
+		mat[2].z = rect.z
+
+		matrices[i] = mat
+		fills[i] = rect.fill
+	}
+
+	OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, vao.instance_mat_buffer)
+	OpenGL.BufferData(
+		OpenGL.ARRAY_BUFFER,
+		INSTANCES * size_of(Mat3),
+		matrices,
+		OpenGL.DYNAMIC_DRAW,
+	)
+	OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, vao.instance_fill_buffer)
+	OpenGL.BufferData(OpenGL.ARRAY_BUFFER, INSTANCES * size_of(Color), fills, OpenGL.DYNAMIC_DRAW)
+
+	return len(rects)
+}
+// }}}
+// {{{ Render the entire queue
+draw_instances :: proc(vao: VAO, instances: ℕ) {
+	OpenGL.BindVertexArray(vao.vao)
+	OpenGL.DrawElementsInstanced(
+		OpenGL.TRIANGLE_FAN,
+		i32(vao.index_count),
+		OpenGL.UNSIGNED_INT,
+		nil,
+		i32(instances),
+	)
+}
+
+render_queue :: proc(state: ^State) {
+	rect_steps := len(queue.rects) / INSTANCES
+
+	for i := 0; i < len(queue.rects); i += INSTANCES {
+		slice := queue.rects[i:]
+		if len(slice) > INSTANCES {slice = slice[:INSTANCES]}
+		instances := set_rect_transforms(&state.rect_vao, slice)
+		draw_instances(state.rect_vao, instances)
+	}
+
+	clear(&queue.rects)
+}
+// }}}
diff --git a/odin/sdl-opengl-rendering/src/vert.glsl b/odin/sdl-opengl-rendering/src/vert.glsl
index 3273106..a5761e3 100644
--- a/odin/sdl-opengl-rendering/src/vert.glsl
+++ b/odin/sdl-opengl-rendering/src/vert.glsl
@@ -1,10 +1,13 @@
 #version 330
 
-in vec2 aPos;
+layout (location = 0) in vec2 aPos;
+layout (location = 1) in vec4 instanceFill;
+layout (location = 2) in mat3 instanceMatrix;
 
 out vec4 vertexColor;
 
 void main() {
-  gl_Position = vec4(aPos.x, aPos.y, 0, 1);
-  vertexColor = vec4((aPos.x + 1) / 2, (aPos.y + 1) / 2, 1, 1);
+  vec3 pos    = instanceMatrix * vec3(aPos.xy, 1);
+  gl_Position = vec4(pos.xyz, 1);
+  vertexColor = instanceFill;
 }