From b661791c7383fc226d26d754649867e87861c12a Mon Sep 17 00:00:00 2001
From: prescientmoon <git@moonythm.dev>
Date: Thu, 5 Jun 2025 04:27:32 +0200
Subject: [PATCH] odin(sdl-opengl-rendering): implement JFA

---
 odin/sdl-opengl-rendering/README.md           |   4 +-
 odin/sdl-opengl-rendering/flake.lock          |  17 +
 odin/sdl-opengl-rendering/flake.nix           |  12 +
 odin/sdl-opengl-rendering/src/external.odin   |   4 +
 odin/sdl-opengl-rendering/src/main.odin       | 175 +++++-----
 odin/sdl-opengl-rendering/src/sdl.odin        |  56 ++-
 .../src/shaders/jfa-seed.frag.glsl            |  16 +
 .../src/shaders/jfa.frag.glsl                 |  39 +++
 .../src/shaders/jfa.vert.glsl                 |  14 +
 .../src/shaders/{frag.glsl => sdf.frag.glsl}  |   8 +-
 .../src/shaders/{vert.glsl => sdf.vert.glsl}  |   7 +-
 odin/sdl-opengl-rendering/src/shape.odin      | 326 ++++++++++++++----
 12 files changed, 509 insertions(+), 169 deletions(-)
 create mode 100644 odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl
 create mode 100644 odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl
 create mode 100644 odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl
 rename odin/sdl-opengl-rendering/src/shaders/{frag.glsl => sdf.frag.glsl} (86%)
 rename odin/sdl-opengl-rendering/src/shaders/{vert.glsl => sdf.vert.glsl} (57%)

diff --git a/odin/sdl-opengl-rendering/README.md b/odin/sdl-opengl-rendering/README.md
index f99c5cd..279d398 100644
--- a/odin/sdl-opengl-rendering/README.md
+++ b/odin/sdl-opengl-rendering/README.md
@@ -6,9 +6,9 @@ Methods to convert:
 [x] unset_clip_rect
 [x] clear_background
 [x] draw_rectangle
-[ ] draw_rectangle_lines
+[x] draw_rectangle_lines
 [x] draw_circle
-[ ] draw_circle_lines
+[x] draw_circle_lines
 [x] draw_line
 [ ] draw_triangle
 [ ] draw_circle_arc_lines_impl
diff --git a/odin/sdl-opengl-rendering/flake.lock b/odin/sdl-opengl-rendering/flake.lock
index 45c1a6f..78b730a 100644
--- a/odin/sdl-opengl-rendering/flake.lock
+++ b/odin/sdl-opengl-rendering/flake.lock
@@ -18,6 +18,22 @@
         "type": "github"
       }
     },
+    "glsl_analyzer": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1749076974,
+        "narHash": "sha256-KnJPO3eVDfaHDwDrE1s0YvkVxpjMXtG8JlhqUe3G+30=",
+        "owner": "starlitcanopy",
+        "repo": "glsl_analyzer",
+        "rev": "0209e194a53370416d4f02487031740fc1ff913a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "starlitcanopy",
+        "repo": "glsl_analyzer",
+        "type": "github"
+      }
+    },
     "nixpkgs": {
       "locked": {
         "lastModified": 1748693115,
@@ -86,6 +102,7 @@
     "root": {
       "inputs": {
         "flake-utils": "flake-utils",
+        "glsl_analyzer": "glsl_analyzer",
         "nixpkgs": "nixpkgs",
         "odin": "odin",
         "ols": "ols"
diff --git a/odin/sdl-opengl-rendering/flake.nix b/odin/sdl-opengl-rendering/flake.nix
index fa10e25..9118c7d 100644
--- a/odin/sdl-opengl-rendering/flake.nix
+++ b/odin/sdl-opengl-rendering/flake.nix
@@ -11,6 +11,9 @@
     ols.inputs.nixpkgs.follows = "nixpkgs";
     ols.inputs.flake-utils.follows = "flake-utils";
     ols.inputs.odin.follows = "odin";
+
+    glsl_analyzer.url = "github:starlitcanopy/glsl_analyzer";
+    glsl_analyzer.flake = false;
   };
 
   outputs =
@@ -26,6 +29,14 @@
           ];
         };
 
+        glsl_analyzer = pkgs.glsl_analyzer.overrideAttrs (_: {
+          src = inputs.glsl_analyzer;
+
+          nativeBuildInputs = [
+            pkgs.zig_0_14.hook
+          ];
+        });
+
         inherit (pkgs) lib;
       in
       {
@@ -42,6 +53,7 @@
             pkgs.seer # Debugger GUI
             pkgs.valgrind # Detect memory leaks
             pkgs.renderdoc # Graphics debugger
+            glsl_analyzer # GLSL language server
           ];
 
           buildInputs = [
diff --git a/odin/sdl-opengl-rendering/src/external.odin b/odin/sdl-opengl-rendering/src/external.odin
index e501ba5..0b97717 100644
--- a/odin/sdl-opengl-rendering/src/external.odin
+++ b/odin/sdl-opengl-rendering/src/external.odin
@@ -1,3 +1,5 @@
+// This file contains types/constants/functions that are already implemented
+// in my proper projects.
 package visuals
 
 ℝ :: f32
@@ -7,6 +9,7 @@ package visuals
 Mat3 :: matrix[3, 3]ℝ
 Mat4 :: matrix[4, 4]ℝ
 Color :: [4]ℝ
+Affine2 :: matrix[2, 3]ℝ
 
 AABB :: struct {
 	top_left:   ℝ²,
@@ -29,6 +32,7 @@ vec2_perp :: proc(v: ℝ²) -> ℝ² {
 	return {-v.y, v.x}
 }
 
+@(private = "file")
 g_state: State
 g_renderer_state :: proc() -> ^State {
 	return &g_state
diff --git a/odin/sdl-opengl-rendering/src/main.odin b/odin/sdl-opengl-rendering/src/main.odin
index 27b20d7..432b3fa 100644
--- a/odin/sdl-opengl-rendering/src/main.odin
+++ b/odin/sdl-opengl-rendering/src/main.odin
@@ -11,91 +11,96 @@ render :: proc() {
 	state := g_renderer_state()
 	state.tick += 1
 
-	dims := screen_dimensions()
-	center := dims / 2
-	draw_rect(ℝ²{30, 20}, ℝ²{100, 200}, Shape_Options{fill = {1, 0, 0, 1}, z = 0.4})
-	draw_rect(
-		10,
-		center + center * math.sin(f32(state.tick) / 60),
-		Shape_Options{fill = {0, 1, 0, 1}, z = 0.6},
-	)
-	draw_rect(
-		ℝ²{1000, 800},
-		center / 3,
-		Shape_Options{fill = {0, 1, 1, 1}, z = 0.5, stroke = {0, 0.5, 0.5, 1}, stroke_width = 3},
-	)
+	// dims := screen_dimensions()
+	// center := dims / 2
+	// draw_rect(ℝ²{30, 20}, ℝ²{100, 200}, Shape_Options{fill = {1, 0, 0, 1}, z = 0.4})
+	// draw_rect(
+	// 	10,
+	// 	center + center * math.sin(f32(state.tick) / 60),
+	// 	Shape_Options{fill = {0, 1, 0, 1}, z = 0.6},
+	// )
+	// draw_rect(
+	// 	ℝ²{1000, 800},
+	// 	center / 3,
+	// 	Shape_Options{fill = {0, 1, 1, 1}, z = 0.5, stroke = {0, 0.5, 0.5, 1}, stroke_width = 3},
+	// )
+	//
+	// // count := math.pow(2, math.mod(ℝ(state.tick) / 30, 7))
+	// 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} / ℝ(count)
+	// 		color := Color{(pos.x + 1) / 2, (pos.y + 1) / 2, 1, 1}
+	//
+	// 		r := dims.x / ℝ(count) / 4
+	// 		pos = pos * dims
+	// 		// pos.y = 2 * center.y - pos.y
+	// 		opts := Shape_Options {
+	// 			fill = color,
+	// 			z    = 0.3,
+	// 		}
+	// 		opts.stroke.a = 1
+	// 		opts.stroke.rgb = 1 - opts.fill.rgb
+	// 		opts.stroke_width = 1
+	//
+	// 		if x > y {
+	// 			draw_rect(pos, 2 * r, opts)
+	// 		} else {
+	// 			draw_circle(pos + r, r, opts)
+	// 		}
+	// 	}
+	// }
+	//
+	// rect := □{center / 2, center / 2 + center * math.sin(f32(state.tick) / 120)}
+	// draw_rect(rect, Shape_Options{fill = {1, 0.6, 0.85, 0.3}})
+	//
+	// // set_clip_rect(rect)
+	// draw_circle(
+	// 	center + center * {-0.25, -0.3},
+	// 	450,
+	// 	Shape_Options {
+	// 		fill = {0, 0, 0.5, 0.75},
+	// 		z = -0.1,
+	// 		stroke = Color{0.7, 0.85, 1, 1},
+	// 		stroke_width = 1,
+	// 	},
+	// )
+	//
+	// draw_line(
+	// 	Line{ℝ²{750, 200}, ℝ²{1800, 1600}, 10},
+	// 	Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5},
+	// 	rounded = false,
+	// )
+	//
+	// draw_line(
+	// 	Line{ℝ²{750, 1000}, ℝ²{1200, 1000}, 5},
+	// 	Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5},
+	// )
+	//
+	// draw_line(
+	// 	Line{ℝ²{1200, 1000}, ℝ²{300, 450}, 5},
+	// 	Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5},
+	// )
+	//
+	// draw_line(
+	// 	Line{ℝ²{230, 1000}, ℝ²{1700, 350}, 20},
+	// 	Shape_Options {
+	// 		fill = Color{1, 0, 1, 0.3},
+	// 		z = -0.7,
+	// 		stroke_width = 3,
+	// 		stroke = Color{0.9, 0.68, 0.8, 1},
+	// 	},
+	// )
+	//
+	// render_queue()
 
-	count := math.pow(2, math.mod(ℝ(state.tick) / 30, 10))
-	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} / ℝ(count)
-			color := Color{(pos.x + 1) / 2, (pos.y + 1) / 2, 1, 1}
-
-			r := dims.x / ℝ(count) / 4
-			pos = pos * dims
-			// pos.y = 2 * center.y - pos.y
-			opts := Shape_Options {
-				fill = color,
-				z    = 0.3,
-			}
-			opts.stroke.a = 1
-			opts.stroke.rgb = 1 - opts.fill.rgb
-			opts.stroke_width = 1
-
-			if x > y {
-				draw_rect(pos, 2 * r, opts)
-			} else {
-				draw_circle(pos + r, r, opts)
-			}
-		}
-	}
-
-	rect := □{center / 2, center / 2 + center * math.sin(f32(state.tick) / 120)}
-	draw_rect(rect, Shape_Options{fill = {1, 0.6, 0.85, 0.3}})
-
-	// set_clip_rect(rect)
-	draw_circle(
-		center + center * {-0.25, -0.3},
-		450,
-		Shape_Options {
-			fill = {0, 0, 0.5, 0.75},
-			z = -0.1,
-			stroke = Color{0.7, 0.85, 1, 1},
-			stroke_width = 1,
-		},
-	)
-
-	draw_line(
-		Line{ℝ²{750, 200}, ℝ²{1800, 1600}, 10},
-		Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5},
-		rounded = false,
-	)
-
-	draw_line(
-		Line{ℝ²{750, 1000}, ℝ²{1200, 1000}, 5},
-		Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5},
-	)
-
-	draw_line(
-		Line{ℝ²{1200, 1000}, ℝ²{300, 450}, 5},
-		Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5},
-	)
-
-	draw_line(
-		Line{ℝ²{230, 1000}, ℝ²{1700, 350}, 20},
-		Shape_Options {
-			fill = Color{1, 0, 1, 0.3},
-			z = -0.7,
-			stroke_width = 3,
-			stroke = Color{0.9, 0.68, 0.8, 1},
-		},
-	)
-
-	// unset_clip_rect()
-
-	render_queue()
+	draw_rect(ℝ²{100, 100}, ℝ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}})
+	draw_rect(ℝ²{100, 400}, ℝ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}})
+	draw_rect(ℝ²{400, 100}, ℝ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}})
+	draw_circle(ℝ²{1200, 350}, 200, Shape_Options{fill = {1, 0, 0, 1}})
+	jfa()
 }
 // }}}
 // {{{ Main
@@ -130,6 +135,8 @@ main :: proc() {
 				switch ([^]u8)(event.text.text)[0] {
 				case 'w':
 					g_renderer_state().wireframe = !g_renderer_state().wireframe
+				case 'p':
+					g_renderer_state().pass = Render_Pass((u8(g_renderer_state().pass) + 1) % 3)
 				}
 			}
 		}
diff --git a/odin/sdl-opengl-rendering/src/sdl.odin b/odin/sdl-opengl-rendering/src/sdl.odin
index 5af22f4..fee33d2 100644
--- a/odin/sdl-opengl-rendering/src/sdl.odin
+++ b/odin/sdl-opengl-rendering/src/sdl.odin
@@ -7,6 +7,13 @@ import "core:log"
 import "vendor:OpenGL"
 import "vendor:sdl3"
 
+// For debugging purposes, we allow drawing up to any given pass
+Render_Pass :: enum {
+	SDF,
+	JFA_Seed,
+	JFA,
+}
+
 State :: struct {
 	window:               ^sdl3.Window,
 
@@ -19,31 +26,32 @@ State :: struct {
 
 	// GPU data
 	rect_mesh:            Mesh,
-	ubo_globals:          UBO,
+	ubos:                 [UBO_ID]UBO,
+	framebuffers:         [Framebuffer_ID]FBO,
+	instance_buffers:     [Instance_Param_Buf]u32,
 
 	// Programs
 	rect_program:         Program,
 	circle_program:       Program,
 	line_program:         Program,
 	rounded_line_program: Program,
+	jfa_program:          Program,
+	jfa_seed_program:     Program,
 
 	// Instance buffers (CPU)
-	buf_matrices:         [INSTANCES]Mat3,
+	buf_matrices:         [INSTANCES]Affine2,
 	buf_colors:           [INSTANCES]Color,
 	buf_lines:            [INSTANCES][2]ℝ²,
 	buf_floats:           [INSTANCES]ℝ,
 	buf_vecs:             [INSTANCES]ℝ²,
 
-	// Instance buffers (GPU)
-	instance_buffers:     [Instance_Param_Buf]u32,
-
 	// Flags
 	tick:                 u32,
 	wireframe:            bool,
+	pass:                 Render_Pass,
 	globals:              Global_Uniforms,
 }
 
-
 // {{{ Screen dimensions
 screen_dimensions :: proc() -> ℝ² {
 	w, h: i32
@@ -149,7 +157,7 @@ sdl_init :: proc() -> (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.ClearColor(0, 0, 0, 0)
 	OpenGL.Enable(OpenGL.DEPTH_TEST)
 	OpenGL.Enable(OpenGL.BLEND)
 	OpenGL.BlendFunc(OpenGL.SRC_ALPHA, OpenGL.ONE_MINUS_SRC_ALPHA)
@@ -158,56 +166,64 @@ sdl_init :: proc() -> (ok: bool) {
 	sdl_on_resize(screen_dimensions())
 	// }}}
 	// {{{ Initialize GPU buffers & programs
-	// Initialize GPU buffers
+	// Initialize instance buffers
 	OpenGL.GenBuffers(len(state.instance_buffers), ([^]u32)(&state.instance_buffers))
 
+	// Initialize UBOs
+	OpenGL.GenBuffers(len(state.ubos), ([^]u32)(&state.ubos))
+	for ubo, i in state.ubos {
+		OpenGL.BindBufferBase(OpenGL.UNIFORM_BUFFER, UBO_ID_BINDING[i], ubo)
+	}
+
 	// Initialize meshes
 	state.rect_mesh = create_mesh({{0, 0}, {1, 0}, {1, 1}, {0, 1}}, {0, 1, 2, 3})
 
 	// Initialize programs
 	state.rect_program = gen_program(
 		{
+			template = .SDF,
 			sdf_name = "sdf_rect",
 			sdf_args = {"v_center", "v_dimensions"},
 			params = {{buf = .Center, name = "center"}, {buf = .Dimensions, name = "dimensions"}},
-			id = 0,
 		},
 	) or_return
 
 	state.circle_program = gen_program(
 		{
+			template = .SDF,
 			sdf_name = "sdf_circle",
 			sdf_args = {"v_center", "v_radius"},
 			params = {{buf = .Center, name = "center"}, {buf = .Radius, name = "radius"}},
-			id = 1,
 		},
 	) or_return
 
 	state.rounded_line_program = gen_program(
 		{
+			template = .SDF,
 			sdf_name = "sdf_line",
 			sdf_args = {"v_line", "v_thickness"},
 			params = {{buf = .Line, name = "line"}, {buf = .Thickness, name = "thickness"}},
-			id = 2,
 		},
 	) or_return
 
 	state.rounded_line_program = gen_program(
 		{
+			template = .SDF,
 			sdf_name = "sdf_rounded_line",
 			sdf_args = {"v_line", "v_thickness"},
 			params = {{buf = .Line, name = "line"}, {buf = .Thickness, name = "thickness"}},
-			id = 3,
 		},
 	) or_return
 
-	state.ubo_globals = create_ubo_globals()
+	state.jfa_seed_program = gen_program({template = .JFA_Seed}) or_return
+	state.jfa_program = gen_program({template = .JFA}) or_return
 	// }}}
 
 	state.q_rects = make([dynamic]Shape(□))
 	state.q_circles = make([dynamic]Shape(Circle2))
 	state.q_lines = make([dynamic]Shape(Line))
 	state.q_rounded_lines = make([dynamic]Shape(Rounded_Line))
+	state.pass = .JFA
 
 	return true
 }
@@ -216,6 +232,12 @@ sdl_init :: proc() -> (ok: bool) {
 sdl_close :: proc() {
 	state := g_renderer_state()
 
+	// TODO: perhaps perform some cleanup here?
+
+	for fb in state.framebuffers {
+		destroy_framebuffer(fb)
+	}
+
 	_ = sdl3.StopTextInput(state.window)
 	sdl3.DestroyWindow(state.window)
 	sdl3.Quit()
@@ -223,6 +245,7 @@ sdl_close :: proc() {
 // }}}
 // {{{ Resize
 sdl_on_resize :: proc(dims: ℝ²) {
+	state := g_renderer_state()
 	OpenGL.Viewport(0, 0, i32(dims.x), i32(dims.y))
 	
   // odinfmt: disable
@@ -234,6 +257,11 @@ sdl_on_resize :: proc(dims: ℝ²) {
   }
   // odinfmt: enable
 
-	g_renderer_state().globals.viewport_matrix = m
+	state.globals.viewport_matrix = m
+
+	for &fb in state.framebuffers {
+		destroy_framebuffer(fb)
+		fb = create_framebuffer(dims)
+	}
 }
 // }}}
diff --git a/odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl b/odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl
new file mode 100644
index 0000000..e22e413
--- /dev/null
+++ b/odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl
@@ -0,0 +1,16 @@
+#header
+
+out vec4 FragColor;
+in vec2 v_uv;
+
+uniform sampler2D input_texture;
+layout(std140, binding = 1) uniform Jfa {
+  float u_offset;
+  vec2 u_resolution;
+};
+
+void main() {
+  vec4 sample_value = texture(input_texture, v_uv);
+
+  FragColor = vec4(v_uv * sample_value.a, 0, 1);
+}
diff --git a/odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl b/odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl
new file mode 100644
index 0000000..e7bd63a
--- /dev/null
+++ b/odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl
@@ -0,0 +1,39 @@
+#header
+
+out vec4 FragColor;
+in vec2 v_uv;
+
+uniform sampler2D input_texture;
+layout(std140, binding = 1) uniform Jfa {
+  float u_offset;
+  vec2 u_resolution;
+};
+
+void main() {
+  vec4 nearest_seed = vec4(-2.0);
+  float nearest_dist = 999999999.9;
+
+  for (float y = -1.0; y <= 1.0; y += 1.0) {
+    for (float x = -1.0; x <= 1.0; x += 1.0) {
+      vec2 sampleUV = v_uv + u_offset * vec2(x, y) / u_resolution;
+
+      if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) {
+        continue;
+      }
+
+      vec4 sample_value = texture(input_texture, sampleUV);
+
+      if (sample_value.xy != vec2(0.0)) {
+        vec2 diff = (sample_value.xy - v_uv) * u_resolution;
+        float dist = dot(diff, diff);
+
+        if (dist < nearest_dist) {
+          nearest_dist = dist;
+          nearest_seed = sample_value;
+        }
+      }
+    }
+  }
+
+  FragColor = nearest_seed;
+}
diff --git a/odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl b/odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl
new file mode 100644
index 0000000..c59a1e4
--- /dev/null
+++ b/odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl
@@ -0,0 +1,14 @@
+#header
+
+layout(location = 0) in vec2 a_pos;
+out vec2 v_uv;
+
+layout(std140, binding = 1) uniform Jfa {
+  float u_offset;
+  vec2 u_resolution;
+};
+
+void main() {
+  v_uv = a_pos;
+  gl_Position = vec4(a_pos * 2 - 1, 0, 1);
+}
diff --git a/odin/sdl-opengl-rendering/src/shaders/frag.glsl b/odin/sdl-opengl-rendering/src/shaders/sdf.frag.glsl
similarity index 86%
rename from odin/sdl-opengl-rendering/src/shaders/frag.glsl
rename to odin/sdl-opengl-rendering/src/shaders/sdf.frag.glsl
index 174d65b..9e4c41f 100644
--- a/odin/sdl-opengl-rendering/src/shaders/frag.glsl
+++ b/odin/sdl-opengl-rendering/src/shaders/sdf.frag.glsl
@@ -1,9 +1,9 @@
 #header
 
 out vec4 FragColor;
-in  vec2 v_pos;
+in vec2 v_pos;
 
-layout(std140) uniform Globals {
+layout(std140, binding = 0) uniform Globals {
   mat4 u_viewport_matrix;
   float u_aa_width;
 };
@@ -33,8 +33,8 @@ float sdf_rounded_line(vec2[2] line, float thickness, vec2 p) {
 
 void main() {
   // This function gets auto-generated to call the right sdf
-  float dist    = sdf(v_pos);
-  float alpha   = smoothstep(u_aa_width, -u_aa_width, dist);
+  float dist = sdf(v_pos);
+  float alpha = smoothstep(u_aa_width, -u_aa_width, dist);
   float s_alpha = smoothstep(u_aa_width, -u_aa_width, abs(dist) - v_stroke_width);
 
   if (alpha < 0.001 && s_alpha < 0.001) discard;
diff --git a/odin/sdl-opengl-rendering/src/shaders/vert.glsl b/odin/sdl-opengl-rendering/src/shaders/sdf.vert.glsl
similarity index 57%
rename from odin/sdl-opengl-rendering/src/shaders/vert.glsl
rename to odin/sdl-opengl-rendering/src/shaders/sdf.vert.glsl
index 79bf4fa..2e21d3e 100644
--- a/odin/sdl-opengl-rendering/src/shaders/vert.glsl
+++ b/odin/sdl-opengl-rendering/src/shaders/sdf.vert.glsl
@@ -1,6 +1,6 @@
 #header
 
-layout (location = 0) in vec2 a_pos;
+layout(location = 0) in vec2 a_pos;
 out vec2 v_pos;
 
 layout(std140, binding = 0) uniform Globals {
@@ -11,10 +11,9 @@ layout(std140, binding = 0) uniform Globals {
 #toplevelExtra
 
 void main() {
-  vec4 pos    = u_viewport_matrix * vec4(i_model_matrix * vec3(a_pos, 1), 1);
+  v_pos = i_model_matrix * vec3(a_pos, 1);
+  vec4 pos = u_viewport_matrix * vec4(v_pos, i_z_offset, 1);
   gl_Position = vec4(pos.xyz, 1);
 
-  v_pos = (i_model_matrix * vec3(a_pos, 1)).xy;
-
   #mainExtra
 }
diff --git a/odin/sdl-opengl-rendering/src/shape.odin b/odin/sdl-opengl-rendering/src/shape.odin
index f52149b..0676846 100644
--- a/odin/sdl-opengl-rendering/src/shape.odin
+++ b/odin/sdl-opengl-rendering/src/shape.odin
@@ -2,6 +2,7 @@ package visuals
 
 import "core:fmt"
 import "core:log"
+import "core:math"
 import "core:math/linalg"
 import "core:slice"
 import "core:strings"
@@ -12,7 +13,7 @@ Shape_Options :: struct {
 	z:            ℝ,
 	fill:         Color,
 	stroke:       Color,
-	stroke_width: f32,
+	stroke_width: ℝ,
 }
 
 Shape :: struct(T: typeid) {
@@ -29,14 +30,14 @@ Line :: struct {
 Rounded_Line :: distinct Line
 // }}}
 // {{{ Shape -> Transform
-to_transform_rect :: proc(rect: Shape(□)) -> (mat: Mat3) {
+to_transform_rect :: proc(rect: Shape(□)) -> (mat: Affine2) {
 	mat[0, 0] = rect.dimensions.x + rect.stroke_width * 2
 	mat[1, 1] = rect.dimensions.y + rect.stroke_width * 2
 	mat[2].xy = rect.top_left - rect.stroke_width
 	return mat
 }
 
-to_transform_circle :: proc(circle: Shape(Circle2)) -> (mat: Mat3) {
+to_transform_circle :: proc(circle: Shape(Circle2)) -> (mat: Affine2) {
 	r := circle.radius + circle.stroke_width
 	mat[0, 0] = r * 2
 	mat[1, 1] = r * 2
@@ -44,7 +45,7 @@ to_transform_circle :: proc(circle: Shape(Circle2)) -> (mat: Mat3) {
 	return mat
 }
 
-to_transform_line :: proc(line: Line) -> (mat: Mat3) {
+to_transform_line :: proc(line: Line) -> (mat: Affine2) {
 	dir := line.to - line.from
 	mat[0].xy = dir / 2
 	mat[1].xy = vec2_perp(linalg.normalize0(dir)) * line.thickness
@@ -52,7 +53,7 @@ to_transform_line :: proc(line: Line) -> (mat: Mat3) {
 	return mat
 }
 
-to_transform_rounded_line :: proc(line: Shape(Rounded_Line)) -> (mat: Mat3) {
+to_transform_rounded_line :: proc(line: Shape(Rounded_Line)) -> (mat: Affine2) {
 	dir := line.to - line.from
 	len := linalg.length(dir) // TODO: return if this is close to 0
 
@@ -119,22 +120,39 @@ draw_line :: proc {
 }
 // }}}
 
-// {{{ GPU data types
+// {{{ Uniforms
 UBO :: u32
-UBO_GLOBALS_BINDING :: 0
-VERTEX_POS_LOCATION :: 0
 
-INSTANCES :: 1024 // The number of instances to allocate space for
+UBO_ID :: enum {
+	Globals,
+	JFA,
+}
+
+@(rodata)
+UBO_ID_BINDING: [UBO_ID]u32 = {
+	.Globals = 0,
+	.JFA     = 1,
+}
 
 Global_Uniforms :: struct {
 	viewport_matrix: Mat4,
 	aaWidth:         f32,
 }
 
+JFA_Uniforms :: struct {
+	offset:     f32,
+	_:          f32, // padding
+	resolution: ℝ²,
+}
+// }}}
+// {{{ VBOS / Programs
+VERTEX_POS_LOCATION :: 0
+INSTANCES :: 1024 // The number of instances to allocate space for
 Instance_Param_Buf :: enum {
 	Fill,
 	Stroke,
 	Stroke_Width,
+	Z_Offset,
 	Model_Mat,
 	Dimensions,
 	Center,
@@ -148,12 +166,13 @@ INSTANCE_PARAM_LOCATIONS: [Instance_Param_Buf]u32 = {
 	.Fill         = 1,
 	.Stroke       = 2,
 	.Stroke_Width = 3,
-	.Model_Mat    = 4,
-	.Dimensions   = 8,
-	.Center       = 7,
-	.Radius       = 8,
-	.Line         = 7,
-	.Thickness    = 9,
+	.Z_Offset     = 4,
+	.Model_Mat    = 5,
+	.Center       = 8,
+	.Dimensions   = 9,
+	.Radius       = 9,
+	.Line         = 8,
+	.Thickness    = 10,
 }
 
 @(rodata)
@@ -161,7 +180,8 @@ INSTANCE_PARAM_TYPE: [Instance_Param_Buf]string = {
 	.Fill         = "vec4",
 	.Stroke       = "vec4",
 	.Stroke_Width = "float",
-	.Model_Mat    = "mat3",
+	.Z_Offset     = "float",
+	.Model_Mat    = "mat3x2",
 	.Dimensions   = "vec2",
 	.Center       = "vec2",
 	.Radius       = "float",
@@ -174,7 +194,8 @@ INSTANCE_PARAM_DIMS: [Instance_Param_Buf][2]i32 = { 	// (rows, cols)
 	.Fill         = {4, 1},
 	.Stroke       = {4, 1},
 	.Stroke_Width = {1, 1},
-	.Model_Mat    = {3, 3},
+	.Z_Offset     = {1, 1},
+	.Model_Mat    = {2, 3},
 	.Dimensions   = {2, 1},
 	.Center       = {2, 1},
 	.Radius       = {1, 1},
@@ -184,29 +205,18 @@ INSTANCE_PARAM_DIMS: [Instance_Param_Buf][2]i32 = { 	// (rows, cols)
 
 // Contains geometry data a shader can run on
 Mesh :: struct {
-	// Geometry data
 	vertex_ind_buffer: u32,
 	vertex_pos_buffer: u32,
 	index_count:       ℕ,
 }
 
-// Contains data required to run a gpu program on some mesh
+// Contains data required to run a GPU program on some mesh
 Program :: struct {
 	program: u32,
 	vao:     u32,
 }
 // }}}
-// {{{ Create UBO
-create_ubo_globals :: proc() -> UBO {
-	id := gen_buffer()
-
-	set_buffer(id, &Global_Uniforms{}, buffer = .Uniform)
-	OpenGL.BindBufferBase(OpenGL.UNIFORM_BUFFER, UBO_GLOBALS_BINDING, id)
-
-	return id
-}
-// }}}
-// {{{ Create mesh
+// {{{ Meshes
 create_mesh :: proc(vertices: []ℝ², indices: []u32) -> (out: Mesh) {
 	out.index_count = len(indices)
 
@@ -219,7 +229,93 @@ create_mesh :: proc(vertices: []ℝ², indices: []u32) -> (out: Mesh) {
 	return out
 }
 // }}}
-// {{{ Shader processing
+// {{{ Frame-buffers
+FBO :: struct {
+	fbo:               u32,
+	tex_color:         u32,
+	tex_depth_stencil: u32,
+}
+
+Framebuffer_ID :: enum {
+	JFA_1,
+	JFA_2,
+}
+
+// TODO: perhaps create/destroy these in bulk
+create_framebuffer :: proc(dims: ℝ²) -> (out: FBO) {
+	OpenGL.GenFramebuffers(1, &out.fbo)
+	OpenGL.BindFramebuffer(OpenGL.FRAMEBUFFER, out.fbo)
+	defer OpenGL.BindFramebuffer(OpenGL.FRAMEBUFFER, 0)
+
+	OpenGL.GenTextures(1, &out.tex_color)
+	OpenGL.BindTexture(OpenGL.TEXTURE_2D, out.tex_color)
+
+	OpenGL.TexImage2D(
+		OpenGL.TEXTURE_2D,
+		0, // mipmap level of detail
+		OpenGL.RGBA, // internal format
+		i32(dims.x),
+		i32(dims.y),
+		0, // border: must be 0
+		OpenGL.RGBA, // format
+		OpenGL.UNSIGNED_BYTE, // pixel data type
+		nil, // data pointer
+	)
+
+	OpenGL.TexParameteri(OpenGL.TEXTURE_2D, OpenGL.TEXTURE_MIN_FILTER, OpenGL.LINEAR)
+	OpenGL.TexParameteri(OpenGL.TEXTURE_2D, OpenGL.TEXTURE_MAG_FILTER, OpenGL.LINEAR)
+	OpenGL.FramebufferTexture2D(
+		OpenGL.FRAMEBUFFER,
+		OpenGL.COLOR_ATTACHMENT0,
+		OpenGL.TEXTURE_2D,
+		out.tex_color,
+		0, // mipmap level of detail
+	)
+
+	// TODO: we do not sample those, so we should generate renderbuffers instead
+	OpenGL.GenTextures(1, &out.tex_depth_stencil)
+	OpenGL.BindTexture(OpenGL.TEXTURE_2D, out.tex_depth_stencil)
+
+	OpenGL.TexImage2D(
+		OpenGL.TEXTURE_2D,
+		0, // mipmap level of detail
+		OpenGL.DEPTH24_STENCIL8, // internal format
+		i32(dims.x),
+		i32(dims.y),
+		0, // border: must be 0
+		OpenGL.DEPTH_STENCIL, // format
+		OpenGL.UNSIGNED_INT_24_8, // pixel data type
+		nil, // data pointer
+	)
+
+	OpenGL.FramebufferTexture2D(
+		OpenGL.FRAMEBUFFER,
+		OpenGL.DEPTH_STENCIL,
+		OpenGL.TEXTURE_2D,
+		out.tex_depth_stencil,
+		0, // mipmap level of detail
+	)
+
+	OpenGL.BindTexture(OpenGL.TEXTURE_2D, 0)
+
+	log.assert(
+		OpenGL.CheckFramebufferStatus(OpenGL.FRAMEBUFFER) == OpenGL.FRAMEBUFFER_COMPLETE,
+		"Failed to initialize framebuffer",
+	)
+
+	return out
+}
+
+destroy_framebuffer :: proc(fbo: FBO) {
+	textures: []u32 = {fbo.tex_color, fbo.tex_depth_stencil}
+	OpenGL.DeleteTextures(2, raw_data(textures))
+
+	fbo := fbo.fbo
+	OpenGL.DeleteFramebuffers(1, &fbo)
+}
+// }}}
+// {{{ Shaders
+@(private = "file")
 Shader_Gen :: struct {
 	template:       string,
 	header:         string,
@@ -227,6 +323,7 @@ Shader_Gen :: struct {
 	main_extra:     string,
 }
 
+@(private = "file")
 process_shader :: proc(gen: Shader_Gen) -> string {
 	s := gen.template
 	s, _ = strings.replace_all(s, "#header\n", gen.header, context.temp_allocator)
@@ -244,40 +341,74 @@ Instance_Param_Gen :: struct {
 	vert_only: bool,
 }
 
-Shader_Opts :: struct {
-	params:   []Instance_Param_Gen,
-
-	// The name of the toplevel sdf function to be used for rendering
-	sdf_name: string,
-
-	// Additional arguments to pass to the sdf function declared above.
-	// These args get passed *befor#* the position vector.
-	sdf_args: []string,
-
-	// File ID to display in error messages
-	id:       u32,
+Program_Template :: enum {
+	SDF,
+	JFA,
+	JFA_Seed,
 }
 
+Shader_Opts :: struct {
+	params:   []Instance_Param_Gen,
+	template: Program_Template,
+
+	// SDF only parameters:
+	// - The name of the top-level SDF function to be used for rendering
+	sdf_name: string,
+
+	// - Additional arguments to pass to the SDF function declared above. These 
+	//   arguments get passed *before* the position vector
+	sdf_args: []string,
+}
 
 gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) {
+	// File ID to display in GLSL error messages
+	@(static) next_shader_id := 0
+	id := next_shader_id
+	next_shader_id += 1
+
+	@(static)
+	@(rodata)
+	VERT_TEMPLATE: [Program_Template]string = {
+		.SDF      = #load("./shaders/sdf.vert.glsl"),
+		.JFA      = #load("./shaders/jfa.vert.glsl"),
+		.JFA_Seed = #load("./shaders/jfa.vert.glsl"),
+	}
+
+	@(static)
+	@(rodata)
+	FRAG_TEMPLATE: [Program_Template]string = {
+		.SDF      = #load("./shaders/sdf.frag.glsl"),
+		.JFA      = #load("./shaders/jfa.frag.glsl"),
+		.JFA_Seed = #load("./shaders/jfa-seed.frag.glsl"),
+	}
+
 	// Instance parameters passed to every shape
-	COMMON_INSTANCE_PARAMS: []Instance_Param_Gen : {
-		{buf = .Fill, name = "fill"},
-		{buf = .Stroke, name = "stroke"},
-		{buf = .Stroke_Width, name = "stroke_width"},
-		{buf = .Model_Mat, name = "model_matrix", vert_only = true},
+	@(static)
+	@(rodata)
+	COMMON_INSTANCE_PARAMS: [Program_Template][]Instance_Param_Gen = {
+		.SDF      = {
+			{buf = .Fill, name = "fill"},
+			{buf = .Stroke, name = "stroke"},
+			{buf = .Stroke_Width, name = "stroke_width"},
+			{buf = .Z_Offset, name = "z_offset", vert_only = true},
+			{buf = .Model_Mat, name = "model_matrix", vert_only = true},
+		},
+		.JFA      = {},
+		.JFA_Seed = {},
 	}
 
 	opts := opts
-	opts.params = slice.concatenate([][]Instance_Param_Gen{COMMON_INSTANCE_PARAMS, opts.params})
+	opts.params = slice.concatenate(
+		[][]Instance_Param_Gen{COMMON_INSTANCE_PARAMS[opts.template], opts.params},
+	)
 
 	v_shader, f_shader: string
 
 	// {{{  Vertex shader generation 
 	{
 		gen: Shader_Gen = {
-			template = #load("./shaders/vert.glsl"),
-			header   = fmt.tprintfln("#version 430\n#line 1 %v", opts.id),
+			template = VERT_TEMPLATE[opts.template],
+			header   = fmt.tprintfln("#version 430\n#line 1 %v", id),
 		}
 
 		toplevel_extra, main_extra: strings.Builder
@@ -326,8 +457,8 @@ gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) {
 	// {{{ Fragment shader generation
 	{
 		gen: Shader_Gen = {
-			template = #load("./shaders/frag.glsl"),
-			header   = fmt.tprintfln("#version 430\n#line 1 %v", opts.id),
+			template = FRAG_TEMPLATE[opts.template],
+			header   = fmt.tprintfln("#version 430\n#line 1 %v", id),
 		}
 
 		toplevel_extra: strings.Builder
@@ -348,12 +479,16 @@ gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) {
 			)
 		}
 
-		strings.write_rune(&toplevel_extra, '\n')
-		fmt.sbprintln(&toplevel_extra, "float sdf(vec2 p) {")
-		fmt.sbprintf(&toplevel_extra, "  return %v(", opts.sdf_name)
-		for a in opts.sdf_args do fmt.sbprintf(&toplevel_extra, "%v, ", a)
-		fmt.sbprintfln(&toplevel_extra, "p);")
-		fmt.sbprintln(&toplevel_extra, "}")
+		if opts.template == .SDF {
+			log.assert(len(opts.sdf_name) > 0, "Empty SDF function name")
+
+			strings.write_rune(&toplevel_extra, '\n')
+			fmt.sbprintln(&toplevel_extra, "float sdf(vec2 p) {")
+			fmt.sbprintf(&toplevel_extra, "  return %v(", opts.sdf_name)
+			for a in opts.sdf_args do fmt.sbprintf(&toplevel_extra, "%v, ", a)
+			fmt.sbprintfln(&toplevel_extra, "p);")
+			fmt.sbprintln(&toplevel_extra, "}")
+		}
 
 		gen.toplevel_extra = strings.to_string(toplevel_extra)
 
@@ -397,6 +532,7 @@ gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) {
 // }}}
 
 // {{{ Render the entire queue
+@(private = "file")
 render_instanced :: proc(program: Program, mesh: Mesh, shapes: ^[dynamic]Shape($T)) {
 	OpenGL.UseProgram(program.program)
 	OpenGL.BindVertexArray(program.vao)
@@ -410,12 +546,13 @@ render_instanced :: proc(program: Program, mesh: Mesh, shapes: ^[dynamic]Shape($
 
 		for shape, i in slice {
 			state.buf_matrices[i] = to_transform(shape)
-			state.buf_matrices[i][2, 2] = shape.z
+			state.buf_floats[i] = shape.z
 			state.buf_colors[i] = shape.fill
 		}
 
 		set_buffer(state.instance_buffers[.Model_Mat], &state.buf_matrices)
 		set_buffer(state.instance_buffers[.Fill], &state.buf_colors)
+		set_buffer(state.instance_buffers[.Z_Offset], &state.buf_floats)
 
 		for shape, i in slice {
 			state.buf_colors[i] = shape.stroke
@@ -468,7 +605,7 @@ render_queue :: proc() {
 	state := g_renderer_state()
 
 	// Update uniform data
-	set_buffer(state.ubo_globals, &state.globals, buffer = .Uniform)
+	set_buffer(state.ubos[.Globals], &state.globals, buffer = .Uniform)
 
 	// Toggle the wireframe
 	OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, state.wireframe ? OpenGL.LINE : OpenGL.FILL)
@@ -481,3 +618,70 @@ render_queue :: proc() {
 	OpenGL.UseProgram(0)
 }
 // }}}
+// {{{ JFA
+jfa :: proc() {
+	state := g_renderer_state()
+	dims := screen_dimensions()
+
+	jfa1 := &state.framebuffers[.JFA_1]
+	jfa2 := &state.framebuffers[.JFA_2]
+
+	OpenGL.BindFramebuffer(OpenGL.FRAMEBUFFER, state.pass == .SDF ? 0 : jfa1.fbo)
+	clear_screen()
+	render_queue()
+	if state.pass == .SDF do return
+
+	OpenGL.BindFramebuffer(OpenGL.FRAMEBUFFER, state.pass == .JFA_Seed ? 0 : jfa2.fbo)
+	clear_screen()
+
+	OpenGL.UseProgram(state.jfa_seed_program.program)
+	OpenGL.BindVertexArray(state.jfa_seed_program.vao)
+	OpenGL.ActiveTexture(OpenGL.TEXTURE0)
+	OpenGL.BindTexture(OpenGL.TEXTURE_2D, jfa1.tex_color)
+	OpenGL.Uniform1i(OpenGL.GetUniformLocation(state.jfa_seed_program.program, "input_texture"), 0)
+	OpenGL.DrawElements(
+		OpenGL.TRIANGLE_FAN,
+		i32(state.rect_mesh.index_count),
+		OpenGL.UNSIGNED_INT,
+		nil,
+	)
+	if state.pass == .JFA_Seed do return
+
+	OpenGL.UseProgram(state.jfa_program.program)
+	OpenGL.BindVertexArray(state.jfa_program.vao)
+	OpenGL.ActiveTexture(OpenGL.TEXTURE0)
+	OpenGL.Uniform1i(OpenGL.GetUniformLocation(state.jfa_program.program, "input_texture"), 0)
+
+	input_fbo := jfa2
+	output_fbo := jfa1
+
+	uniforms: JFA_Uniforms = {
+		resolution = dims,
+	}
+
+	passes := math.ceil(math.log2(math.max(dims.x, dims.y)))
+
+	for i in 0 ..< passes {
+		OpenGL.BindFramebuffer(
+			OpenGL.FRAMEBUFFER,
+			state.pass == .JFA && i == passes - 1 ? 0 : output_fbo.fbo,
+		)
+		clear_screen()
+
+		uniforms.offset = math.pow(2, passes - i - 1)
+		set_buffer(state.ubos[.JFA], &uniforms, buffer = .Uniform)
+
+		OpenGL.BindTexture(OpenGL.TEXTURE_2D, input_fbo.tex_color)
+		OpenGL.DrawElements(
+			OpenGL.TRIANGLE_FAN,
+			i32(state.rect_mesh.index_count),
+			OpenGL.UNSIGNED_INT,
+			nil,
+		)
+
+		temp := input_fbo
+		input_fbo = output_fbo
+		output_fbo = temp
+	}
+}
+// }}}