Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5095449
fix rendering, now no heavy computation on each instance, do once ren…
sid597 Jun 27, 2025
48f74ef
fix editor
sid597 Nov 27, 2025
8ddf6d9
format
sid597 Nov 28, 2025
4da2f23
decouple, fix fonts at various sizes, use jetbrains mono font
sid597 Nov 28, 2025
439965a
implement selection rendering, painting layer over layer
sid597 Nov 28, 2025
ef98411
use the reactive way of doing things learned much
sid597 Dec 3, 2025
4b99e8c
bug was due to false :initial-capacity key, scrolling is still slow, …
sid597 Dec 4, 2025
03ddbca
perf(webgpu): refactor to zero-alloc demand-driven render loop for 240Hz
sid597 Dec 4, 2025
ae96103
culling, correct usage of missionary to convert events to flows and r…
sid597 Dec 10, 2025
6081784
fix minor bug
sid597 Dec 11, 2025
f5184d3
fix after nvme ormat
sid597 Dec 17, 2025
84cc2af
add lezer parser
sid597 Dec 23, 2025
4f63fc8
implement selection
sid597 Dec 31, 2025
d24b2c2
add a blinking caret
sid597 Jan 3, 2026
8369a65
Add caret navigation and keyboard controls to WebGPU editor
sid597 Jan 3, 2026
4535026
Add text editing with live syntax highlighting
sid597 Jan 3, 2026
260a592
Implement Lezer features: bracket matching and code folding
sid597 Jan 7, 2026
491b78f
sci integration, core editor features
sid597 Jan 15, 2026
bce6246
Add command panel UI with keyboard input
claude Jan 15, 2026
3a3e970
best practices
sid597 Jan 16, 2026
b28fcc4
Merge pull request #35 from sid597/electric-archi
sid597 Jan 16, 2026
a07ba9a
Add settings panel, font switching, and MSDF rendering improvements
sid597 Jan 19, 2026
08da2b6
Add theme switching with Zed-style per-instance GPU colors
sid597 Jan 23, 2026
177f942
Add file explorer sidebar, cached fold/bracket flows, and idle CPU o…
sid597 Feb 2, 2026
36031f4
Add active file highlighting in sidebar, switch to DejaVu Sans Mono, …
sid597 Feb 2, 2026
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
1 change: 1 addition & 0 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
org.clojure/tools.logging {:mvn/version "1.2.4"}
ch.qos.logback/logback-classic {:mvn/version "1.2.11"}
datascript/datascript {:mvn/version "1.5.2"}
org.babashka/sci {:mvn/version "0.8.43"}
com.rpl/rama {:mvn/version "0.17.0"}
com.roamresearch/backend-sdk {:mvn/version "0.0.4"}
com.rpl/rama-helpers {:mvn/version "0.9.3"}
Expand Down
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.

1 change: 1 addition & 0 deletions resources/public/font_atlas.json.bak

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.
Binary file added resources/public/font_atlas.png.bak
Binary file not shown.
1 change: 1 addition & 0 deletions resources/public/fonts/dejavu_sans_mono_atlas.json

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions resources/public/fonts/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"fonts": [
{
"name": "Ubuntu Sans Mono",
"id": "ubuntu-sans-mono",
"atlas": "ubuntu_sans_mono_atlas.png",
"metrics": "ubuntu_sans_mono_atlas.json",
"charWidth": 0.56,
"defaults": {
"fontSize": 19,
"lineHeight": 1.2,
"pxRange": 8,
"sharpness": 0.0,
"snapToPixel": true,
"showDiagnostics": false
}
},
{
"name": "Ubuntu Mono",
"id": "ubuntu-mono",
"atlas": "ubuntu_mono_atlas.png",
"metrics": "ubuntu_mono_atlas.json",
"charWidth": 0.56,
"defaults": {
"fontSize": 19,
"lineHeight": 1.2,
"pxRange": 8,
"sharpness": 0.0,
"snapToPixel": true,
"showDiagnostics": false
}
},
{
"name": "DejaVu Sans Mono",
"id": "dejavu-sans-mono",
"atlas": "dejavu_sans_mono_atlas.png",
"metrics": "dejavu_sans_mono_atlas.json",
"charWidth": 0.60,
"default": true,
"defaults": {
"fontSize": 19,
"lineHeight": 1.2,
"pxRange": 16,
"sharpness": 0.05,
"snapToPixel": true,
"showDiagnostics": false
}
},
{
"name": "Noto Sans Mono",
"id": "noto-sans-mono",
"atlas": "noto_sans_mono_atlas.png",
"metrics": "noto_sans_mono_atlas.json",
"charWidth": 0.60,
"defaults": {
"fontSize": 19,
"lineHeight": 1.2,
"pxRange": 8,
"sharpness": 0.0,
"snapToPixel": true,
"showDiagnostics": false
}
}
],
"settings": {
"fontSize": {"min": 8, "max": 40, "default": 16},
"lineHeight": {"min": 1.0, "max": 2.0, "default": 1.2},
"pxRange": {"min": 4, "max": 12, "default": 8},
"sharpness": {"min": -0.2, "max": 0.2, "default": 0.0},
"snapToPixel": {"default": true},
"showDiagnostics": {"default": false}
}
}
1 change: 1 addition & 0 deletions resources/public/fonts/noto_sans_mono_atlas.json

Large diffs are not rendered by default.

Binary file added resources/public/fonts/noto_sans_mono_atlas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/public/fonts/ubuntu_mono_atlas.json

Large diffs are not rendered by default.

Binary file added resources/public/fonts/ubuntu_mono_atlas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/public/fonts/ubuntu_sans_mono_atlas.json

Large diffs are not rendered by default.

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