Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,420 changes: 5,420 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"react-dom": "18.2.0",
"recharts": "^2.4.1",
"shadow-cljs": "^2.20.1",
"web-tree-sitter": "^0.22.6",
"w3c-keyname": "^2.2.4"
},
"devDependencies": {
Expand Down
Binary file added resources/public/JetBrainsMono-Regular.ttf
Binary file not shown.
2 changes: 1 addition & 1 deletion resources/public/font_atlas.json

Large diffs are not rendered by default.

Binary file modified resources/public/font_atlas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions src/app/client/webgpu/Summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
Of course. I understand completely. Creating a context document to go with your code is a fantastic practice. It saves an immense amount of time and frustration when you revisit the project later.

Here is a detailed summary and context document. You can save this as a `README.md` or a developer note file and co-locate it with your `core.cljs` file.

---

### **Developer's Context: WebGPU MSDF Text Rendering Engine**

**Last Updated:** June 2025
**Author:** [Your Name]
**Project Goal:** To build a highly-efficient rendering system in ClojureScript and WebGPU capable of drawing, panning, and zooming a large number of text elements at a high frame rate.

---

### **1. High-Level Architecture: The "Setup-Once, Render-Many" Model**

This code is built on the fundamental principle of modern real-time graphics: **do expensive work once on the CPU, then do cheap work many times on the GPU.**

Our initial approach was inefficient because we were rebuilding the entire rendering pipeline (compiling shaders, creating buffers, etc.) every single time the mouse moved. This is like demolishing and rebuilding a car factory 60 times a second just to change the car's color.

The correct and final architecture separates the logic into two distinct phases:

* **A. The Setup Phase (`setup-text-renderer`)**: This is the "factory builder". It runs **only once** at the start. It performs all the heavy lifting:
1. Calculates the static "world" positions of all text characters.
2. Creates permanent GPU buffers to store this geometry.
3. Uploads the geometry to the GPU **once**.
4. Compiles the WGSL shaders.
5. Builds a highly-optimized, reusable `GPURenderPipeline`.
6. Bundles all these persistent GPU objects into a `renderer` map.

* **B. The Render Phase (`draw-text`)**: This is the "factory operator". It's extremely lightweight and runs **every frame** (e.g., via `requestAnimationFrame`). Its only jobs are:
1. Get the latest camera state (pan position, zoom level).
2. Send just those few numbers to a small, dynamic uniform buffer on the GPU.
3. Tell the GPU: "Use the factory (`pipeline`) and materials (`buffers`) we already made, apply this new camera view, and draw everything."

This separation is the key to achieving high performance.

### **2. Code Walkthrough & Key Components**

* **`shape-text`**: This is the core of the CPU-side data preparation. It takes a list of strings and their desired top-left coordinates in "world space" (a pixel-based coordinate system where Y increases upwards). For each character, it:
1. Looks up the character in the **MSDF (Multi-channel Signed Distance Field)** font atlas data.
2. Retrieves its metrics: `planeBounds` (the character's geometry relative to its origin), `atlasBounds` (the character's location on the font texture), and `advance` (how far to move the cursor for the next character).
3. Calculates the final vertex `positions` for the quad that will display the character.
4. Calculates the `uvs` (texture coordinates) that map the quad's vertices to the correct region on the MSDF font atlas texture.

* **`prepare-vertex-data`**: A utility function that takes the output of `shape-text` and flattens it into the precise `Float32Array` (for vertex data) and `Uint16Array` (for index data) formats that the GPU requires.

* **`setup-text-renderer`**: As described above, this is the master setup function that orchestrates the one-time creation of all necessary WebGPU objects.

* **`draw-text`**: The per-frame render function. It takes the `renderer` object map from the setup phase and the current `camera-state` to execute a draw call.

* **Vertex Shader (`vertex-shader-with-camera`)**: Its job is to take a single vertex from the static buffer and figure out where it should be on the screen. It does this by:
1. Applying the `camera.zoom` to the vertex position.
2. Applying the `camera.pan` to the zoomed position.
3. Converting the final world-space pixel coordinate into WebGPU's normalized "clip space" (from `-1.0` to `1.0` on both axes).
4. It also flips the Y-axis (`-clip_space.y`) to account for the difference between our world coordinate system (Y-up) and the typical screen coordinate system (Y-down).

* **Fragment Shader (`text-fragment-shader`)**: Its job is to determine the color of a single pixel on a character's quad. It does this by:
1. Receiving the interpolated UV coordinate for the pixel.
2. Sampling the MSDF font texture at that coordinate.
3. The MSDF texture doesn't store color; it stores *distance*. The shader interprets this distance value to calculate a crisp, smooth alpha (opacity) value, allowing the text to be scaled to any size without pixelation.
4. It then combines this calculated opacity with a hardcoded color (e.g., red) to produce the final pixel color.

### **3. The Debugging Journey: A Record of Fixes**

This code is the result of solving three critical bugs. Understanding them is key to understanding why the final code is structured the way it is.

* **Bug #1: The Blank Screen**
* **Symptom:** The screen was being cleared to the background color, but nothing was being drawn.
* **Discovery:** A "hardcoded quad" test (drawing a simple square instead of complex text) worked perfectly. This proved the entire WebGPU pipeline (shaders, buffers, uniforms) was functional and that the error had to be in the data being fed to it.
* **Lesson:** When in doubt, isolate the problem. Test the graphics pipeline with the simplest possible data to confirm its integrity.

* **Bug #2: Invisibly Small Text**
* **Symptom:** Console logs showed that the correct *number* of vertices were being generated, but they were still invisible. The data wasn't `NaN`, just not showing up.
* **Discovery:** The `font-size` scaling logic was incorrect. It was calculating the scale factor as `desired_size / atlas_size` (e.g., `16 / 256`), resulting in a microscopic scale factor of `0.0625`. All the geometry was being rendered at a fraction of a pixel in size.
* **Solution:** The correct approach is to simply use the desired pixel size (`fsize`) as the scaling factor for the normalized font metrics. The `font-size` variable was corrected to just be `fsize`.

* **Bug #3: Mirrored Text**
* **Symptom:** The text was finally rendering but was flipped horizontally, like looking in a mirror.
* **Discovery:** This is a classic coordinate system mismatch. The geometry of the quads was being generated correctly, but the texture was being mapped onto them backwards along the horizontal U-axis.
* **Solution:** The fix was to swap the left (`al`) and right (`ar`) texture coordinates in the `uvs` array within the `shape-text` function, effectively "un-mirroring" the texture application.

This journey highlights the importance of methodical debugging: isolate the problem, inspect the data at each stage of the pipeline, and verify your mathematical assumptions.
26 changes: 13 additions & 13 deletions src/app/client/webgpu/compute.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
:entryPoint "modifySquare"})})


(def vertices-render-shader
(clj->js {:label "vertices render shader descriptor"
:code "
@vertex
fn renderVertices(@location(0) pos: vec2f) -> @builtin(position) vec4<f32> {
return vec4f(pos, 0.0, 1.0);
}

@fragment
fn renderVerticesFragment() -> @location(0) vec4f {
return vec4f(0.9, 0.9, 0.9, 1);
}
"}))

(defn render-new-vertices [context new-vertices device fformat num-rectangles output-buffer]
(js/console.log "RENDER NEW VERTICES"(js/Float32Array. new-vertices) fformat)
Expand Down Expand Up @@ -51,16 +64,3 @@
(.end render-pass)
(.submit (.-queue device) [(.finish encoder)])))

(def vertices-render-shader
(clj->js {:label "vertices render shader descriptor"
:code "
@vertex
fn renderVertices(@location(0) pos: vec2f) -> @builtin(position) vec4<f32> {
return vec4f(pos, 0.0, 1.0);
}

@fragment
fn renderVerticesFragment() -> @location(0) vec4f {
return vec4f(0.9, 0.9, 0.9, 1);
}
"}))
Loading
Loading