<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrey Kolkov</title>
    <description>The latest articles on DEV Community by Andrey Kolkov (@kolkov).</description>
    <link>https://dev.to/kolkov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F277150%2Fdc37d68a-1fc4-4584-a7a6-0e640febd7a8.jpeg</url>
      <title>DEV Community: Andrey Kolkov</title>
      <link>https://dev.to/kolkov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kolkov"/>
    <language>en</language>
    <item>
      <title>gogpu/ui v0.1.54: We Built Flutter's Gesture System in Pure Go — Arena, Recognizers, and All</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:33:34 +0000</pubDate>
      <link>https://dev.to/kolkov/gogpuui-v0154-we-built-flutters-gesture-system-in-pure-go-arena-recognizers-and-all-31bl</link>
      <guid>https://dev.to/kolkov/gogpuui-v0154-we-built-flutters-gesture-system-in-pure-go-arena-recognizers-and-all-31bl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Series: Building Go's GPU Ecosystem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recent articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/kolkov/gogpuui-v0121-enterprise-render-pipeline-layer-tree-damage-tracking-0-gpu-idle-5adm"&gt;Enterprise Render Pipeline — Layer Tree, Damage Tracking, 0% GPU Idle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kolkov/gogpu-extracting-a-gpu-compositor-in-pure-go-patterns-from-flutter-and-chromium-9ig"&gt;Extracting a GPU Compositor — Patterns from Flutter and Chromium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kolkov/we-built-a-pure-go-3d-renderer-then-embedded-it-inside-a-gui-widget-4mo4"&gt;Pure Go 3D Renderer Embedded Inside a GUI Widget&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kolkov/google-says-go-is-ideal-for-ai-assisted-engineering-weve-been-proving-it-for-a-year-2j7p"&gt;Google Says Go Is Ideal for AI-Assisted Engineering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem: Every Widget Reinvented Click Detection
&lt;/h2&gt;

&lt;p&gt;Our GUI toolkit has 27 widgets — buttons, checkboxes, sliders, text fields, dropdowns, dialogs, tab views, tree views, data tables, docking panels, and more — and each one implemented its own click/drag/selection logic from scratch. Button tracked &lt;code&gt;pressed&lt;/code&gt; state. Slider tracked &lt;code&gt;dragging&lt;/code&gt;. TextField had its own &lt;code&gt;MouseDrag&lt;/code&gt; handler. No code was shared. No disambiguation existed.&lt;/p&gt;

&lt;p&gt;When we wanted to add text selection to TextField (drag to select, double-click to select word, triple-click to select all), we realized the ad-hoc approach couldn't scale. What happens when a TextField is inside a ScrollView? Both want drag events. Who wins?&lt;/p&gt;

&lt;p&gt;Flutter solved this problem in 2017 with the &lt;a href="https://docs.flutter.dev/ui/interactivity/gestures#gesture-disambiguation" rel="noopener noreferrer"&gt;GestureArena&lt;/a&gt;. We decided to port the same architecture to Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Gesture Arena
&lt;/h3&gt;

&lt;p&gt;The arena is the core disambiguation mechanism. When a pointer goes down, all widgets under the pointer register their gesture recognizers in the arena. The recognizers compete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First to accept wins.&lt;/strong&gt; If a recognizer is confident it detected its gesture (e.g., drag exceeded the slop threshold), it accepts and all others are rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Last to not reject wins.&lt;/strong&gt; If everyone else rejects (e.g., no movement for drag), the remaining recognizer wins by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sweep after pointer up.&lt;/strong&gt; If no one accepted, the first member wins as a last resort.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Widget implements gesture.GestureAware&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GestureHitTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;gesture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recognizer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;gesture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recognizer&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clickRec&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Four Recognizers + VelocityTracker
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Recognizer&lt;/th&gt;
&lt;th&gt;What it detects&lt;/th&gt;
&lt;th&gt;Used by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ClickRecognizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single/double/triple click with timing&lt;/td&gt;
&lt;td&gt;Button, Checkbox, Radio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DragRecognizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pan, horizontal, vertical drag with velocity&lt;/td&gt;
&lt;td&gt;Slider, SplitView&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LongPressRecognizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Press-and-hold (500ms, frame-based)&lt;/td&gt;
&lt;td&gt;Future: context menu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TapAndDragRecognizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Click counting + drag in one recognizer&lt;/td&gt;
&lt;td&gt;TextField&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VelocityTracker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Least-squares velocity for fling detection&lt;/td&gt;
&lt;td&gt;ScrollView momentum&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Per-Device Thresholds
&lt;/h3&gt;

&lt;p&gt;Mouse and touch have different precision. A 1px movement on a mouse is intentional; on a touchscreen it's noise. We use the same constants Flutter validated against millions of users:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Mouse&lt;/th&gt;
&lt;th&gt;Touch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drag threshold (slop)&lt;/td&gt;
&lt;td&gt;1px&lt;/td&gt;
&lt;td&gt;18px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Double-click timeout&lt;/td&gt;
&lt;td&gt;300ms&lt;/td&gt;
&lt;td&gt;300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Double-click distance&lt;/td&gt;
&lt;td&gt;4px (Chromium)&lt;/td&gt;
&lt;td&gt;100px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anti-bounce minimum&lt;/td&gt;
&lt;td&gt;40ms&lt;/td&gt;
&lt;td&gt;40ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Unified Pointer Pipeline
&lt;/h3&gt;

&lt;p&gt;We replaced the legacy mouse callback path with a single &lt;code&gt;PointerEvent&lt;/code&gt;-based pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Platform → PointerEvent → HandlePointerEvent
    ├── Gesture Arena (recognizer dispatch for GestureAware widgets)
    └── Derived MouseEvent (existing widgets continue working unchanged)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every pointer event flows through this single path. No dual dispatch. Existing widgets receive &lt;code&gt;MouseEvent&lt;/code&gt; as before — derived from the same &lt;code&gt;PointerEvent&lt;/code&gt; that feeds the arena.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container Widget Hit-Testing
&lt;/h3&gt;

&lt;p&gt;The hardest bug we hit: a Collapsible widget (parent) and a Checkbox (child) both had ClickRecognizers. The parent won the arena and silently dropped clicks meant for the child.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;GestureHitTest(pos geometry.Point)&lt;/code&gt; receives widget-local coordinates. Container widgets only return recognizers when the pointer is in their interactive region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Collapsible returns recognizer ONLY for header clicks&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GestureHitTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;gesture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recognizer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;headerRect&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bounds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Width&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headerHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;headerRect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="c"&gt;// Content-area click → child widgets handle it&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;gesture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Recognizer&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clickRec&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TextField: The Full Story
&lt;/h2&gt;

&lt;p&gt;TextField was our proof-of-concept. Before v0.1.54, it could place a cursor on click. That's it. No selection, no word selection, no clipboard.&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Click&lt;/strong&gt; to place cursor (with Shift+Click for range extension)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drag&lt;/strong&gt; to select text character-by-character&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double-click&lt;/strong&gt; to select word (uses &lt;code&gt;wordBoundsAt()&lt;/code&gt; with Unicode word boundaries)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triple-click&lt;/strong&gt; to select all text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double-click + drag&lt;/strong&gt; for word-by-word selection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ctrl+C/V/X&lt;/strong&gt; for OS clipboard (Win32, macOS, Linux)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this through one &lt;code&gt;TapAndDragRecognizer&lt;/code&gt; that provides &lt;code&gt;consecutiveTapCount&lt;/code&gt; on every callback — exactly how Flutter's &lt;code&gt;TextSelectionGestureDetector&lt;/code&gt; works.&lt;/p&gt;

&lt;h2&gt;
  
  
  OS Clipboard
&lt;/h2&gt;

&lt;p&gt;We added &lt;code&gt;widget.ClipboardProvider&lt;/code&gt; — a DI interface (same pattern as our &lt;code&gt;SoundPlayer&lt;/code&gt;) that bridges widget clipboard requests to the platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// In widget package — no platform imports&lt;/span&gt;
&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClipboardWrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"copied text"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClipboardRead&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c"&gt;// Desktop layer registers the platform implementation&lt;/span&gt;
&lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterClipboardProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gogpuApp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Win32/macOS/Linux&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ctrl+C copies to system clipboard. Ctrl+V pastes from it. Works across applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signals Integration
&lt;/h2&gt;

&lt;p&gt;Gesture state is exposed as opt-in reactive signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;dragRecognizer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gesture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDragRecognizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gesture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DragConfig&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OnDragStart&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;handleStart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;gesture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithDraggingSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDragging&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c"&gt;// Widget binds to signal for reactive redraw&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BindToScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDragging&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bool signals suppress no-op notifications. Point signals fire on every change. Frame-based timers (no goroutines) for long press.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~8,300 LOC&lt;/strong&gt; added (gesture/ package + widget migrations + clipboard + tests)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;124 gesture tests&lt;/strong&gt;, 96% coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20 widgets&lt;/strong&gt; implement &lt;code&gt;GestureAware&lt;/code&gt; with &lt;code&gt;GestureHitTest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0 goroutines&lt;/strong&gt; in the entire gesture system — single-threaded by design&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 enterprise references studied&lt;/strong&gt;: Flutter (source code verified), Chromium, GTK4, Qt6, Android&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The gesture system is designed for multi-touch from day one. &lt;code&gt;PointerEvent&lt;/code&gt; carries &lt;code&gt;PointerID&lt;/code&gt; and &lt;code&gt;PointerType&lt;/code&gt; (mouse/touch/pen). When Android support lands (&lt;a href="https://github.com/besmpl" rel="noopener noreferrer"&gt;@besmpl&lt;/a&gt; is working on it), pinch-to-zoom and multi-finger gestures will plug into the same arena.&lt;/p&gt;

&lt;p&gt;Context menu (right-click) is the next visible feature. The gesture infrastructure supports any button — &lt;code&gt;ClickRecognizer&lt;/code&gt; just needs a &lt;code&gt;ButtonRight&lt;/code&gt; config.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;gogpu/ui&lt;/strong&gt; is an enterprise GUI toolkit for Go with GPU-accelerated rendering. Zero CGO, 220K+ LOC, 70 packages, 7,800+ tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help Us Build Go's GUI Future
&lt;/h2&gt;

&lt;p&gt;The gesture system is the kind of infrastructure that needs real-world validation. We tested with our gallery and examples, but the edge cases live in your applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it.&lt;/strong&gt; Clone the repo, run the gallery, drag-select some text, double-click a word. If something feels wrong — &lt;a href="https://github.com/gogpu/ui/issues" rel="noopener noreferrer"&gt;file an issue&lt;/a&gt;. If it feels right — even better. Tell us.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/gogpu/ui.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;ui
&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vulkan go run ./examples/gallery/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Star the repo.&lt;/strong&gt; It sounds small, but GitHub stars are how Go developers discover projects. We're listed in &lt;a href="https://github.com/avelino/awesome-go" rel="noopener noreferrer"&gt;awesome-go&lt;/a&gt; — every star helps us stay there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spread the word.&lt;/strong&gt; A mention in your team's Slack, a retweet, a discussion with a colleague who's frustrated with GUI options in Go. Most Go developers don't know a pure Go GUI toolkit with GPU rendering exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contribute.&lt;/strong&gt; The gesture system is designed for extensibility. Build a custom recognizer, add a new widget, improve accessibility, test on your hardware. Check &lt;a href="https://github.com/gogpu/ui/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;. &lt;a href="https://github.com/besmpl" rel="noopener noreferrer"&gt;@besmpl&lt;/a&gt; contributed 20+ PRs across the ecosystem including Android arm64 support — contributors make this project better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support the project.&lt;/strong&gt; If gogpu saves you from CGO hell or fills a gap in Go's ecosystem, consider &lt;a href="https://opencollective.com/gogpu" rel="noopener noreferrer"&gt;supporting on Open Collective&lt;/a&gt;. Open source is free to use but not free to build.&lt;/p&gt;

&lt;p&gt;The gogpu ecosystem is 1.25M+ lines of pure Go with zero CGO. gogpu/ui alone: 220K+ LOC, 70 packages, 7,800+ tests. Five GPU backends. Three platforms. One goal: make Go the best language for building desktop applications.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://github.com/orgs/gogpu/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; · &lt;a href="https://github.com/gogpu/ui/releases/tag/v0.1.54" rel="noopener noreferrer"&gt;Release v0.1.54&lt;/a&gt; · &lt;a href="https://github.com/avelino/awesome-go" rel="noopener noreferrer"&gt;awesome-go&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>flutter</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Google Says Go Is Ideal for AI-Assisted Engineering. We've Been Proving It for a Year.</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Wed, 12 Aug 2026 08:02:47 +0000</pubDate>
      <link>https://dev.to/kolkov/google-says-go-is-ideal-for-ai-assisted-engineering-weve-been-proving-it-for-a-year-2j7p</link>
      <guid>https://dev.to/kolkov/google-says-go-is-ideal-for-ai-assisted-engineering-weve-been-proving-it-for-a-year-2j7p</guid>
      <description>&lt;p&gt;Yesterday Google published &lt;a href="https://developers.googleblog.com/why-go-is-an-ideal-language-for-ai-assisted-software-engineering/" rel="noopener noreferrer"&gt;"Why Go is an Ideal Language for AI-Assisted Software Engineering"&lt;/a&gt; — arguing that AI shifted the bottleneck from writing code to reviewing it, and Go's integrated platform provides the guardrails AI teammates need.&lt;/p&gt;

&lt;p&gt;We read it and thought: &lt;em&gt;yes, we know.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Since fall 2025 we've been building Pure Go infrastructure that Go was missing — starting with &lt;a href="https://github.com/scigolib/hdf5" rel="noopener noreferrer"&gt;HDF5&lt;/a&gt; and &lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;Born ML&lt;/a&gt; (GPU-accelerated machine learning), then launching &lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;GoGPU&lt;/a&gt; in December 2025. Today it's 1.25 million lines of code. 15 repositories. Shader compiler, WebGPU implementation, 2D/3D graphics, GUI toolkit, audio engine, multi-process compositor. Zero CGO. All built using &lt;a href="https://dev.to/kolkov/from-vibe-coding-to-agentic-engineering-what-karpathy-got-right-and-whats-missing-62e"&gt;Smart Coding&lt;/a&gt; — our methodology where humans own architecture, specification, and review (70%), while AI handles mechanical implementation (30%), with cumulative knowledge transfer between sessions.&lt;/p&gt;

&lt;p&gt;Here's what Google got right, what we learned the hard way, and what the article doesn't mention.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google Got Right
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "The bottleneck shifted from writing to reviewing"
&lt;/h3&gt;

&lt;p&gt;This is the central thesis, and it's correct. When AI generates hundreds of lines per session, the limiting factor becomes: &lt;em&gt;can you verify this is correct?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In our workflow, every PR gets claim-by-claim validation. When a contributor submits "fix: correct PDF y-axis transform," we don't just read the diff — we verify the math:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transform.Then applies the receiver first.
Translate(0,h).Then(Scale(1,-1)) → y' = -(y+h) ← WRONG
Scale(1,-1).Then(Translate(0,h)) → y' = h-y     ← CORRECT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We write the proof in a temp file, run it, confirm the output, &lt;em&gt;then&lt;/em&gt; approve. AI generates the fix. Humans verify the math. Google's thesis in action.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Go's integrated platform enables AI self-correction loops"
&lt;/h3&gt;

&lt;p&gt;Google argues that gofmt, go test, go vet, and the compiler form a feedback loop where AI can iteratively fix its own output. True — and we run this loop on every change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gofmt &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;                    &lt;span class="c"&gt;# format&lt;/span&gt;
go build ./...                &lt;span class="c"&gt;# compile (catches type errors, hallucinated APIs)&lt;/span&gt;
go vet ./...                  &lt;span class="c"&gt;# static analysis&lt;/span&gt;
golangci-lint run &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5m &lt;span class="c"&gt;# 30+ linters&lt;/span&gt;
go &lt;span class="nb"&gt;test&lt;/span&gt; ./...                 &lt;span class="c"&gt;# behavioral verification&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across 1.25M LOC, this catches most AI mistakes before a human ever looks at the code. The compiler is the first reviewer.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Static types = safety net for agentic code"
&lt;/h3&gt;

&lt;p&gt;Google: &lt;em&gt;"If an AI agent attempts to use a non-existent method, pass an incorrect type, or leave a variable uninitialized, the code simply will not compile."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We cross-compile for three platforms on every PR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;windows go build ./...
&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux   go build ./...
&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;darwin  go build ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches platform-specific hallucinations that single-platform builds miss — an AI might generate a Windows syscall in a Linux file, or reference a macOS framework in cross-platform code.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Single static binary, zero system dependencies"
&lt;/h3&gt;

&lt;p&gt;Our ecosystem proves this at scale. &lt;code&gt;go build&lt;/code&gt; produces a binary that includes a WebGPU implementation (Vulkan/DX12/Metal/GLES/Software backends), a shader compiler (WGSL → SPIR-V/MSL/GLSL/HLSL/DXIL), a 2D graphics engine, and a GUI toolkit. No DLLs, no shared libraries, no runtime dependencies. No C compiler required.&lt;/p&gt;

&lt;p&gt;This is what "batteries-included" means when taken seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Google Didn't Do — and We Did
&lt;/h2&gt;

&lt;p&gt;Google created Go in 2009. They wrote the article about Go being ideal for AI in 2026. In between — 17 years — Go lacked three things every serious platform needs: professional graphics, GPU access, and an ML ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No GPU graphics.&lt;/strong&gt; No GPU-accelerated 2D library. No shader compiler. No WebGPU implementation. No GUI toolkit with native rendering. Java had Swing, then JavaFX. .NET had WPF, then MAUI. Rust got wgpu, then Bevy. Go got Fyne (CGO + OpenGL) and Ebiten (game engine, not a platform).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No GPU compute.&lt;/strong&gt; Python has PyTorch, CUDA, TensorFlow. Rust has wgpu-rs for compute shaders and &lt;a href="https://github.com/tracel-ai/burn" rel="noopener noreferrer"&gt;Burn&lt;/a&gt; for ML. Go had... &lt;code&gt;os/exec&lt;/code&gt; to shell out to Python. A language "ideal for AI-assisted engineering" that couldn't run a GPU kernel without calling C. The irony: Google co-created the &lt;a href="https://www.w3.org/TR/webgpu/" rel="noopener noreferrer"&gt;WebGPU standard&lt;/a&gt; (W3C) and built &lt;a href="https://dawn.googlesource.com/dawn" rel="noopener noreferrer"&gt;Dawn&lt;/a&gt; — the reference WebGPU implementation for Chrome. But they never gave Go access to their own standard. We built the &lt;a href="https://en.wikipedia.org/wiki/WebGPU" rel="noopener noreferrer"&gt;third native WebGPU implementation&lt;/a&gt; in the world — and the only one in Go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No ML ecosystem.&lt;/strong&gt; Google writes the article about Go and AI, then builds TensorFlow in Python/C++, JAX in Python, and Gemini in... not Go. The language they praise as ideal for AI couldn't train a model.&lt;/p&gt;

&lt;p&gt;Google builds Flutter (Dart), Android UI (Kotlin), Chrome (C++), TensorFlow (Python/C++). They never invested in making Go a first-class platform for graphics, GPU compute, or machine learning.&lt;/p&gt;

&lt;p&gt;We're fixing all three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Graphics:&lt;/strong&gt; Shader compiler ahead of Rust naga on some features (DXIL generator — Rust hasn't shipped theirs). WebGPU implementation — the third in the world after Chrome's Dawn and Firefox's wgpu, and the only one in Go. GUI toolkit listed in &lt;a href="https://github.com/avelino/awesome-go" rel="noopener noreferrer"&gt;awesome-go&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU compute:&lt;/strong&gt; &lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;Born ML&lt;/a&gt; — a production ML framework fully migrated to our Pure Go GPU stack. Trains models on GPU. No Python, no CUDA, no CGO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The full stack:&lt;/strong&gt; 1.25M lines of Pure Go. From shader compilation to neural network training to GUI rendering — one language, one toolchain, &lt;code&gt;go build&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google's article validates &lt;em&gt;why&lt;/em&gt; Go is the right language. GoGPU and Born ML prove &lt;em&gt;what's possible&lt;/em&gt; when someone actually builds the missing pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned That Google Didn't Mention
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI-generated code needs claim-by-claim validation, not just compilation
&lt;/h3&gt;

&lt;p&gt;Google focuses on the compiler as the safety net. The compiler catches &lt;em&gt;syntax&lt;/em&gt; and &lt;em&gt;type&lt;/em&gt; errors. It does not catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong algorithm&lt;/strong&gt; — code compiles but produces incorrect output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong architecture&lt;/strong&gt; — code works but cements the wrong design&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong numbers&lt;/strong&gt; — "renderer.go: ~720 → ~400 LOC" when the actual delta is 2,174 → 1,892&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our dev.to articles, we validate every factual claim against code before publishing. "Flutter's flow/ has ~15K LOC" — we count: actual is ~22K total lines. "ssa.Func has 30+ fields" — we count: actual is 41. "UI has 24 widgets" — we check README: actual is 27.&lt;/p&gt;

&lt;p&gt;The compiler can't do this. Humans must.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise references prevent AI from reinventing wheels badly
&lt;/h3&gt;

&lt;p&gt;Google mentions "cleaner training data" from Go's standardized ecosystem. But they don't address a deeper problem: AI confidently generates &lt;em&gt;plausible-looking architecture&lt;/em&gt; that violates well-established patterns.&lt;/p&gt;

&lt;p&gt;When we needed a GPU compositor, AI would happily generate a god-struct with 200 fields and bidirectional dependencies. Instead, we researched how Flutter (&lt;code&gt;flow/&lt;/code&gt;), Chromium (&lt;code&gt;cc/&lt;/code&gt;), and GTK4 (&lt;code&gt;GSK&lt;/code&gt;) handle compositors. The answer was consistent: unidirectional deps, concrete structs, minimal callback interfaces. Research before code.&lt;/p&gt;

&lt;p&gt;We formalized this as a rule: &lt;strong&gt;every GPU/HAL/sync/barrier change starts with enterprise reference research.&lt;/strong&gt; AI proposes, references validate.&lt;/p&gt;

&lt;p&gt;But references aren't always available. When you're building the third WebGPU implementation in the world, or the first Pure Go DXIL generator, there's no "how Flutter does it" to consult. You're the pioneer. AI can't help here either — it will confidently generate plausible architecture that has never been tested at scale. This is where human judgment, deep domain knowledge, and the willingness to read GPU specs directly become irreplaceable. Google's article assumes references exist. In practice, sometimes you &lt;em&gt;are&lt;/em&gt; the reference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-agent architecture, not one AI editing everything
&lt;/h3&gt;

&lt;p&gt;Google's article implies a single AI agent working on a single codebase. At our scale (15 repos), the architecture is fundamentally different.&lt;/p&gt;

&lt;p&gt;Each repository has its &lt;strong&gt;own specialized agent&lt;/strong&gt; with domain-specific sub-agents — a shader compiler agent that knows SPIR-V opcodes, a HAL backend agent that understands Vulkan barrier semantics, a GUI toolkit agent that knows widget lifecycle patterns. These agents are tuned for the specific library they serve, with their own project-level instructions, enterprise references, and research protocols.&lt;/p&gt;

&lt;p&gt;The ecosystem-level agent &lt;strong&gt;only coordinates&lt;/strong&gt; — it tracks cascade releases, manages cross-repo issues, and communicates via GitHub. It does NOT implement features in other repos. When it makes a claim ("this API should change in wgpu"), the repo-level agent &lt;strong&gt;independently validates&lt;/strong&gt; that claim through its own deep research. They agree, or they push back with evidence. Very often, the first architectural instinct turns out to be fundamentally wrong — the repo agent catches it because it knows the domain deeply.&lt;/p&gt;

&lt;p&gt;Cross-repo code changes by the coordination agent are &lt;strong&gt;rare exceptions&lt;/strong&gt;, limited to thin interface seams (a new method on &lt;code&gt;gpucontext.DeviceProvider&lt;/code&gt;, a version bump in &lt;code&gt;go.mod&lt;/code&gt;). The real work happens inside each repo's agent session.&lt;/p&gt;

&lt;p&gt;This isn't a preference — it's a &lt;strong&gt;technical necessity&lt;/strong&gt;. No single AI agent can load 1.25M lines of code into its context window. Each repo is 96K-324K lines — already pushing context limits for a single session. The only way to work at ecosystem scale is specialized agents that deeply understand their domain, coordinated by a lightweight orchestrator that trusts but verifies.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go.work&lt;/code&gt; plays a different but critical role: when all repos are in a shared workspace, &lt;strong&gt;any agent in any repo&lt;/strong&gt; always builds and tests against the current local state of every ecosystem dependency — not the last published version. An agent working on &lt;code&gt;gg&lt;/code&gt; sees unpublished &lt;code&gt;wgpu&lt;/code&gt; changes. An agent fixing a &lt;code&gt;naga&lt;/code&gt; shader bug can immediately verify the fix compiles against the latest &lt;code&gt;wgpu&lt;/code&gt; consumer code. No publish-wait-update cycles. When it's time for a PR or release, the code goes through &lt;strong&gt;multi-stage validation&lt;/strong&gt;: each repo's agent runs its own pre-flight checks (build, test, lint across platforms, feature completeness audit, dependency freshness, documentation scan). The ecosystem agent validates cross-repo consistency. Agents check each other's claims — stubs, incomplete implementations, architectural shortcuts are caught before they reach production. And at the end, &lt;strong&gt;the human reviews and approves&lt;/strong&gt; before anything is published. No release ships without explicit human sign-off.&lt;/p&gt;

&lt;p&gt;The entire process is backed by &lt;strong&gt;persistent knowledge artifacts&lt;/strong&gt;. Every deep investigation produces a research document with enterprise references and source-verified findings. Before implementation, an Architecture Decision Record (ADR) is written — documenting the decision, alternatives rejected, and enterprise patterns adopted. Kanban tasks break the work into trackable units with clear acceptance criteria.&lt;/p&gt;

&lt;p&gt;When it's time to implement, specialized agents receive the full context — the research doc, the ADR, the task description, the relevant enterprise references. They write the code. Then the main repo agent &lt;strong&gt;reviews everything they produced&lt;/strong&gt; — validates claims against code, checks for stubs, verifies test coverage, confirms the implementation matches the ADR. Only after confirming the code meets acceptance criteria does the agent approve the commit and public documentation update. If it doesn't meet the bar, the implementation iterates until it does. This is not a one-shot process — it's a quality ratchet that only moves forward.&lt;/p&gt;

&lt;p&gt;But it's the multi-agent architecture, not go.work, that makes 15-repo AI-assisted development possible at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI lint versions drift — and AI can't predict it
&lt;/h3&gt;

&lt;p&gt;A practical problem Google's article doesn't address: golangci-lint &lt;code&gt;latest&lt;/code&gt; in CI means the rules change under you. We've had PRs that pass locally (v2.12.2 on Windows, Go 1.25.5) but fail in CI (same v2.12.2, but Go 1.25.12 on Linux) because staticcheck behavior differs between Go toolchain versions.&lt;/p&gt;

&lt;p&gt;AI generates code that passes &lt;em&gt;your&lt;/em&gt; lint. CI runs &lt;em&gt;different&lt;/em&gt; lint. The fix is pinning versions — but &lt;code&gt;latest&lt;/code&gt; is the default in most GitHub Actions, including golangci-lint's own action.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Scale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total Pure Go code&lt;/td&gt;
&lt;td&gt;1.25M LOC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repositories&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub stars&lt;/td&gt;
&lt;td&gt;1,200+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development period&lt;/td&gt;
&lt;td&gt;~1 year (HDF5 + Born ML fall 2025, GoGPU from Dec 2025)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CGO required&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External C/Rust dependencies&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU backends&lt;/td&gt;
&lt;td&gt;5 (Vulkan, DX12, Metal, GLES, Software)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shader targets&lt;/td&gt;
&lt;td&gt;5 (SPIR-V, MSL, GLSL, HLSL, DXIL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platforms&lt;/td&gt;
&lt;td&gt;Windows, macOS, Linux, Browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contributors&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All built using &lt;a href="https://dev.to/kolkov/from-vibe-coding-to-agentic-engineering-what-karpathy-got-right-and-whats-missing-62e"&gt;Smart Coding&lt;/a&gt; — human architecture + AI implementation + cumulative knowledge — with exactly the Go guardrails Google describes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ecosystem
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;LOC&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;naga&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;324K&lt;/td&gt;
&lt;td&gt;Shader compiler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;gg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;312K&lt;/td&gt;
&lt;td&gt;2D graphics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;wgpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;254K&lt;/td&gt;
&lt;td&gt;WebGPU implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;ui&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;211K&lt;/td&gt;
&lt;td&gt;GUI toolkit (27 widgets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;gogpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;96K&lt;/td&gt;
&lt;td&gt;App framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ 10 more repos&lt;/td&gt;
&lt;td&gt;~53K&lt;/td&gt;
&lt;td&gt;3D, audio, composition, system tray, types&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Google's article is the theory. GoGPU is the proof. If you're considering Go for an AI-assisted project — we can confirm: the guardrails work. The compiler catches the easy bugs. The toolchain catches the medium bugs. Humans catch the hard bugs. And Go makes all three layers practical at scale.&lt;/p&gt;

&lt;p&gt;But guardrails alone aren't enough. You also need a methodology — &lt;a href="https://dev.to/kolkov/from-vibe-coding-to-agentic-engineering-what-karpathy-got-right-and-whats-missing-62e"&gt;Smart Coding&lt;/a&gt; — where humans own the architecture, AI handles implementation, and cumulative knowledge files ensure each session builds on the last. Go provides the platform. Smart Coding provides the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Star us on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;github.com/gogpu/gogpu&lt;/a&gt; ⭐&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support continued development:&lt;/strong&gt; &lt;a href="https://opencollective.com/gogpu" rel="noopener noreferrer"&gt;opencollective.com/gogpu&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>ai</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>GoGPU: Extracting a GPU Compositor in Pure Go — Patterns from Flutter and Chromium</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Tue, 11 Aug 2026 15:12:05 +0000</pubDate>
      <link>https://dev.to/kolkov/gogpu-extracting-a-gpu-compositor-in-pure-go-patterns-from-flutter-and-chromium-9ig</link>
      <guid>https://dev.to/kolkov/gogpu-extracting-a-gpu-compositor-in-pure-go-patterns-from-flutter-and-chromium-9ig</guid>
      <description>&lt;p&gt;We had a suspicion the compositor was in the wrong package. But it worked, there was only one consumer, and refactoring a GPU compositor is genuinely hard. So we shipped it and moved on.&lt;/p&gt;

&lt;p&gt;Then the 3D renderer arrived. Then the overlays broke. Suspicion turned into certainty — at the worst possible time.&lt;/p&gt;

&lt;p&gt;This is the story of that migration, and what we learned from Flutter, Go stdlib, and Chromium along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup: a Compositor Hidden Inside a Drawing Library
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;GoGPU&lt;/a&gt; is a Pure Go GPU ecosystem — 1.25M lines of code, zero CGO. The architecture has three layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gogpu    — app framework (windowing, events, lifecycle)
gg       — 2D graphics (paths, text, GPU SDF, rasterizer)
wgpu     — WebGPU implementation (Vulkan/DX12/Metal/GLES/Software)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we first built GPU-accelerated rendering for &lt;code&gt;gg&lt;/code&gt;, the compositor logic naturally ended up inside &lt;code&gt;gg&lt;/code&gt;. It was the only consumer. It owned the render target, the blit pipeline, the MSAA resolve, the damage tracking. It decided when to clear vs preserve content.&lt;/p&gt;

&lt;p&gt;This is an &lt;strong&gt;ownership inversion&lt;/strong&gt;. In every enterprise graphics stack — Chromium, Flutter, GTK4 — the &lt;em&gt;compositor&lt;/em&gt; owns the render target and content cache. The &lt;em&gt;drawing library&lt;/em&gt; just records draw commands into whatever surface it's given:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Drawing library&lt;/th&gt;
&lt;th&gt;Compositor&lt;/th&gt;
&lt;th&gt;Who owns the surface?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chromium&lt;/td&gt;
&lt;td&gt;Skia&lt;/td&gt;
&lt;td&gt;cc/ (tile manager)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compositor&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GTK4&lt;/td&gt;
&lt;td&gt;Render nodes&lt;/td&gt;
&lt;td&gt;GSK&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compositor&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flutter&lt;/td&gt;
&lt;td&gt;Dart Canvas&lt;/td&gt;
&lt;td&gt;flow/&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compositor&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GoGPU (before)&lt;/td&gt;
&lt;td&gt;gg&lt;/td&gt;
&lt;td&gt;gogpu&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;gg&lt;/strong&gt; ← wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We suspected this wasn't right. But &lt;code&gt;gg&lt;/code&gt; was the only renderer, it worked, and the refactoring would be complex. So we shipped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breaking Point: Three Problems, One Root Cause
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem 1: Damage rects were single-source
&lt;/h3&gt;

&lt;p&gt;Damage tracking already lived in gogpu — but as a simple passthrough. gg called &lt;code&gt;SetDamageRects()&lt;/code&gt; with its dirty regions, gogpu forwarded them to the platform compositor via &lt;code&gt;VkPresentRegionsKHR&lt;/code&gt;. One source, one consumer, worked fine.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;g3d&lt;/code&gt; arrived. A 3D scene rotating at 60fps sends full-viewport damage. But &lt;code&gt;SetDamageRects&lt;/code&gt; only accepted one set of rects — from gg. When gg rendered a HUD overlay on top of a g3d scene, only the HUD's damage was reported. The g3d viewport outside the HUD? Stale frames on Wayland compositors. Confirmed by @porjo on Asahi Linux (g3d#22) — visible mosaic artifacts because Mesa's &lt;code&gt;wl_surface_damage_buffer&lt;/code&gt; only updated the HUD area.&lt;/p&gt;

&lt;p&gt;So we redesigned the damage system from scratch. &lt;code&gt;SetDamageRects&lt;/code&gt; (single source, simple passthrough) was replaced with &lt;code&gt;RegisterDamageSource&lt;/code&gt; — each renderer registers as a named source, reports its own damage per frame, and gogpu unions all sources at present time. Chromium's &lt;code&gt;DamageTracker&lt;/code&gt; uses the same union pattern. That was ADR-065 — not a refactor, a reimplementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: Two overlays, two libraries
&lt;/h3&gt;

&lt;p&gt;We had &lt;strong&gt;two independent debug overlay implementations&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;gg&lt;/strong&gt; had a damage overlay (&lt;code&gt;damage_debug.go&lt;/code&gt;) — green flash-and-fade on dirty regions (&lt;code&gt;GOGPU_DEBUG_DAMAGE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ui&lt;/strong&gt; had a dirty widget overlay (&lt;code&gt;debug_dirty.go&lt;/code&gt;) — cyan rectangles on repainted widgets (&lt;code&gt;GOGPU_DEBUG_DIRTY&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both worked fine in isolation. Each had its own env var, its own rendering logic, its own fade effects. And neither knew about the other. A g3d-only app had &lt;strong&gt;zero debug overlays&lt;/strong&gt; — no damage visualization, no FPS counter, nothing.&lt;/p&gt;

&lt;p&gt;The fix: move overlays to the compositor level. A g3d-only app without gg still needs overlays. A multi-renderer app (gg + g3d) needs a unified overlay that sees damage from all sources. That was ADR-066 — a pluggable &lt;code&gt;DebugOverlay&lt;/code&gt; system at the compositor level, with built-in damage and FPS overlays.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 3: The move that broke everything
&lt;/h3&gt;

&lt;p&gt;ADR-065 and ADR-066 shipped together in v0.51.0. Damage sources worked. Overlays worked — until the content was idle and the overlay had a 400ms fade animation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Overlay fade: NeedsAnimationFrame() → RequestRedraw()
2. Next frame: gg says "nothing dirty" → no GPU commands
3. gg owns the render target → no commands = frame skipped
4. Overlay never draws → animation freezes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compositor (gogpu) couldn't render overlays independently because &lt;strong&gt;gg owned the surface&lt;/strong&gt;. Without gg's cooperation, the compositor couldn't even decide whether to clear or preserve the render target. The Vulkan spec says acquired swapchain images have UNDEFINED content — so without gg drawing, the overlay rendered on garbage.&lt;/p&gt;

&lt;p&gt;Three problems, one root cause: &lt;strong&gt;gg owned what the compositor should own.&lt;/strong&gt; The shortcut that "worked fine" for one consumer became a blocking architectural problem when the second and third consumers appeared. Every enterprise reference confirmed what we'd only suspected — the compositor must own the render target, not the drawing library.&lt;/p&gt;

&lt;p&gt;Delaying further would have meant building workarounds. Each workaround would cement the wrong architecture deeper. So we stopped delaying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: Moving the Compositor Out of gg (ADR-067)
&lt;/h2&gt;

&lt;p&gt;The first step was getting the compositor code out of &lt;code&gt;gg&lt;/code&gt; and into &lt;code&gt;gogpu&lt;/code&gt; where it belongs.&lt;/p&gt;

&lt;p&gt;We introduced a &lt;strong&gt;compositor-owned composition texture&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before:
  swapchain acquired → gg renders directly → present
  (gg decides Clear/Load, gg owns MSAA resolve)

After:
  swapchain acquired → gogpu clears composition texture
  → gg/g3d render INTO composition texture (they don't know)
  → gogpu draws overlays on top
  → gogpu blits composition texture → swapchain → present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key insight: content renderers don't need to know about the compositor. &lt;code&gt;Context.SurfaceView()&lt;/code&gt; returns the composition texture view instead of the swapchain view. From &lt;code&gt;gg&lt;/code&gt;'s perspective, nothing changed — it renders to a &lt;code&gt;TextureView&lt;/code&gt;, same as before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overlay-only frames became first-class.&lt;/strong&gt; When content is idle but an overlay is animating, the compositor uses &lt;code&gt;LoadOpLoad&lt;/code&gt; (preserve) instead of &lt;code&gt;LoadOpClear&lt;/code&gt;, draws the overlay on the preserved content, and presents. No cooperation from &lt;code&gt;gg&lt;/code&gt; needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: Extracting internal/compositor/ (ADR-069)
&lt;/h2&gt;

&lt;p&gt;After moving compositor logic into &lt;code&gt;gogpu&lt;/code&gt;, we had ~2,500 LOC of compositor code mixed with ~4,000 LOC of application framework in the same package. Time for the second step: extract it into &lt;code&gt;internal/compositor/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Research first
&lt;/h3&gt;

&lt;p&gt;Before writing extraction code, we studied three references:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flutter's &lt;code&gt;flow/&lt;/code&gt; (~22K lines)&lt;/strong&gt; — concrete structs, unidirectional deps (&lt;code&gt;shell/&lt;/code&gt; → &lt;code&gt;flow/&lt;/code&gt;, never reverse), GPU resources borrowed per frame through a callback. Closest to our situation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go stdlib &lt;code&gt;ssa.Func&lt;/code&gt; (41 fields)&lt;/strong&gt; — the largest &lt;code&gt;internal/&lt;/code&gt; split in Go. Struct ownership + &lt;code&gt;Frontend&lt;/code&gt; callback interface. Parent creates the struct, calls methods. Internal package calls back through a minimal interface when it needs the parent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chromium &lt;code&gt;cc/&lt;/code&gt; (~190K lines)&lt;/strong&gt; — interface-heavy dependency injection with &lt;code&gt;*Client&lt;/code&gt;/&lt;code&gt;*Delegate&lt;/code&gt; abstract classes everywhere. A C++ necessity, over-engineered for Go &lt;code&gt;internal/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we actually built
&lt;/h3&gt;

&lt;p&gt;The compositor isn't a single god-struct. It's a set of focused types that the root package composes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gogpu/ (root package, renderer.go)
│
├── RenderTarget {
│     damageSources  []*compositor.DamageSource
│     blitPipeline    compositor.BlitPipeline
│     compositeState  compositor.CompositeState
│   }
│
└── internal/compositor/
    ├── blit.go            — BlitResources, BlitDrawRecorder interface
    ├── blit_pipeline.go   — BlitPipeline, CompositeState
    ├── damage_source.go   — DamageSource, per-renderer damage tracking
    ├── damage_scissor.go  — pure geometry: rect union, scissor clipping
    ├── damage_overlay.go  — DamageDebugOverlay (damage rect visualization)
    ├── fps_overlay.go     — FPSDebugOverlay (frame rate counter)
    ├── overlay_pipeline.go — shared GPU pipeline for overlays
    └── shader.go          — WGSL shader sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Content renderers (gg, g3d) implement one interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BlitDrawRecorder&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;RecordBlitDraws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;wgpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RenderPassEncoder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compositor owns the render pass lifecycle. Renderers just record their draws.&lt;/p&gt;

&lt;h3&gt;
  
  
  The numbers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;renderer.go&lt;/code&gt;: 2,174 → 1,892 lines (-282 LOC of compositor logic removed)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;internal/compositor/&lt;/code&gt;: 2,357 LOC (self-contained subsystem)&lt;/li&gt;
&lt;li&gt;Root package wiring: 194 LOC (thin delegation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Zero public API change.&lt;/strong&gt; All &lt;code&gt;App&lt;/code&gt;, &lt;code&gt;Context&lt;/code&gt;, &lt;code&gt;RenderTarget&lt;/code&gt; types stayed in the root package. &lt;code&gt;RegisterDamageSource()&lt;/code&gt; return type narrowed to &lt;code&gt;gpucontext.DamageReporter&lt;/code&gt; interface — which external consumers already used. No breaking change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules That Emerged
&lt;/h2&gt;

&lt;p&gt;These are now enforced in CI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. No type aliases for migrated types.&lt;/strong&gt; &lt;code&gt;type X = internal.X&lt;/code&gt; is for transitional migration only. We changed return types to interfaces instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Call internal directly.&lt;/strong&gt; No unexported wrapper functions around &lt;code&gt;internal/&lt;/code&gt; calls. At call sites: &lt;code&gt;r.blitPipeline.Init(device, format, shader)&lt;/code&gt;, not an unexported &lt;code&gt;initBlitPipeline()&lt;/code&gt; wrapper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Unidirectional deps, enforced in CI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Compositor dependency direction&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;if grep -r '"github.com/gogpu/gogpu"' internal/compositor/; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "compositor must not import root package"&lt;/span&gt;
      &lt;span class="s"&gt;exit 1&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Performance is not a reason to avoid extraction.&lt;/strong&gt; We benchmarked interface calls: ~1.4-3.0ns vs ~1.3ns for direct calls. At 10 calls/frame × 60fps = ~60ns per frame vs ~16.7ms budget. &lt;strong&gt;0.0005%&lt;/strong&gt; overhead. Go 1.20+ devirtualizes most of these anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"We'll fix it later" has a deadline you don't control.&lt;/strong&gt; We suspected the compositor was in the wrong place for months. The deadline to fix it wasn't set by us — it was set by the overlay freeze bug and the 3D renderer needing damage tracking. Under pressure, every refactoring is harder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise references agree on ownership.&lt;/strong&gt; We checked Chromium, Flutter, and GTK4. In &lt;em&gt;all three&lt;/em&gt;, the compositor owns the surface and content cache. The drawing library just draws. This isn't a coincidence — it's the architecture that survives multi-renderer apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not everything needs a god struct.&lt;/strong&gt; The Go stdlib &lt;code&gt;ssa.Func&lt;/code&gt; pattern (single struct, 40+ fields, callback interface) is powerful but not universal. Our compositor worked better as a set of focused types (3-8 fields each) that the root package composes. Each type has a clear responsibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;internal/&lt;/code&gt; is underrated.&lt;/strong&gt; Most Go projects either put everything in one package or create deep hierarchies. &lt;code&gt;internal/&lt;/code&gt; gives you encapsulation without the API commitment. The compositor is free to change its struct layout without breaking external consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ecosystem
&lt;/h2&gt;

&lt;p&gt;GoGPU is 1.25M lines of Pure Go — a full GPU ecosystem, not just one library:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;LOC&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;naga&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;324K&lt;/td&gt;
&lt;td&gt;Shader compiler (WGSL → SPIR-V/MSL/GLSL/HLSL/DXIL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;gg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;312K&lt;/td&gt;
&lt;td&gt;2D graphics (Skia-class rasterizer, GPU SDF, ClearType)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;wgpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;254K&lt;/td&gt;
&lt;td&gt;WebGPU (Vulkan/DX12/Metal/GLES/Software/Browser)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;ui&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;211K&lt;/td&gt;
&lt;td&gt;GUI toolkit (27 widgets, 4 design systems)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;gogpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;96K&lt;/td&gt;
&lt;td&gt;App framework + compositor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/g3d" rel="noopener noreferrer"&gt;g3d&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;14K&lt;/td&gt;
&lt;td&gt;3D rendering (scene graph, PBR, forward renderer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/compose" rel="noopener noreferrer"&gt;compose&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;10K&lt;/td&gt;
&lt;td&gt;Multi-process composition (Unix socket, LZ4, hot-plug)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/systray" rel="noopener noreferrer"&gt;systray&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;8K&lt;/td&gt;
&lt;td&gt;System tray (Win32/macOS/Linux, zero CGO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/audio" rel="noopener noreferrer"&gt;audio&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;3.5K&lt;/td&gt;
&lt;td&gt;Audio engine (WASAPI, WAV, Mixer)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero CGO. &lt;code&gt;go build&lt;/code&gt; and it works. Windows, macOS, Linux, Browser.&lt;/p&gt;

&lt;p&gt;1,200+ GitHub stars. Listed in &lt;a href="https://github.com/avelino/awesome-go" rel="noopener noreferrer"&gt;awesome-go&lt;/a&gt;. Used by &lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;Born ML&lt;/a&gt; for GPU compute, &lt;a href="https://github.com/darkliquid/ironwail-go" rel="noopener noreferrer"&gt;ironwail-go&lt;/a&gt; for Quake 1 rendering.&lt;/p&gt;




&lt;p&gt;The compositor migration landed across three releases: v0.51.0 (&lt;a href="https://github.com/gogpu/gogpu/commit/5651dee" rel="noopener noreferrer"&gt;ADR-065&lt;/a&gt; + &lt;a href="https://github.com/gogpu/gogpu/commit/5651dee" rel="noopener noreferrer"&gt;ADR-066&lt;/a&gt;, damage source reimplementation + pluggable overlays), v0.52.0 (&lt;a href="https://github.com/gogpu/gogpu/commit/458f232" rel="noopener noreferrer"&gt;ADR-067&lt;/a&gt;, compositor-owned render target), and v0.52.1 (&lt;a href="https://github.com/gogpu/gogpu/commit/4beb54f" rel="noopener noreferrer"&gt;ADR-069&lt;/a&gt;, package extraction).&lt;/p&gt;

&lt;p&gt;If your drawing library owns compositor state and you only have one consumer — fix it now. The second consumer always arrives sooner than you expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It, Break It, Tell Us
&lt;/h2&gt;

&lt;p&gt;GoGPU is pre-v1.0 — this is the best time to influence the API before it freezes. We need your help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test on your hardware.&lt;/strong&gt; We develop on Intel Iris Xe (Windows) and Mesa llvmpipe (Linux). AMD, NVIDIA, Apple Silicon, Adreno — every GPU we haven't tested is a bug we haven't found. Run the examples, file issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute.&lt;/strong&gt; PRs welcome — from typo fixes to new backends. We review everything. Check &lt;a href="https://github.com/gogpu/gogpu/labels/good%20first%20issue" rel="noopener noreferrer"&gt;good first issues&lt;/a&gt; across the org.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write about it.&lt;/strong&gt; Tutorials, reviews, comparisons — in any language. &lt;a href="https://www.root.cz/autori/pavel-tisnovsky/" rel="noopener noreferrer"&gt;Pavel Tišnovský&lt;/a&gt; wrote three parts on root.cz (Czech), covering gg 2D graphics and gogpu windowing with 12 interactive examples. If you write something, let us know — we'll link it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spread the word.&lt;/strong&gt; Star us, mention us in your talks, recommend us when someone asks "how do I do GPU in Go?"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Support the Project
&lt;/h2&gt;

&lt;p&gt;GoGPU is built by a small team in their spare time. If you find it useful, consider supporting continued development:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://opencollective.com/gogpu" rel="noopener noreferrer"&gt;Support GoGPU on Open Collective&lt;/a&gt;&lt;/strong&gt; — every contribution helps us spend more time on the ecosystem instead of client work.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Star us on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;github.com/gogpu/gogpu&lt;/a&gt; ⭐&lt;/p&gt;

</description>
      <category>go</category>
      <category>gpu</category>
      <category>architecture</category>
      <category>graphics</category>
    </item>
    <item>
      <title>We Built a Pure Go 3D Renderer — Then Embedded It Inside a GUI Widget</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Sun, 02 Aug 2026 21:33:11 +0000</pubDate>
      <link>https://dev.to/kolkov/we-built-a-pure-go-3d-renderer-then-embedded-it-inside-a-gui-widget-4mo4</link>
      <guid>https://dev.to/kolkov/we-built-a-pure-go-3d-renderer-then-embedded-it-inside-a-gui-widget-4mo4</guid>
      <description>&lt;p&gt;Go has had a 2D graphics story for a while now. But 3D? The options have been either CGO wrappers around OpenGL, abandoned projects from 2019, or "use Ebiten and write your own engine."&lt;/p&gt;

&lt;p&gt;We decided to build the missing piece: &lt;strong&gt;g3d&lt;/strong&gt; — a Pure Go 3D rendering library with scene graph, PBR materials, and forward rendering pipeline. Zero CGO, all five GPU backends (Vulkan, Metal, DX12, GLES, Software), 374 tests, built on our &lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;WebGPU implementation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But a 3D renderer that only works standalone is half the story. The real question is: &lt;strong&gt;how does it compose with UI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post covers the two integration patterns we shipped in v0.1.4 — patterns that every enterprise 3D application uses, from Unity to Blender to CAD tools. And how we filed eight cross-repo issues across five repositories to make it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 20-Line Rotating Cube
&lt;/h2&gt;

&lt;p&gt;Before we get to integration — here's what g3d looks like standalone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetBackground&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;sun&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDirectionalLight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithLightColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;White&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithLightIntensity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sun&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LightNode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;cube&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewBoxGeometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewStandardMaterial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithRoughness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cube&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MeshNode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;camera&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewPerspectiveCamera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;800.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;600.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CameraNode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Vec3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Z&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scene graph, PBR materials, directional lighting — all CPU data structures. No GPU device needed until you render. The API is inspired by Three.js, but idiomatic Go: interfaces + functional options, not OOP inheritance chains.&lt;/p&gt;

&lt;p&gt;The renderer creates itself lazily on first frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gogpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;renderer&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRenderer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GPUContextProvider&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SurfaceView&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Rotating PBR cube on Vulkan. Single binary, zero CGO, &lt;code&gt;go build &amp;amp;&amp;amp; ./app&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Challenge: 3D + UI
&lt;/h2&gt;

&lt;p&gt;A standalone 3D window is a demo. A 3D viewport inside a GUI application — that's a product. Think about what real 3D applications look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Game:&lt;/strong&gt; fullscreen 3D world with health bar, minimap, crosshair overlaid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAD tool:&lt;/strong&gt; 3D model viewer with toolbar, property panel, status bar around it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IDE:&lt;/strong&gt; 3D preview panel embedded alongside code editor and file tree&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are two fundamentally different composition patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;3D Renders To&lt;/th&gt;
&lt;th&gt;UI Renders To&lt;/th&gt;
&lt;th&gt;Who Owns the Surface&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fullscreen overlay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Swapchain directly&lt;/td&gt;
&lt;td&gt;Same swapchain (on top)&lt;/td&gt;
&lt;td&gt;Both, sequentially&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Embedded widget&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Offscreen texture&lt;/td&gt;
&lt;td&gt;Swapchain (compositor)&lt;/td&gt;
&lt;td&gt;UI framework&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We implemented both. And both required solving problems that no Go project had solved before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Fullscreen 3D + 2D Overlay
&lt;/h2&gt;

&lt;p&gt;This is the game/CAD pattern. g3d renders the 3D scene to fill the entire window, then gg (our 2D graphics library) draws HUD elements on top: title, FPS counter, crosshair, status bar.&lt;/p&gt;

&lt;p&gt;The enterprise pattern is well-established — we researched seven engines:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;How They Do It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bevy + egui&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate render pass, &lt;code&gt;LoadOp::Load&lt;/code&gt;, &lt;code&gt;depth_stencil: None&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dear ImGui&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate render pass, &lt;code&gt;DepthEnable=false&lt;/code&gt;, alpha blending&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Screen Space Overlay canvas, rendered after everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Godot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CanvasLayer, separate from 3D viewport&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Three.js&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;clearDepth()&lt;/code&gt; between 3D and HUD render calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The universal answer: &lt;strong&gt;two render passes on the same surface&lt;/strong&gt;. First pass clears and renders 3D with depth testing. Second pass loads the existing content and renders 2D with alpha blending, no depth.&lt;/p&gt;

&lt;p&gt;In g3d + gogpu + gg, this looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gogpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Pass 1: g3d renders 3D scene (LoadOp::Clear + depth)&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SurfaceView&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c"&gt;// Bridge: tell gogpu the surface has content&lt;/span&gt;
    &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkExternalContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// Pass 2: gg renders 2D HUD (LoadOp::Load + alpha blend)&lt;/span&gt;
    &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;drawHUD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RenderTarget&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MarkExternalContent()&lt;/code&gt; is the key API. It tells the gogpu framework: "someone already rendered to this surface — don't clear it." Without this call, gg's render pass would wipe the 3D scene with &lt;code&gt;LoadOp::Clear&lt;/code&gt;. With it, gg uses &lt;code&gt;LoadOp::Load&lt;/code&gt; and draws transparently on top.&lt;/p&gt;

&lt;p&gt;This is the same concept as Qt's &lt;code&gt;beginExternal()&lt;/code&gt;/&lt;code&gt;endExternal()&lt;/code&gt;, Flutter's &lt;code&gt;InlinePassContext&lt;/code&gt;, and Unity's &lt;code&gt;Camera.DontClear&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result
&lt;/h3&gt;

&lt;p&gt;A rotating PBR cube at 60 FPS with a full HUD overlay — title, live FPS counter, animated crosshair, status bar with backend info. All Pure Go, all on the same swapchain, zero copies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vulkan go run ./examples/fullscreen-overlay/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pattern 2: 3D Inside a GUI Widget
&lt;/h2&gt;

&lt;p&gt;This is the CAD/IDE pattern. The 3D viewport is one widget among many — surrounded by buttons, text, panels. The UI framework owns the window; g3d renders into an offscreen GPU texture; the compositor blits it into the widget tree.&lt;/p&gt;

&lt;p&gt;This is how Qt's &lt;code&gt;QRhiWidget&lt;/code&gt;, Unity's &lt;code&gt;RenderTexture&lt;/code&gt;, and Godot's &lt;code&gt;SubViewport&lt;/code&gt; work.&lt;/p&gt;

&lt;p&gt;In g3d + gogpu/ui:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;vp&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gpuview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;gpuview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;gpuview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Continuous&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;gpuview&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="n"&gt;gpucontext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextureView&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wgpuView&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;wgpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextureView&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pointer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wgpuView&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;gpuview.Widget&lt;/code&gt; (formerly Viewport3D, renamed for universality) handles the GPU texture lifecycle. It creates an offscreen texture, passes it to your &lt;code&gt;OnRender&lt;/code&gt; callback, and hands the result to the Layer Tree compositor for blitting into the final frame.&lt;/p&gt;

&lt;p&gt;Your 3D renderer doesn't know or care that it's inside a widget. It receives a &lt;code&gt;TextureView&lt;/code&gt; and renders to it — same API as fullscreen rendering.&lt;/p&gt;

&lt;p&gt;The UI layout is standard gogpu/ui:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;uiApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primitives&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;primitives&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"g3d GPUView"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bold&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;vp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// 3D viewport widget&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextOpt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Pause / Resume"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;paused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;paused&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;28&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShadowLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Material 3 themed card with a 3D viewport, title, and control buttons. The 3D content renders at GPU speed inside the widget while the rest of the UI renders through the standard compositor pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run ./examples/viewport3d/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Eight Issues Across Five Repos
&lt;/h2&gt;

&lt;p&gt;Getting both patterns working wasn't just API design — it was a debugging marathon across five repositories (g3d, gg, gogpu, ui, wgpu) and eight issues. Here are the three hardest:&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: MSAA Resolve Overwrites External Content
&lt;/h3&gt;

&lt;p&gt;When gg renders its 2D overlay, it uses 4x MSAA for antialiased edges. The MSAA render pass writes to an intermediate texture, then resolves to the swapchain. The resolve is a full overwrite — it doesn't care what was on the swapchain before.&lt;/p&gt;

&lt;p&gt;So even with &lt;code&gt;LoadOp::Load&lt;/code&gt;, the MSAA intermediate texture was empty (it never contained the 3D scene), and the resolve wrote &lt;code&gt;empty + HUD&lt;/code&gt; to the swapchain. 3D content gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; gg v0.50.9 implemented Strategy C from Skia's playbook — render the overlay into a separate offscreen MSAA texture, resolve to a 1x intermediate with alpha, then alpha-blend composite onto the swapchain via textured quad. The 3D content survives because the swapchain is never a resolve target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: GPU Buffer Use-After-Free
&lt;/h3&gt;

&lt;p&gt;g3d's renderer created uniform buffers per frame with &lt;code&gt;MappedAtCreation&lt;/code&gt;, used them in a render pass, submitted the command buffer, and immediately released them via &lt;code&gt;defer&lt;/code&gt;. But &lt;code&gt;queue.Submit()&lt;/code&gt; is asynchronous — the GPU was still reading the buffers when Go freed them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command buffer at index 0 references released buffer "g3d_frame_uniforms"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Replaced per-frame allocation with persistent uniform buffers updated via &lt;code&gt;queue.WriteBuffer()&lt;/code&gt;. Geometry buffers cached by identity. Bind groups deferred-released at the start of the next frame. This is the same pattern gg uses internally — zero allocations in the hot path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: Layer Tree Missing External Texture Node
&lt;/h3&gt;

&lt;p&gt;The gpuview widget created its texture, fired &lt;code&gt;OnRender&lt;/code&gt;, and g3d rendered into it correctly. But the widget was invisible. The Layer Tree compositor had &lt;code&gt;ExternalTextureLayer&lt;/code&gt; support in its rendering code — but nobody created those nodes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buildBoundaryLayer()&lt;/code&gt; only created &lt;code&gt;PictureLayer&lt;/code&gt; nodes. Widgets with external GPU textures needed a parallel &lt;code&gt;ExternalTextureLayer&lt;/code&gt; node so the compositor would blit their content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; ui v0.1.49 added &lt;code&gt;externalTextureWidget&lt;/code&gt; interface detection in the Layer Tree builder. Widgets that provide &lt;code&gt;Texture()&lt;/code&gt; and &lt;code&gt;ViewportSize()&lt;/code&gt; automatically get an &lt;code&gt;ExternalTextureLayer&lt;/code&gt; alongside their &lt;code&gt;PictureLayer&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Command Encoder: The Third Way
&lt;/h2&gt;

&lt;p&gt;Sometimes you don't want two separate &lt;code&gt;queue.Submit()&lt;/code&gt; calls. g3d v0.1.3 added &lt;code&gt;RenderTo()&lt;/code&gt; — record g3d render passes into a caller-owned command encoder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateCommandEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// g3d records its render pass&lt;/span&gt;
&lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RenderTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetView&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Other renderers record their passes&lt;/span&gt;
&lt;span class="n"&gt;overlayRenderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// One submit for the entire frame&lt;/span&gt;
&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates the multi-submit overhead and gives you precise control over render pass ordering. It's the pattern gogpu v0.48.4 uses internally via &lt;code&gt;dc.CommandEncoder()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Here's how g3d fits into the gogpu ecosystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Application
    ├── gogpu (window + GPU device)
    ├── g3d  (3D scene → render passes)
    ├── gg   (2D graphics → render passes)
    └── ui   (widget tree → compositor → render passes)
              └── gpuview widget ← g3d renders here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;g3d depends &lt;strong&gt;down&lt;/strong&gt; only — on wgpu and gpucontext. Never on gogpu, gg, or ui. This means you can use g3d in any context: with gogpu, with your own windowing, or headless for testing.&lt;/p&gt;

&lt;p&gt;The shared GPU device comes through &lt;code&gt;gpucontext.DeviceProvider&lt;/code&gt; — the same &lt;code&gt;database/sql&lt;/code&gt;-style interface pattern that gg, ui, and Born ML use. One device, shared across all renderers, zero-copy resource sharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ecosystem Is Accelerating
&lt;/h2&gt;

&lt;p&gt;g3d doesn't exist in isolation. The gogpu ecosystem is growing in directions that directly benefit 3D applications:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero-CGO Android.&lt;/strong&gt; @besmpl proved that &lt;code&gt;CGO_ENABLED=0 -buildmode=c-shared&lt;/code&gt; works on Android arm64 via &lt;code&gt;//go:nativeexport&lt;/code&gt; — a Go toolchain patch that eliminates the last CGO dependency for mobile GPU apps. The &lt;a href="https://github.com/besmpl/go-android-cgo-free-prototype" rel="noopener noreferrer"&gt;prototype&lt;/a&gt; runs on API 30 emulator. Once upstream, g3d will run on Android with the same &lt;code&gt;go build&lt;/code&gt; simplicity as desktop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pure Go Race Detector.&lt;/strong&gt; The GoGPU ecosystem can't use &lt;code&gt;go test -race&lt;/code&gt; today because GPU backends require &lt;code&gt;CGO_ENABLED=0&lt;/code&gt;. We're &lt;a href="https://github.com/golang/go/issues/76786" rel="noopener noreferrer"&gt;contributing to a Pure Go race detector&lt;/a&gt; that achieves read-path parity with TSAN (0.985x on Apple M1). @besmpl delivered 20 optimization commits in 5 days. When merged, every GPU-accelerated Go project gets race detection for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser/WASM.&lt;/strong&gt; wgpu's triple-backend architecture (ADR-038) means g3d will run in the browser via WebGPU — same Go code, &lt;code&gt;GOOS=js GOARCH=wasm go build&lt;/code&gt;. The browser backend is already shipping in wgpu for 2D; 3D follows the same path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composition library.&lt;/strong&gt; &lt;a href="https://github.com/gogpu/compose" rel="noopener noreferrer"&gt;gogpu/compose&lt;/a&gt; enables multi-process GPU composition — think VS Code's renderer process architecture, but for Go. A g3d viewport in one process, a 2D editor in another, composited via Unix socket + LZ4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community tutorials.&lt;/strong&gt; Pavel Tišnovský (2,185+ articles on root.cz, Czech Republic) published &lt;a href="https://www.root.cz/clanky/tvorba-2d-i-3d-grafiky-a-animaci-v-go-s-vyuzitim-projektu-gogpu/" rel="noopener noreferrer"&gt;88 minutes of tutorial content&lt;/a&gt; covering the ecosystem with 54 working examples — the first serious European press coverage of Go GPU graphics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Games are happening.&lt;/strong&gt; @kivutar built &lt;a href="https://www.youtube.com/watch?v=KgdScyi_xZ0" rel="noopener noreferrer"&gt;Goro — a Ragnarok Online client&lt;/a&gt; using gogpu/ui in Pure Go (no CGO), with working UI, mercenaries, and gameplay. @darkliquid's &lt;a href="https://github.com/gogpu/gogpu/issues/163" rel="noopener noreferrer"&gt;Quake 1 port&lt;/a&gt; runs on our Vulkan stack. The Go game dev community is real — and it needs a 3D library that doesn't require CGO.&lt;/p&gt;

&lt;h2&gt;
  
  
  By the Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;g3d LOC&lt;/td&gt;
&lt;td&gt;~11,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;374&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU backends&lt;/td&gt;
&lt;td&gt;5 (Vulkan, Metal, DX12, GLES, Software)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;3 (hello-cube, fullscreen-overlay, viewport3d)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-repo bugs found&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-repo bugs fixed&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Releases&lt;/td&gt;
&lt;td&gt;3 (v0.1.2 → v0.1.4, Jul 26 – Aug 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem total&lt;/td&gt;
&lt;td&gt;1.2M+ LOC Pure Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Standalone 3D&lt;/span&gt;
go run github.com/gogpu/g3d/examples/hello-cube@latest

&lt;span class="c"&gt;# Fullscreen 3D + 2D overlay&lt;/span&gt;
go run github.com/gogpu/g3d/examples/fullscreen-overlay@latest

&lt;span class="c"&gt;# 3D inside UI widget&lt;/span&gt;
go run github.com/gogpu/g3d/examples/viewport3d@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Select your GPU backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vulkan   go run ./examples/hello-cube/
&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dx12     go run ./examples/hello-cube/
&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;software go run ./examples/hello-cube/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;g3d v0.1.4 has the foundation: scene graph, PBR materials, forward renderer, and two production-ready integration patterns. Here's what's coming:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Real Materials.&lt;/strong&gt; Cook-Torrance BRDF replaces Blinn-Phong. Shadow mapping (directional + point). Normal maps, metallic/roughness textures, emissive maps. This is where g3d goes from "demo-ready" to "product-ready."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — GLTF 2.0.&lt;/strong&gt; Binary &lt;code&gt;.glb&lt;/code&gt; and JSON &lt;code&gt;.gltf&lt;/code&gt; loading with PBR materials, skeletal animation, and morph targets. GLTF is the "JPEG of 3D" — every modeling tool exports it, every engine imports it. g3d will too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4 — Scale.&lt;/strong&gt; Instance batching for thousands of objects. Environment maps for reflections. Post-processing pipeline (bloom, tone mapping, FXAA). Skybox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 5 — Performance.&lt;/strong&gt; BVH-accelerated frustum culling, LOD (level of detail), SIMD math via Go 1.25+ &lt;code&gt;goexperiment.simd&lt;/code&gt;. The &lt;a href="https://github.com/gomlx/gomlx" rel="noopener noreferrer"&gt;GoMLX PackGEMM&lt;/a&gt; team proved 14x speedup with Pure Go AVX-512 — same approach applies to matrix math.&lt;/p&gt;

&lt;p&gt;The rendering library was the last missing piece in Go's graphics ecosystem. 2D graphics, GUI toolkit, shader compiler, audio engine, system tray — all Pure Go, all shipping. Now 3D joins them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How You Can Help
&lt;/h2&gt;

&lt;p&gt;This project grows through real-world usage. Every bug report from a different GPU, every feature request from a real application, every "it works on my AMD/NVIDIA/Apple M4" — that's data we can't generate alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test it on your hardware.&lt;/strong&gt; We develop on Intel Iris Xe. AMD, NVIDIA, Apple Silicon, Adreno — we need your GPU. A simple &lt;code&gt;go run ./examples/hello-cube/&lt;/code&gt; and a one-line issue "works on RTX 4080 / Vulkan" is genuinely valuable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build something with it.&lt;/strong&gt; A 3D file viewer. A data visualizer. A game prototype. A CAD preview panel. The API is designed to be embedded — g3d doesn't own your application, you own g3d. The integration patterns in this article work today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Report what's missing.&lt;/strong&gt; We're tracking Phase 2–5 features, but priority depends on what people actually need. Shadows? GLTF? Instance batching? Textures? &lt;a href="https://github.com/orgs/gogpu/discussions" rel="noopener noreferrer"&gt;Tell us&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spread the word.&lt;/strong&gt; Star the repos, share the examples, write about your experience. Go's graphics ecosystem has been invisible for 17 years because nobody knew it existed. That changes when the community talks about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contribute code.&lt;/strong&gt; The &lt;a href="https://github.com/gogpu/g3d/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; has everything you need. Good first issues are labeled. We review PRs within 24 hours. Every contributor gets credit in the CHANGELOG and release notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support the project.&lt;/strong&gt; We accept donations via &lt;a href="https://opencollective.com/gogpu" rel="noopener noreferrer"&gt;Open Collective&lt;/a&gt; to fund development, testing on diverse GPU hardware, and CI infrastructure. Every contribution helps us test on more platforms and ship faster.&lt;/p&gt;

&lt;p&gt;The ecosystem is 1.2M+ lines of Pure Go — but the codebase isn't what makes it real. Users make it real. Applications make it real. The more people building on g3d, the faster it reaches the quality bar that Go deserves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;g3d:&lt;/strong&gt; &lt;a href="https://github.com/gogpu/g3d" rel="noopener noreferrer"&gt;github.com/gogpu/g3d&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GoGPU ecosystem:&lt;/strong&gt; &lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;github.com/gogpu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions:&lt;/strong&gt; &lt;a href="https://github.com/orgs/gogpu/discussions" rel="noopener noreferrer"&gt;github.com/orgs/gogpu/discussions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;root.cz coverage (Czech):&lt;/strong&gt; &lt;a href="https://www.root.cz/clanky/tvorba-2d-i-3d-grafiky-a-animaci-v-go-s-vyuzitim-projektu-gogpu/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; · &lt;a href="https://www.root.cz/clanky/tvorba-2d-i-3d-grafiky-a-animaci-v-go-s-vyuzitim-projektu-gogpu-2-cast/" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go waited 17 years for a professional graphics ecosystem. We're building it — and 3D rendering just shipped. Now we need you building on it.&lt;/p&gt;

</description>
      <category>go</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Go's sql.Null[T] Will Never Get JSON Support. Here's What We Built Instead.</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Sat, 04 Jul 2026 18:52:37 +0000</pubDate>
      <link>https://dev.to/kolkov/gos-sqlnullt-will-never-get-json-support-heres-what-we-built-instead-2apk</link>
      <guid>https://dev.to/kolkov/gos-sqlnullt-will-never-get-json-support-heres-what-we-built-instead-2apk</guid>
      <description>&lt;p&gt;Go 1.22 gave us &lt;code&gt;sql.Null[T]&lt;/code&gt; — a generic nullable type for SQL. Problem solved?&lt;/p&gt;

&lt;p&gt;No. Try marshaling it to JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"V"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not &lt;code&gt;"Alice"&lt;/code&gt;. That's not &lt;code&gt;null&lt;/code&gt;. That's a broken API response.&lt;/p&gt;

&lt;p&gt;And it will &lt;strong&gt;never be fixed&lt;/strong&gt;. &lt;a href="https://github.com/golang/go/issues/68375" rel="noopener noreferrer"&gt;Issue #68375&lt;/a&gt; — proposal to add &lt;code&gt;MarshalJSON&lt;/code&gt; to &lt;code&gt;sql.Null[T]&lt;/code&gt; — was &lt;strong&gt;closed as infeasible&lt;/strong&gt;. Go's policy forbids adding marshal methods to types that already have a "reasonable" default marshaling. The struct marshaling &lt;code&gt;{"V":...,"Valid":...}&lt;/code&gt; counts as "reasonable."&lt;/p&gt;

&lt;p&gt;This is a permanent gap in Go's standard library.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Bad Options (Before opt)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;sql.Null[T]&lt;/code&gt; — SQL works, JSON broken
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Null&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="s"&gt;`json:"name"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// Marshals to: {"name":{"V":"Alice","Valid":true}}&lt;/span&gt;
&lt;span class="c"&gt;// You wanted:  {"name":"Alice"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;code&gt;*string&lt;/code&gt; — JSON works, everything else is awkward
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"name"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// Pointer allocation overhead on every value&lt;/span&gt;
&lt;span class="c"&gt;// Can't distinguish "field absent" from "field is null"&lt;/span&gt;
&lt;span class="c"&gt;// user.Name == nil — is this "not set" or "set to null"?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;code&gt;guregu/null&lt;/code&gt; — Works, but carries 10 years of legacy
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/guregu/null" rel="noopener noreferrer"&gt;guregu/null&lt;/a&gt; (2,070 stars) is the de facto standard. It works. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v1→v6 legacy&lt;/strong&gt; — API evolved over 10 years, backward compatibility constraints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No three-state&lt;/strong&gt; — can't distinguish "field absent" from "field null" (critical for PATCH APIs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;Map&lt;/code&gt;/&lt;code&gt;FlatMap&lt;/code&gt;&lt;/strong&gt; — no functional composition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;OrNull&lt;/code&gt; constructors&lt;/strong&gt; — every project writes the same boilerplate helpers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic &lt;code&gt;Value[T]&lt;/code&gt;&lt;/strong&gt; has &lt;code&gt;MarshalText&lt;/code&gt; and &lt;code&gt;Equal&lt;/code&gt; commented out — couldn't make them work generically&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  opt: Option&amp;lt;T&amp;gt; for Go
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/coregx/opt" rel="noopener noreferrer"&gt;coregx/opt&lt;/a&gt; — designed from scratch for Go 1.24+. No legacy, no compromises.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/coregx/opt"&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;  &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="s"&gt;`json:"name"`&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="s"&gt;`json:"email,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;Age&lt;/span&gt;   &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;    &lt;span class="s"&gt;`json:"age"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Age&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntOrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c"&gt;// 0 means "not set" → null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// {"name":"Alice","age":null}&lt;/span&gt;
&lt;span class="c"&gt;// Email omitted (omitzero) — not null, just absent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON works. SQL works. No pointer overhead. No boilerplate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes opt Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Three-State &lt;code&gt;Field[T]&lt;/code&gt; — The PATCH API Killer Feature
&lt;/h3&gt;

&lt;p&gt;Every REST API with PATCH endpoints has this problem: how do you distinguish "the client didn't send this field" from "the client explicitly set it to null"?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PatchUser&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;  &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="s"&gt;`json:"name,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="s"&gt;`json:"email,omitzero"`&lt;/span&gt;
    &lt;span class="n"&gt;Age&lt;/span&gt;   &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="s"&gt;`json:"age,omitzero"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Client sends: {"name":"John","email":null}&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="n"&gt;PatchUser&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c"&gt;// true  → set name to "John"&lt;/span&gt;
&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c"&gt;// true  → set email to NULL in DB&lt;/span&gt;
&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Age&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAbsent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c"&gt;// true  → don't touch age&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three states: &lt;strong&gt;absent&lt;/strong&gt; (don't touch), &lt;strong&gt;null&lt;/strong&gt; (set to NULL), &lt;strong&gt;value&lt;/strong&gt; (set to value).&lt;/p&gt;

&lt;p&gt;Rust does this with &lt;code&gt;Option&amp;lt;Option&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;. Kotlin with nullable + optional. In Go, no library properly solves this — guregu/null doesn't distinguish absent from null, and pointer-based approaches (&lt;code&gt;**T&lt;/code&gt;) are impractical. coregx/opt's &lt;code&gt;Field[T]&lt;/code&gt; is the first clean solution with concrete typed constructors, &lt;code&gt;OrNull&lt;/code&gt;, &lt;code&gt;FlatMap&lt;/code&gt;, and a &lt;code&gt;zero/&lt;/code&gt; subpackage.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;OrNull&lt;/code&gt; Constructors — No More Boilerplate
&lt;/h3&gt;

&lt;p&gt;Every project using nullable types with a database writes the same helpers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// This is in EVERY Go project with nullable DB fields&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;optStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;optInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With opt, this is built in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;CompanyRow&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;City&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringOrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;City&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;     &lt;span class="c"&gt;// "" → null&lt;/span&gt;
    &lt;span class="n"&gt;OGRN&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringOrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OGRN&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;         &lt;span class="c"&gt;// "" → null&lt;/span&gt;
    &lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntOrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EmployeeCount&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="c"&gt;// 0 → null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One function per type. Zero boilerplate. Available for all 8 concrete types including &lt;code&gt;BoolOrNull&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Functional API — Map, FlatMap, Equal
&lt;/h3&gt;

&lt;p&gt;Inspired by Rust's &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Transform if valid, propagate null&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Alice  "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c"&gt;// opt.Option[string]{"Alice", true}&lt;/span&gt;
&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c"&gt;// opt.Option[int]{5, true}&lt;/span&gt;

&lt;span class="c"&gt;// Chain operations that may produce null&lt;/span&gt;
&lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FlatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c"&gt;// Nil-safe comparison&lt;/span&gt;
&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c"&gt;// true&lt;/span&gt;
&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;guregu/null has &lt;code&gt;ValueOr&lt;/code&gt;. That's it. No &lt;code&gt;Map&lt;/code&gt;, no &lt;code&gt;FlatMap&lt;/code&gt;, no &lt;code&gt;OrElse&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Generic &lt;code&gt;Option[T]&lt;/code&gt; That Actually Works
&lt;/h3&gt;

&lt;p&gt;guregu's generic &lt;code&gt;Value[T]&lt;/code&gt; has &lt;code&gt;MarshalText&lt;/code&gt; and &lt;code&gt;Equal&lt;/code&gt; &lt;strong&gt;commented out&lt;/strong&gt; in the source code — they couldn't make them work generically.&lt;/p&gt;

&lt;p&gt;opt's &lt;code&gt;Option[T]&lt;/code&gt; is fully functional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Works with ANY type&lt;/span&gt;
&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyCustomStruct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// {"Name":"test"}&lt;/span&gt;

&lt;span class="n"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyCustomStruct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. &lt;code&gt;zero/&lt;/code&gt; Subpackage — Alternative Semantics
&lt;/h3&gt;

&lt;p&gt;Sometimes you want zero values to &lt;strong&gt;be&lt;/strong&gt; null, and null to marshal as zero (not &lt;code&gt;null&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/coregx/opt/zero"&lt;/span&gt;

&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;// Invalid — empty = null&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// "" (not "null")&lt;/span&gt;

&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c"&gt;// Invalid — 0 = null&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// 0 (not "null")&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;&lt;code&gt;From("")&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Marshal null&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Valid (empty string)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opt/zero&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Invalid (null)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;""&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;coregx/opt&lt;/th&gt;
&lt;th&gt;guregu/null&lt;/th&gt;
&lt;th&gt;&lt;code&gt;*T&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;sql.Null[T]&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generic nullable&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;Option[T]&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;No JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three-state (PATCH)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;Field[T]&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concrete types (String, Int...)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8 types&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 types&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;OrNull&lt;/code&gt; constructors&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FieldFromOption&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map / FlatMap&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OrElse (lazy)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON marshal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Broken&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Scanner/Valuer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;omitzero (Go 1.24+)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zero-is-null subpackage&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;zero/&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zero/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;json/v2 compatible&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy code&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;v1→v6&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Zero-allocation unmarshal. Bool operations under 2 nanoseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BenchmarkBoolMarshalJSON     0.85 ns/op    0 allocs
BenchmarkBoolUnmarshalJSON   2.1 ns/op     0 allocs
BenchmarkIntUnmarshalJSON    193 ns/op     1 alloc
BenchmarkStringUnmarshalJSON 137 ns/op     0 allocs
BenchmarkStructMarshalJSON   876 ns/op     9 allocs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why New Projects Should Start with opt
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sql.Null[T]&lt;/code&gt; will never get JSON support&lt;/strong&gt; — this is official Go team position (&lt;a href="https://github.com/golang/go/issues/68375" rel="noopener noreferrer"&gt;#68375&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;*T&lt;/code&gt; can't do three-state&lt;/strong&gt; — nil means both "absent" and "null"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;guregu/null works&lt;/strong&gt; but carries v1→v6 legacy and has no PATCH support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;opt starts clean&lt;/strong&gt; — Go 1.24+, generics-first, no backward compatibility burden&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Field[T]&lt;/code&gt; is unique&lt;/strong&gt; — no other Go library properly solves the PATCH three-state problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — only Go stdlib&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;json/v2 ready&lt;/strong&gt; — works today, optimizable when json/v2 goes stable&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/coregx/opt@v0.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/coregx/opt"&lt;/span&gt;

&lt;span class="c"&gt;// Always valid&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Zero means "not set"&lt;/span&gt;
&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntOrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// null&lt;/span&gt;

&lt;span class="c"&gt;// From pointer (nil → null)&lt;/span&gt;
&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringFromPtr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emailPtr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Three-state for PATCH&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PatchRequest&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Bio&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="s"&gt;`json:"bio,omitzero"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full API and examples: &lt;a href="https://github.com/coregx/opt" rel="noopener noreferrer"&gt;github.com/coregx/opt&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Help Us Get to v1.0.0
&lt;/h2&gt;

&lt;p&gt;opt is in active development. We're heading toward a stable v1.0.0 and need your help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Try it&lt;/strong&gt; in your project — replace &lt;code&gt;*string&lt;/code&gt; or &lt;code&gt;guregu/null&lt;/code&gt; and tell us how it goes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report issues&lt;/strong&gt; — edge cases, driver compatibility, unexpected behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggest features&lt;/strong&gt; — what nullable types should do that no library does yet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send PRs&lt;/strong&gt; — new types, better tests, documentation improvements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share&lt;/strong&gt; — if opt solved a problem for you, tell your team or write about it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every bug report, feature idea, and PR brings us closer to a stable API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/coregx/opt" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://github.com/coregx/opt/issues" rel="noopener noreferrer"&gt;Issues&lt;/a&gt; | &lt;a href="https://pkg.go.dev/github.com/coregx/opt" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;opt is part of the &lt;a href="https://github.com/coregx" rel="noopener noreferrer"&gt;coregx&lt;/a&gt; ecosystem — high-performance Go libraries for production applications.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>api</category>
    </item>
    <item>
      <title>Stop Concatenating SQL Strings in Go — Dynamic Queries Done Right</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Fri, 03 Jul 2026 18:07:36 +0000</pubDate>
      <link>https://dev.to/kolkov/stop-concatenating-sql-strings-in-go-dynamic-queries-done-right-131j</link>
      <guid>https://dev.to/kolkov/stop-concatenating-sql-strings-in-go-dynamic-queries-done-right-131j</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a follow-up to my &lt;a href="https://dev.to/kolkov/relica-a-zero-dependency-query-builder-for-go-that-wont-break-your-code-1f63"&gt;earlier article about Relica&lt;/a&gt; — a zero-dependency SQL query builder for Go. Since then, we've shipped a major &lt;a href="https://github.com/coregx/relica/releases/tag/v0.12.0" rel="noopener noreferrer"&gt;v0.12.0 release&lt;/a&gt; with comprehensive security hardening, zero-panic guarantee, and integration tests on all three databases. But today I want to talk about the real problem — the one that made me build Relica in the first place.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you've built an API with filterable endpoints in Go, you've written code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"SELECT * FROM products WHERE 1=1"&lt;/span&gt;
&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;" AND category = $"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Itoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinPrice&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;" AND price &amp;gt;= $"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Itoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InStock&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;" AND stock &amp;gt; 0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The infamous &lt;code&gt;WHERE 1=1&lt;/code&gt; hack. Manual placeholder numbering that breaks when you reorder conditions. String concatenation that smells like SQL injection. And when you switch from PostgreSQL (&lt;code&gt;$1, $2&lt;/code&gt;) to MySQL (&lt;code&gt;?, ?&lt;/code&gt;) — you rewrite every single query.&lt;/p&gt;

&lt;p&gt;I've been writing Go for production systems since 2019, and this pattern shows up in &lt;em&gt;every&lt;/em&gt; codebase I've worked on. It's the single most common SQL pain point in the Go ecosystem, and in 2026, the mainstream libraries still don't solve it cleanly.&lt;/p&gt;

&lt;p&gt;Let me show you why — and what I did about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Every Go SQL Library Gets Dynamic Queries Wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The sqlc problem: static by design
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/sqlc-dev/sqlc" rel="noopener noreferrer"&gt;sqlc&lt;/a&gt; is brilliant for what it does — generating type-safe Go code from SQL. But dynamic queries? That's been &lt;a href="https://github.com/sqlc-dev/sqlc/discussions/364" rel="noopener noreferrer"&gt;the #1 feature request since 2020&lt;/a&gt;, with 150+ reactions and dozens of comments — still unresolved.&lt;/p&gt;

&lt;p&gt;The official workaround looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Optional filter in sqlc&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;has_category&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;has_min_price&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;min_price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're writing &lt;em&gt;more&lt;/em&gt; SQL, not less. And this pattern defeats index optimization — PostgreSQL can't use a category index when the condition is wrapped in &lt;code&gt;NOT @has_category OR ...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your API has 8 optional filters, you get 8 boolean parameters and 8 CASE-like conditions. It's not scalable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The sqlx problem: no builder, no maintainer
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/jmoiron/sqlx" rel="noopener noreferrer"&gt;sqlx&lt;/a&gt; extends &lt;code&gt;database/sql&lt;/code&gt; with struct scanning — and that's it. No query builder. Dynamic queries mean string concatenation, exactly like raw &lt;code&gt;database/sql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But here's the bigger problem: &lt;strong&gt;sqlx is effectively abandoned&lt;/strong&gt;. Multiple issues (&lt;a href="https://github.com/jmoiron/sqlx/issues/883" rel="noopener noreferrer"&gt;#883&lt;/a&gt;, &lt;a href="https://github.com/jmoiron/sqlx/issues/969" rel="noopener noreferrer"&gt;#969&lt;/a&gt;) ask "is this project dead?" with no response. A &lt;a href="https://www.mikejohnson.dev/posts/2025/03/sqlx-fork" rel="noopener noreferrer"&gt;community fork&lt;/a&gt; was created in March 2025 because "the original project has been inactive for several years."&lt;/p&gt;

&lt;p&gt;Building production systems on an abandoned library is a ticking time bomb.&lt;/p&gt;

&lt;h3&gt;
  
  
  The GORM problem: magic without transparency
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://gorm.io/" rel="noopener noreferrer"&gt;GORM&lt;/a&gt; handles dynamic queries well — &lt;code&gt;db.Where("category = ?", cat).Where("price &amp;gt;= ?", min)&lt;/code&gt; works naturally. But GORM's abstractions come with hidden costs that surface in production:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The zero-value gotcha:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// GORM silently ignores zero-value fields&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// Generated: SELECT * FROM products&lt;/span&gt;
&lt;span class="c"&gt;// Expected:  SELECT * FROM products WHERE price = 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GORM skips &lt;code&gt;Price: 0&lt;/code&gt; because Go's zero value for int is 0. Your query returns &lt;em&gt;all&lt;/em&gt; products when you wanted free ones. This has burned &lt;a href="https://jsnfwlr.com/blog/2025/03/30/why-gorm-is-overrated/" rel="noopener noreferrer"&gt;countless developers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden N+1 queries:&lt;/strong&gt;&lt;br&gt;
Fetching 100 orders with customer data? GORM might fire 101 queries — one for orders, one per customer. You won't know until your p99 latency spikes from 2ms to 6 seconds under load. The SQL is hidden behind layers of reflection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it brings external dependencies&lt;/strong&gt; into your &lt;code&gt;go.sum&lt;/code&gt; — GORM core plus a driver means multiple transitive packages. Each one is a potential supply chain vulnerability, a version conflict, a breaking change in an update.&lt;/p&gt;
&lt;h3&gt;
  
  
  The squirrel problem: unmaintained and unsafe
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/Masterminds/squirrel" rel="noopener noreferrer"&gt;squirrel&lt;/a&gt; was once the go-to Go query builder. But it has 96 open issues, no releases since 2023, and a &lt;a href="https://deepsource.com/directory/go/issues/GO-S1017" rel="noopener noreferrer"&gt;known security issue&lt;/a&gt; with unsafe identifier quoting. The maintainer explicitly stated they "will not necessarily respond" to bug reports.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Go Developers Actually Want
&lt;/h2&gt;

&lt;p&gt;The Go community keeps having the &lt;a href="https://status-code.medium.com/why-go-developers-cant-stop-arguing-about-orms-4fa32df510b0" rel="noopener noreferrer"&gt;same debate&lt;/a&gt;: ORM vs raw SQL. But both sides want the same thing — convenience without losing control.&lt;/p&gt;

&lt;p&gt;The requirements are clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic query builder&lt;/strong&gt; — composable WHERE conditions without string concatenation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent SQL&lt;/strong&gt; — see exactly what's being sent to the database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero magic&lt;/strong&gt; — no hidden queries, no zero-value gotchas, no reflection surprises&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal dependencies&lt;/strong&gt; — ideally zero in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-database&lt;/strong&gt; — same API for PostgreSQL, MySQL, SQLite&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is exactly why I built &lt;a href="https://github.com/coregx/relica" rel="noopener noreferrer"&gt;Relica&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some context: &lt;a href="https://github.com/go-ozzo/ozzo-dbx" rel="noopener noreferrer"&gt;ozzo-dbx&lt;/a&gt; by Qiang Xue (creator of the Yii framework) was one of the best-designed Go query builders — clean Expression API, composable WHERE clauses, struct scanning without ORM magic. But it was built in 2015-2016 and hasn't been updated for modern Go.&lt;/p&gt;

&lt;p&gt;Qiang &lt;a href="https://github.com/go-ozzo/ozzo-validation/issues/207" rel="noopener noreferrer"&gt;just gave me full maintainer rights&lt;/a&gt; to the entire &lt;a href="https://github.com/go-ozzo" rel="noopener noreferrer"&gt;go-ozzo&lt;/a&gt; ecosystem. I've been maintaining Relica as ozzo-dbx's successor — rebuilt from scratch with Go 1.21+, zero production dependencies, and comprehensive security hardening that the original never had.&lt;/p&gt;

&lt;p&gt;If you used ozzo-dbx and loved the design but wished someone was keeping the lights on — &lt;a href="https://github.com/coregx/relica" rel="noopener noreferrer"&gt;Relica&lt;/a&gt; is what it was always meant to become.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building Dynamic Queries Without String Concatenation
&lt;/h2&gt;

&lt;p&gt;Here's that same filtering problem with Relica's Expression API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinPrice&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GreaterOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinPrice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxPrice&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LessOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxPrice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InStock&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;WHERE 1=1&lt;/code&gt;. No placeholder numbering. No string concatenation. Each &lt;code&gt;.Where()&lt;/code&gt; call adds an AND condition — conditions compose naturally.&lt;/p&gt;

&lt;p&gt;Column names are automatically quoted for your dialect (&lt;code&gt;"price"&lt;/code&gt; for PostgreSQL, &lt;code&gt;`price`&lt;/code&gt; for MySQL). Values are always parameterized — SQL injection is structurally impossible through the Expression API.&lt;/p&gt;

&lt;p&gt;And switching databases? Change the connection string. The query code is identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  See the SQL Before You Run It
&lt;/h3&gt;

&lt;p&gt;Unlike GORM, you always know what SQL Relica generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToSQL&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// PostgreSQL: SELECT "id", "name", "price", "category"&lt;/span&gt;
&lt;span class="c"&gt;//   FROM "products"&lt;/span&gt;
&lt;span class="c"&gt;//   WHERE "category" = $1 AND "price" &amp;gt;= $2 AND "stock" &amp;gt; $3&lt;/span&gt;

&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// [electronics 100 0]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ToSQL()&lt;/code&gt; works on all six query types — SELECT, INSERT, UPDATE, DELETE, UPSERT, and BatchInsert. In development, log every query. In production, use it for debugging slow queries. No guessing, no Debug mode — just call &lt;code&gt;ToSQL()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Complex Filters with Expression Combinators
&lt;/h3&gt;

&lt;p&gt;For OR conditions, nested logic, and subqueries — the Expression API composes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Active premium users OR users with high spending&lt;/span&gt;
&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"premium"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total_spent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c"&gt;// Generated: WHERE ("status" = $1 AND "plan" = $2) OR ("total_spent" &amp;gt; $3)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is composable at the Go level — not SQL string templates, not code generation, not struct tags. Plain Go code that produces safe, readable SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Dynamic Queries: What a Modern Go Query Builder Looks Like
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Struct-Based CRUD Without the ORM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c"&gt;// user.ID is populated automatically&lt;/span&gt;
&lt;span class="c"&gt;// PostgreSQL uses RETURNING, MySQL uses LastInsertId — you don't care&lt;/span&gt;

&lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"alice.smith@example.com"&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdateChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// Only updates email — detects changed fields via reflect&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not an ORM. There are no relations, no eager loading, no migrations, no hooks. It's struct scanning and query building — explicit operations that produce predictable SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  JOINs with Properly Quoted Table Aliases
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"c.name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"e.name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"e.salary"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"companies c"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;LeftJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"employees e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"e.company_id = c.id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"c.status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"e.salary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;80000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Table-aliased columns like &lt;code&gt;c.status&lt;/code&gt; are correctly split and quoted for each dialect — &lt;code&gt;"c"."status"&lt;/code&gt; for PostgreSQL, &lt;code&gt;`c`.`status`&lt;/code&gt; for MySQL. This was &lt;a href="https://github.com/coregx/relica/pull/22" rel="noopener noreferrer"&gt;a real bug we found and fixed&lt;/a&gt; — every identifier in the entire SQL generation pipeline is now properly quoted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transactions with Full API Parity
&lt;/h3&gt;

&lt;p&gt;All query methods available on &lt;code&gt;DB&lt;/code&gt; are also available on &lt;code&gt;Tx&lt;/code&gt; — including batch operations and upserts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transactional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;99.99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;149.50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;  &lt;span class="c"&gt;// auto-commit&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c"&gt;// Panic inside the closure? Auto-rollback + re-panic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Error Classification That Works Across Databases
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsUniqueViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Duplicate email — same function on PostgreSQL, MySQL, SQLite&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrEmailTaken&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsForeignKeyViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Referenced record doesn't exist&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrInvalidReference&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No parsing error strings. No database-specific error code checks. One function, three databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-Grade Engineering
&lt;/h2&gt;

&lt;p&gt;Relica isn't a weekend project. The v0.12.0 release involved a thorough security review that fixed 32 findings across identifier quoting, error handling, API consistency, and correctness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1700+ tests&lt;/strong&gt;, ~90% coverage — unit tests with &lt;code&gt;go-sqlmock&lt;/code&gt;, integration tests on real PostgreSQL, MySQL, and SQLite via testcontainers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero production dependencies&lt;/strong&gt; — only &lt;code&gt;database/sql&lt;/code&gt; from the standard library&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero panics from public API&lt;/strong&gt; — every error path returns &lt;code&gt;error&lt;/code&gt;, never crashes your process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero lint issues&lt;/strong&gt; — golangci-lint with 10+ analyzers including cyclop, govet, goconst&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Listed in &lt;a href="https://github.com/avelino/awesome-go" rel="noopener noreferrer"&gt;awesome-go&lt;/a&gt;&lt;/strong&gt; under SQL Query Builders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API stability commitment&lt;/strong&gt; — Relica follows "never release v2.0.0" philosophy. After v1.0, your code won't break on updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integration tests run on real databases with &lt;a href="https://github.com/coregx/relica/tree/main/test/testdata" rel="noopener noreferrer"&gt;SQL reserved words as column names&lt;/a&gt; (&lt;code&gt;order&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;group&lt;/code&gt;) — because if your query builder can't handle &lt;code&gt;WHERE "order" = $1&lt;/code&gt;, it's not production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started in 30 Seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/coregx/relica
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/coregx/relica"&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"github.com/lib/pq"&lt;/span&gt;  &lt;span class="c"&gt;// or mysql, or sqlite&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"postgres://user:pass@localhost/mydb?sslmode=disable"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c"&gt;// Dynamic query — no string concatenation&lt;/span&gt;
&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"created_at DESC"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works with &lt;code&gt;lib/pq&lt;/code&gt;, &lt;code&gt;pgx&lt;/code&gt;, &lt;code&gt;go-sql-driver/mysql&lt;/code&gt;, &lt;code&gt;modernc.org/sqlite&lt;/code&gt;, and any &lt;code&gt;database/sql&lt;/code&gt;-compatible driver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full docs&lt;/strong&gt;: &lt;a href="https://pkg.go.dev/github.com/coregx/relica" rel="noopener noreferrer"&gt;pkg.go.dev/github.com/coregx/relica&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Migration guides&lt;/strong&gt;: &lt;a href="https://github.com/coregx/relica/blob/main/docs/guides/MIGRATION_FROM_GORM.md" rel="noopener noreferrer"&gt;From GORM&lt;/a&gt; | &lt;a href="https://github.com/coregx/relica/blob/main/docs/guides/MIGRATION_FROM_SQLX.md" rel="noopener noreferrer"&gt;From sqlx&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Go SQL Ecosystem Deserves Better
&lt;/h2&gt;

&lt;p&gt;The fact that dynamic WHERE clauses — the most basic requirement of any API with filters — remain unsolved in Go's most popular SQL libraries is a failure of the ecosystem.&lt;/p&gt;

&lt;p&gt;sqlc is great but static. sqlx is abandoned. GORM trades transparency for convenience. squirrel is unmaintained and insecure.&lt;/p&gt;

&lt;p&gt;Relica exists because I needed the middle ground: &lt;strong&gt;query builder convenience with raw SQL transparency, zero dependencies, and an API that won't change when you upgrade&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you've been doing &lt;code&gt;WHERE 1=1&lt;/code&gt; — you can stop now.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/coregx/relica" rel="noopener noreferrer"&gt;github.com/coregx/relica&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How You Can Help
&lt;/h3&gt;

&lt;p&gt;Relica is open source and actively maintained. Here's how to get involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Try it&lt;/strong&gt; — replace one &lt;code&gt;WHERE 1=1&lt;/code&gt; query in your project and see if it clicks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report bugs&lt;/strong&gt; — if something doesn't work as expected, &lt;a href="https://github.com/coregx/relica/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;. We respond fast (0 open issues as of this writing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send PRs&lt;/strong&gt; — bug fixes, new dialect support, documentation improvements — all welcome&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Star the repo&lt;/strong&gt; — it helps other developers discover Relica when searching for Go query builders&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell others&lt;/strong&gt; — if Relica solved a real problem for you, share it with your team or community&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write about it&lt;/strong&gt; — blog posts, tutorials, comparisons — the more perspectives, the better the ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Go SQL ecosystem has been stuck between "too much magic" and "too much boilerplate" for too long. Let's fix that together.&lt;/p&gt;

</description>
      <category>go</category>
      <category>database</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>ThinkPad E14 Gen 7: no sound from speakers or mic on Windows 10? It's the Windows 11-only audio APOs</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Mon, 22 Jun 2026 16:36:59 +0000</pubDate>
      <link>https://dev.to/kolkov/thinkpad-e14-gen-7-no-sound-from-speakers-or-mic-on-windows-10-its-the-windows-11-only-audio-apos-1aje</link>
      <guid>https://dev.to/kolkov/thinkpad-e14-gen-7-no-sound-from-speakers-or-mic-on-windows-10-its-the-windows-11-only-audio-apos-1aje</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — On a laptop that ships Windows 11 but was downgraded to Windows 10, the built-in speakers can go completely silent while headphones and USB audio work perfectly. The cause is usually &lt;strong&gt;not&lt;/strong&gt; "the hardware is unsupported." On my ThinkPad E14 Gen 7 the audio driver registers its Dolby &lt;strong&gt;APO&lt;/strong&gt; (Audio Processing Object) COM classes only inside an INF section gated to Windows 11. On Windows 10 that section is skipped, so the audio engine tries to &lt;code&gt;CoCreateInstance&lt;/code&gt; an APO that was never registered, fails with &lt;code&gt;REGDB_E_CLASSNOTREG&lt;/code&gt;, and the &lt;strong&gt;entire speaker stream fails to build&lt;/strong&gt; — no sound, empty Volume Mixer, "Failed to play test tone." Registering the five missing classes by hand brings the speakers back (clean stereo). Everything below is reproducible with PowerShell, and every claim is backed by an HRESULT or an event-log entry rather than a guess. The same machine's &lt;strong&gt;built-in microphone&lt;/strong&gt; was dead too — a sibling APO bug with the &lt;em&gt;opposite&lt;/em&gt; fix; there's a companion section on it near the end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tested on
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lenovo ThinkPad E14 Gen 7 (Intel), machine type 21SX (21SXS0N500)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Windows 10 Pro 22H2 (build 19045)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audio stack&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Senary Audio" (Conexant/Synaptics CX) codec · Intel Smart Sound Technology (SST) · Dolby DAX3 APO · Elevoc APO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope of fix&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Speakers output in stereo; built-in mic captures (raw). &lt;em&gt;Dolby on speakers and Elevoc AI noise-suppression on the mic are not reachable on Win10 — see "What I couldn't fix" and the microphone section.&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Disclaimer.&lt;/strong&gt; This involves editing &lt;code&gt;HKLM&lt;/code&gt; and removing driver packages. It's unofficial and at your own risk. Create a &lt;strong&gt;System Restore point&lt;/strong&gt; first. A full rollback is included at the end. Nothing here touches firmware or hardware.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  30-second background: what an APO is
&lt;/h2&gt;

&lt;p&gt;Modern Windows audio runs effects — bass boost, Dolby/DTS, smart-amp speaker protection — as &lt;strong&gt;Audio Processing Objects (APOs)&lt;/strong&gt;: COM DLLs the audio engine loads into &lt;code&gt;audiodg.exe&lt;/code&gt; and inserts into a per-endpoint processing graph &lt;em&gt;before&lt;/em&gt; any audio reaches the device. If the engine can't build that graph for an endpoint, &lt;strong&gt;nothing&lt;/strong&gt; plays on it. Hold onto that: a broken APO doesn't just disable an effect, it can take down the whole endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speakers: nothing.&lt;/strong&gt; No sound at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Headphones (3.5 mm) and USB audio: perfect.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;With &lt;em&gt;Speakers&lt;/em&gt; set as the default device, the &lt;strong&gt;Volume Mixer shows zero applications&lt;/strong&gt; — not even &lt;em&gt;System Sounds&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The Windows &lt;strong&gt;"Test" tone fails&lt;/strong&gt; with &lt;em&gt;"Failed to play test tone."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Device Manager looks &lt;strong&gt;clean&lt;/strong&gt; — no yellow bangs; every audio device reports &lt;em&gt;This device is working properly&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is what makes it maddening: by every surface-level check, the audio stack is "fine."&lt;/p&gt;

&lt;h2&gt;
  
  
  The tempting (and wrong) conclusion
&lt;/h2&gt;

&lt;p&gt;This laptop officially ships &lt;strong&gt;Windows 11 only&lt;/strong&gt;. The obvious story writes itself: &lt;em&gt;"Lenovo doesn't validate Windows 10, the smart-amp driver doesn't exist for it, so the speakers physically can't work. Reinstall Windows 11."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That conclusion is &lt;strong&gt;unverified&lt;/strong&gt; — and it turned out to be wrong. "Officially unsupported" means the vendor doesn't test or ship a Win10 driver; it does &lt;strong&gt;not&lt;/strong&gt; mean the hardware can't make sound. And one clue flatly contradicts the pessimistic story: the endpoint enumerates as &lt;strong&gt;&lt;code&gt;Speakers (Senary Audio)&lt;/code&gt; in the ACTIVE state&lt;/strong&gt;, and the driver &lt;em&gt;did&lt;/em&gt; install. The path is alive. So let's diagnose instead of guessing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Everything from here to &lt;strong&gt;The fix&lt;/strong&gt; is read-only. Run PowerShell as your normal user for diagnostics.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1 — Confirm the hardware and driver are healthy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_SoundDevice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Manufacturer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ConfigManagerErrorCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Format-List&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every device reported &lt;code&gt;Status = OK&lt;/code&gt;, &lt;code&gt;ConfigManagerErrorCode = 0&lt;/code&gt; — the Senary codec and the Intel SST OED/BUS components. The services run, too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Dolby|Senary|Elevoc'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;StartMode&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# DolbyDAXAPI         Running  Auto&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# SenaryAudioApp.Svc  Running  Auto&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the files are installed and the services are running. The problem is &lt;strong&gt;higher up the stack&lt;/strong&gt; — in stream creation, not the driver binding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Catch the real failure with a WASAPI probe
&lt;/h2&gt;

&lt;p&gt;If the engine can't build an endpoint's APO graph, it can't even resolve the endpoint's shared mix format: &lt;code&gt;IAudioClient::GetMixFormat&lt;/code&gt; and &lt;code&gt;Initialize&lt;/code&gt; fail, so no app can open a session — which is &lt;em&gt;exactly&lt;/em&gt; why the mixer is empty. Let's confirm that directly (full self-contained probe is in the appendix). On the &lt;strong&gt;Speakers&lt;/strong&gt; endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Speakers GetMixFormat hr=0x80040154   # REGDB_E_CLASSNOTREG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0x80040154&lt;/code&gt; is &lt;strong&gt;&lt;code&gt;REGDB_E_CLASSNOTREG&lt;/code&gt; — "class not registered."&lt;/strong&gt; The engine is trying to &lt;code&gt;CoCreateInstance&lt;/code&gt; a COM object in the speaker's effect chain, and it isn't registered. For contrast, &lt;code&gt;IsFormatSupported&lt;/code&gt; in &lt;strong&gt;exclusive&lt;/strong&gt; mode returned &lt;code&gt;S_OK&lt;/code&gt; on the same endpoint — the hardware happily accepts a real PCM format. &lt;strong&gt;The wall is software, in the shared-mode effect graph.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Find which APO classes the endpoint uses
&lt;/h2&gt;

&lt;p&gt;The endpoint's effect chain lives in the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# 1) find your Speakers endpoint GUID:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ep&lt;/span&gt;&lt;span class="s2"&gt;\&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSChildName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;\Properties"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-EA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'{a45c254e-df1c-4efd-8020-67d146a850e0},2'&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PSCustomObject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GUID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PSChildName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# 2) dump its FxProperties (replace &amp;lt;GUID&amp;gt;):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ep&lt;/span&gt;&lt;span class="s2"&gt;\&amp;lt;GUID&amp;gt;\FxProperties"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Format-List&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;FxProperties&lt;/code&gt;, the property keyed &lt;code&gt;{d04e05a6-594b-4fb6-a80d-01af5eed7d1d}&lt;/code&gt; holds the &lt;strong&gt;APO CLSIDs&lt;/strong&gt; the engine instantiates. My speakers endpoint referenced four of the driver's Dolby DAX3 wrapper classes (SFX/EFX/OSFX/OMFX); the driver defines a fifth, MFX, that shows up on other endpoints. I registered all five to be safe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{0EBD8605-17BB-4AE7-AD76-E86F99A425E9}  (SFX  — stream effect)     &amp;lt;- on speakers
{0EBD8607-17BB-4AE7-AD76-E86F99A425E9}  (EFX  — endpoint effect)   &amp;lt;- on speakers
{0EBD8611-17BB-4AE7-AD76-E86F99A425E9}  (OSFX — offload stream effect) &amp;lt;- on speakers
{0EBD8612-17BB-4AE7-AD76-E86F99A425E9}  (OMFX — offload mode effect)   &amp;lt;- on speakers
{0EBD8606-17BB-4AE7-AD76-E86F99A425E9}  (MFX  — mode effect; driver-defined, registered for completeness)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ These GUIDs are specific to my driver version. &lt;strong&gt;Read your own&lt;/strong&gt; &lt;code&gt;FxProperties&lt;/code&gt;. If the layout differs, treat every CLSID-shaped value there as a candidate and test each one in Step 4 — the culprits are whichever aren't registered.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4 — Confirm those classes are not registered
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$apo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'0EBD8605-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'0EBD8606-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s1"&gt;'0EBD8607-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'0EBD8611-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s1"&gt;'0EBD8612-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$apo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\SOFTWARE\Classes\CLSID\{&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;}\InprocServer32"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-EA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'(default)'&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s1"&gt;'{0}  {1}'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'*** NOT REGISTERED ***'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five came back &lt;strong&gt;&lt;code&gt;NOT REGISTERED&lt;/code&gt;&lt;/strong&gt; — no &lt;code&gt;InprocServer32&lt;/code&gt;, so &lt;code&gt;CoCreateInstance&lt;/code&gt; can only fail. There's the &lt;code&gt;REGDB_E_CLASSNOTREG&lt;/code&gt;. Telling contrast: a &lt;em&gt;different&lt;/em&gt; APO on the same endpoint (Elevoc, CLSID &lt;code&gt;001897D5-…&lt;/code&gt;) &lt;strong&gt;was&lt;/strong&gt; registered and pointed at a real DLL. Only the Dolby classes were missing. Why?&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — The smoking gun in the INF
&lt;/h2&gt;

&lt;p&gt;The Dolby APO DLL is present on disk; Windows just never registered it. Find the driver INF and look at how it registers the classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;SystemRoot&lt;/span&gt;&lt;span class="s2"&gt;\System32\DriverStore\FileRepository\dax3_swc_aposvc.inf_amd64_*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Directory&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;\dax3_swc_aposvc.inf"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'AddReg|APO_AddReg|InProcServer32|DolbyDax3Apo'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decisive lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;AddReg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;APO_AddReg_Win11&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="nn"&gt;[APO_AddReg_Win11]&lt;/span&gt;
&lt;span class="err"&gt;HKR,Classes\CLSID\%FX_DolbyAPO_WrapperSFX_CLSID_V2%\InProcServer32,,%REG_EXPAND_SZ%,%13%\DolbyDax3Apo.dll&lt;/span&gt;
&lt;span class="err"&gt;HKR,Classes\CLSID\%FX_DolbyAPO_WrapperSFX_CLSID_V2%\InProcServer32,ThreadingModel,,"Both"&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The COM-registration block is literally named &lt;strong&gt;&lt;code&gt;APO_AddReg_Win11&lt;/code&gt;&lt;/strong&gt; and only applies on Windows 11. On Windows 10 it is skipped: the device installs, the services start, the DLL is copied — but the five Dolby APO classes are never registered. The Elevoc APO registered fine because &lt;em&gt;its&lt;/em&gt; INF isn't gated to Win11.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root cause, verified end to end
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codec / SST / services&lt;/td&gt;
&lt;td&gt;✅ healthy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint &lt;code&gt;Speakers (Senary Audio)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ ACTIVE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exclusive-mode format support&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;S_OK&lt;/code&gt; (hardware is fine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared-mode &lt;code&gt;GetMixFormat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;REGDB_E_CLASSNOTREG&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 Dolby APO CLSIDs in &lt;code&gt;HKCR\CLSID&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ &lt;strong&gt;not registered&lt;/strong&gt; (Win11-only INF section skipped)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Do what &lt;code&gt;APO_AddReg_Win11&lt;/code&gt; would have done: register the five CLSIDs against the Dolby APO DLL with &lt;code&gt;ThreadingModel = Both&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, get the real DLL path (the DriverStore folder hash is machine-specific):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;SystemRoot&lt;/span&gt;&lt;span class="s2"&gt;\System32\DriverStore\FileRepository\dax3_swc_aposvc.inf_amd64_*\DolbyDax3Apo.dll"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option A — registry file
&lt;/h3&gt;

&lt;p&gt;Save as &lt;code&gt;fix-dolby-apo.reg&lt;/code&gt; &lt;strong&gt;using your own CLSIDs and DLL path&lt;/strong&gt; (note the doubled backslashes). Write to &lt;code&gt;HKLM\SOFTWARE\Classes&lt;/code&gt;, &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;HKEY_CLASSES_ROOT&lt;/code&gt;: APOs load inside &lt;code&gt;audiodg.exe&lt;/code&gt; (a system service), so the registration must be machine-wide.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8605-17BB-4AE7-AD76-E86F99A425E9}\InprocServer32]
@="C:\\Windows\\System32\\DriverStore\\FileRepository\\dax3_swc_aposvc.inf_amd64_2d43ea2009b9f4a0\\DolbyDax3Apo.dll"
"ThreadingModel"="Both"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8606-17BB-4AE7-AD76-E86F99A425E9}\InprocServer32]
@="C:\\Windows\\System32\\DriverStore\\FileRepository\\dax3_swc_aposvc.inf_amd64_2d43ea2009b9f4a0\\DolbyDax3Apo.dll"
"ThreadingModel"="Both"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8607-17BB-4AE7-AD76-E86F99A425E9}\InprocServer32]
@="C:\\Windows\\System32\\DriverStore\\FileRepository\\dax3_swc_aposvc.inf_amd64_2d43ea2009b9f4a0\\DolbyDax3Apo.dll"
"ThreadingModel"="Both"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8611-17BB-4AE7-AD76-E86F99A425E9}\InprocServer32]
@="C:\\Windows\\System32\\DriverStore\\FileRepository\\dax3_swc_aposvc.inf_amd64_2d43ea2009b9f4a0\\DolbyDax3Apo.dll"
"ThreadingModel"="Both"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8612-17BB-4AE7-AD76-E86F99A425E9}\InprocServer32]
@="C:\\Windows\\System32\\DriverStore\\FileRepository\\dax3_swc_aposvc.inf_amd64_2d43ea2009b9f4a0\\DolbyDax3Apo.dll"
"ThreadingModel"="Both"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merge it &lt;strong&gt;elevated&lt;/strong&gt; (right-click → Merge → accept UAC), then restart the audio engine from an elevated prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Restart-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Audiosrv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option B — PowerShell (auto-detects the DLL path)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;#requires -RunAsAdministrator&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dll&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;SystemRoot&lt;/span&gt;&lt;span class="s2"&gt;\System32\DriverStore\FileRepository\dax3_swc_aposvc.inf_amd64_*\DolbyDax3Apo.dll"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$apo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'0EBD8605-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'0EBD8606-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s1"&gt;'0EBD8607-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'0EBD8611-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="s1"&gt;'0EBD8612-17BB-4AE7-AD76-E86F99A425E9'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$apo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\SOFTWARE\Classes\CLSID\{&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;}\InprocServer32"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'(default)'&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dll&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'ThreadingModel'&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Both'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Restart-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Audiosrv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;strong&gt;Settings → System → Sound → set "Speakers (Senary Audio)" as the output device&lt;/strong&gt; and play something. Still silent? Reboot once — the registration is picked up cleanly on a fresh start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify it worked
&lt;/h2&gt;

&lt;p&gt;Re-run the WASAPI probe; the failure is gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Speakers GetMixFormat   = 0x00000000   # S_OK  -&amp;gt; the stream graph builds
Speakers SHARED Initialize = 0x00000000 # the shared stream opens -&amp;gt; apps can play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Volume Mixer immediately starts listing applications again, and — the real test — &lt;strong&gt;the speakers play.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one setting to leave OFF — and why (with proof)
&lt;/h2&gt;

&lt;p&gt;Keep &lt;strong&gt;Speaker Properties → Advanced → Signal Enhancements → "Enable audio enhancements" unchecked.&lt;/strong&gt; With it off, the speakers play clean stereo. Turn it on and they go silent again until you turn it back off.&lt;/p&gt;

&lt;p&gt;I chased &lt;em&gt;why&lt;/em&gt;, because "it just doesn't work on Win10" is exactly the hand-wave this post argues against. Comparing the &lt;strong&gt;Speakers&lt;/strong&gt; and &lt;strong&gt;Headphones&lt;/strong&gt; endpoints under &lt;code&gt;…\MMDevices\Audio\Render\{guid}\FxProperties&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both endpoints reference the &lt;strong&gt;same&lt;/strong&gt; Dolby APO classes (now registered) and the same Elevoc APO.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;headphones&lt;/strong&gt; endpoint plays fine &lt;strong&gt;with enhancements on&lt;/strong&gt; — so the Dolby APO is &lt;em&gt;not&lt;/em&gt; fundamentally incompatible with Windows 10. It runs.&lt;/li&gt;
&lt;li&gt;The difference: the &lt;strong&gt;speakers&lt;/strong&gt; endpoint has extra &lt;code&gt;,100&lt;/code&gt; values binding each Dolby APO instance to the DAX3 APO &lt;strong&gt;service&lt;/strong&gt; device (&lt;code&gt;SWD\DRIVERENUM\{…}#DOLBYAPO_DAX3APOSVC&lt;/code&gt;). The headphones endpoint has no such binding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So enabling enhancements on the speakers activates the &lt;strong&gt;service-backed&lt;/strong&gt; Dolby processing (speaker tuning + smart-amp), and &lt;em&gt;that&lt;/em&gt; initialization is what fails. The proof is in the Application event log — the instant enhancements go on, &lt;strong&gt;&lt;code&gt;audiodg.exe&lt;/code&gt; crashes in a tight loop:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Faulting application name: AUDIODG.EXE
Faulting module name: KERNELBASE.dll
Exception code: 0xe06d7363        # SEH code for an unhandled C++ exception
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine loads the Dolby APO into &lt;code&gt;audiodg&lt;/code&gt;, the APO throws, &lt;code&gt;audiodg&lt;/code&gt; dies, Windows restarts it, it reloads the APO, it throws again — hence media apps reporting &lt;em&gt;"can't play audio, restart your PC."&lt;/em&gt; The headphones path never loads that service-bound APO, so it never crashes. (Watch it live: &lt;strong&gt;Event Viewer → Windows Logs → Application&lt;/strong&gt;, filter to &lt;em&gt;Application Error&lt;/em&gt;, while toggling the checkbox.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Because the vendor effect chain — which includes smart-amp speaker-protection processing — is bypassed in this configuration, don't run the speakers at maximum volume for extended periods.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I ruled out (so you don't have to)
&lt;/h2&gt;

&lt;p&gt;Enterprise debugging is as much about &lt;em&gt;eliminating&lt;/em&gt; hypotheses as confirming them. Things I tested and discarded, with evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"The APO metadata is missing."&lt;/strong&gt; The same Win11 INF section also writes APO metadata under &lt;code&gt;AudioEngine\AudioProcessingObjects&lt;/code&gt; (&lt;a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/audio/implementing-audio-processing-objects" rel="noopener noreferrer"&gt;Microsoft's APO docs&lt;/a&gt; confirm an &lt;code&gt;Apo_AddReg&lt;/code&gt; section registers in &lt;em&gt;two&lt;/em&gt; places). I replicated it exactly — &lt;code&gt;FriendlyName&lt;/code&gt;, &lt;code&gt;Flags=0xc&lt;/code&gt;, connection counts, &lt;code&gt;APOInterface0&lt;/code&gt; — for all five wrappers. Enhancements &lt;strong&gt;still&lt;/strong&gt; crashed &lt;code&gt;audiodg&lt;/code&gt;. Not the blocker (and the working Elevoc APO has no such metadata at all).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Wrong / mismatched driver."&lt;/strong&gt; I'd installed several driver packages while troubleshooting. I verified the &lt;em&gt;bound&lt;/em&gt; set is consistent: Intel SST &lt;code&gt;20.40.12690.2&lt;/code&gt; on every SST device, and &lt;code&gt;DolbyDax3Apo.dll&lt;/code&gt; + &lt;code&gt;DAX3API.exe&lt;/code&gt; both &lt;code&gt;3.30811.890.0&lt;/code&gt;. There was no version skew and no duplicate Dolby package in the DriverStore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale driver packages.&lt;/strong&gt; &lt;code&gt;pnputil /enum-drivers&lt;/code&gt; revealed three leftover Intel SST packages (including an ancient &lt;code&gt;10.23.0.3586&lt;/code&gt; from 2020) — all &lt;strong&gt;unbound&lt;/strong&gt;. I removed them with &lt;code&gt;pnputil /delete-driver oemNN.inf&lt;/code&gt; (no &lt;code&gt;/force&lt;/code&gt;, which refuses if a package is in use). Good hygiene; &lt;strong&gt;no effect on the crash.&lt;/strong&gt; Worth knowing it wasn't the cause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Net: the crash is an APO-level runtime incompatibility inside a closed-source Dolby DLL, not a registry, metadata, or driver-version gap. Pinning down the exact throw would need a crash dump with Dolby's private symbols, which won't change the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I couldn't fix
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Full Dolby speaker post-processing is not reachable on Windows 10 here.&lt;/strong&gt; The service-backed APO crashes &lt;code&gt;audiodg&lt;/code&gt;, and that's the genuine boundary the OEM's Windows 11 validation sits behind. The shipped, stable result: &lt;strong&gt;working speakers in clean stereo, enhancements off.&lt;/strong&gt; Honest beats optimistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback
&lt;/h2&gt;

&lt;p&gt;Remove the keys and restart audio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows Registry Editor Version 5.00

[-HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8605-17BB-4AE7-AD76-E86F99A425E9}]
[-HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8606-17BB-4AE7-AD76-E86F99A425E9}]
[-HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8607-17BB-4AE7-AD76-E86F99A425E9}]
[-HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8611-17BB-4AE7-AD76-E86F99A425E9}]
[-HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{0EBD8612-17BB-4AE7-AD76-E86F99A425E9}]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're never worse off than the original "no sound" state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it work on &lt;em&gt;your&lt;/em&gt; machine
&lt;/h2&gt;

&lt;p&gt;The exact GUIDs, DLL, and INF differ per vendor and driver version, but the method generalizes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Probe the &lt;strong&gt;Speakers&lt;/strong&gt; endpoint with WASAPI. If &lt;code&gt;GetMixFormat&lt;/code&gt; returns &lt;code&gt;0x80040154&lt;/code&gt;, you have this class of bug.&lt;/li&gt;
&lt;li&gt;Read the endpoint's &lt;code&gt;FxProperties&lt;/code&gt; → key &lt;code&gt;{d04e05a6-594b-4fb6-a80d-01af5eed7d1d}&lt;/code&gt; for the &lt;strong&gt;APO CLSIDs&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Check each under &lt;code&gt;HKLM:\SOFTWARE\Classes\CLSID\{…}\InprocServer32&lt;/code&gt;. Missing ones are your culprits.&lt;/li&gt;
&lt;li&gt;Find the owning driver in &lt;code&gt;DriverStore\FileRepository&lt;/code&gt; and &lt;code&gt;Select-String&lt;/code&gt; its INF for an &lt;code&gt;AddReg&lt;/code&gt; section that's OS-gated (look for &lt;code&gt;Win11&lt;/code&gt;, a build decoration, or an unapplied section). It names the &lt;strong&gt;DLL&lt;/strong&gt; and &lt;strong&gt;ThreadingModel&lt;/strong&gt; to register.&lt;/li&gt;
&lt;li&gt;Register the missing CLSIDs against that DLL and &lt;code&gt;Restart-Service Audiosrv&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern shows up on many "Windows 11-only" laptops force-installed with Windows 10, and occasionally after Windows Update swaps an audio driver. Realtek / Conexant / Synaptics codecs paired with Dolby / DTS / Waves / Nahimic APO stacks are all candidates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Companion case: the microphone — same crash, the opposite fix
&lt;/h2&gt;

&lt;p&gt;After the speakers, the built-in &lt;strong&gt;microphone&lt;/strong&gt; turned out to be dead too — and it's the same &lt;code&gt;audiodg&lt;/code&gt; crash wearing a different mask, with a fix that's the mirror image of the speaker one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; the internal mic array captures nothing; a 3.5 mm headset mic or a USB mic works fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Probe (capture side — &lt;code&gt;EnumAudioEndpoints&lt;/code&gt; with &lt;code&gt;dataFlow = eCapture&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microphone Array  GetMixFormat = 0x00000000      # format resolves fine...
Microphone Array  Initialize   = 0x800706BE      # ...but RPC_S_CALL_FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetMixFormat&lt;/code&gt; &lt;em&gt;succeeding&lt;/em&gt; — unlike the speakers' &lt;code&gt;REGDB_E_CLASSNOTREG&lt;/code&gt; — means the capture APO classes &lt;strong&gt;are&lt;/strong&gt; registered. The &lt;code&gt;0x800706BE&lt;/code&gt; (&lt;code&gt;RPC_S_CALL_FAILED&lt;/code&gt;) on &lt;code&gt;Initialize&lt;/code&gt; is the tell: the RPC channel to &lt;code&gt;audiodg&lt;/code&gt; dropped mid-call, because &lt;code&gt;audiodg&lt;/code&gt; &lt;strong&gt;crashed&lt;/strong&gt;. The Application log confirms the same &lt;code&gt;0xe06d7363&lt;/code&gt; C++ exception — this time thrown by the &lt;strong&gt;Elevoc&lt;/strong&gt; AI noise-suppression capture APO as it initializes on the mic stream. Same family as the speakers' "enhancements on" crash, opposite registration state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the speaker fix doesn't transfer.&lt;/strong&gt; The Dolby case was a &lt;em&gt;missing&lt;/em&gt; class — you add it. Here the Elevoc class is present and loads, then throws at runtime. Worse, every endpoint-level workaround failed to &lt;em&gt;stick&lt;/em&gt;: turning off "audio enhancements", deleting the APO from the endpoint's &lt;code&gt;FxProperties&lt;/code&gt;, even stopping the Elevoc service — the &lt;strong&gt;driver re-applies the effect chain on every audio restart&lt;/strong&gt;. And there's no Win10 driver to fall back to: Lenovo ships only Windows 11 audio drivers for this model, Windows Update offers no audio driver, and copying the correct per-codec config into &lt;code&gt;System32\ElevocConfig&lt;/code&gt; didn't stop the crash. (Elevoc &lt;em&gt;does&lt;/em&gt; build Win10 APOs — they ship on older models — but only as GNA-era builds for those platforms' codecs, with no models for this machine. So the APO isn't inherently Win10-hostile; there just isn't a build that fits this hardware.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: remove the layer.&lt;/strong&gt; Since the crash is the Elevoc APO itself and it's driver-enforced, take Elevoc out of the capture path at the driver level so the mic falls back to raw Intel SST capture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# elevated. Find the oemNN.inf publish names first:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pnputil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/enum-drivers&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c"&gt;# locate: elevocapo64.inf, elevocapo64ext.inf, intcdmicext_e.inf&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# then remove each (this uninstalls from the device and deletes the package):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pnputil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/delete-driver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;oemNN.inf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/uninstall&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Restart-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Audiosrv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c"&gt;# then reboot to finish&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-probe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microphone Array  GetMixFormat = 0x00000000
Microphone Array  Initialize   = 0x00000000   # stream opens; audiodg stable, 0 crashes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mic now works — &lt;strong&gt;raw&lt;/strong&gt;, without Elevoc's AI noise-suppression, which is a fair trade for a microphone that exists. Reversible: reinstalling the vendor "Audio Driver" bundle restores Elevoc (and the crash). If you need the AI processing, its only fully-supported home on this hardware is Windows 11.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The symmetry worth remembering:&lt;/strong&gt; one endpoint failed because a needed APO was &lt;em&gt;missing&lt;/em&gt; → &lt;strong&gt;register it&lt;/strong&gt;; the other failed because a present APO was &lt;em&gt;incompatible&lt;/em&gt; → &lt;strong&gt;remove it&lt;/strong&gt;. Same &lt;code&gt;audiodg&lt;/code&gt;, opposite remedies — and the WASAPI HRESULT told you which was which (&lt;code&gt;REGDB_E_CLASSNOTREG&lt;/code&gt; vs a crash-induced &lt;code&gt;RPC_S_CALL_FAILED&lt;/code&gt;) before you changed a thing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Officially unsupported" is a support policy, not a law of physics.&lt;/strong&gt; Verify before you conclude.&lt;/li&gt;
&lt;li&gt;A broken &lt;strong&gt;APO&lt;/strong&gt; can silence an entire endpoint — render &lt;em&gt;or&lt;/em&gt; capture — not just an effect, because the engine can't build the processing graph.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;HRESULT names the failure mode&lt;/strong&gt;: &lt;code&gt;REGDB_E_CLASSNOTREG&lt;/code&gt; = a needed APO isn't registered (&lt;strong&gt;add it&lt;/strong&gt;); a crash-induced &lt;code&gt;RPC_S_CALL_FAILED&lt;/code&gt; with &lt;code&gt;audiodg&lt;/code&gt; faulting &lt;code&gt;0xe06d7363&lt;/code&gt; = a present APO is incompatible and throws (&lt;strong&gt;remove it&lt;/strong&gt;). Opposite diagnoses, opposite fixes.&lt;/li&gt;
&lt;li&gt;Register APOs under &lt;strong&gt;&lt;code&gt;HKLM\SOFTWARE\Classes\CLSID&lt;/code&gt;&lt;/strong&gt; (machine-wide) — &lt;code&gt;audiodg.exe&lt;/code&gt; is a service and won't see per-user &lt;code&gt;HKCU&lt;/code&gt; classes.&lt;/li&gt;
&lt;li&gt;Endpoint-level edits (&lt;code&gt;FxProperties&lt;/code&gt;, "disable enhancements") &lt;strong&gt;don't stick&lt;/strong&gt; if the vendor driver re-applies the effect chain — for those, you act at the driver level (&lt;code&gt;pnputil /delete-driver&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Let the &lt;strong&gt;event log and HRESULTs&lt;/strong&gt; carry the argument. Every step here was a code, not a hunch — including the parts that say "this can't be fixed."&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Appendix: the WASAPI probe
&lt;/h2&gt;

&lt;p&gt;Self-contained; run as your normal user. It finds the &lt;code&gt;Speakers&lt;/code&gt; endpoint and reports &lt;code&gt;GetMixFormat&lt;/code&gt; (and, after the fix, the shared-mode &lt;code&gt;Initialize&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$cs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sh"&gt;@'
using System; using System.Runtime.InteropServices;
public static class P {
  [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class E {}
  [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  interface IE { [PreserveSig] int EnumAudioEndpoints(int f,int m,out IC c); [PreserveSig] int GetDefaultAudioEndpoint(int f,int r,out ID d); }
  [Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  interface IC { [PreserveSig] int GetCount(out int c); [PreserveSig] int Item(int i,out ID d); }
  [Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  interface ID { [PreserveSig] int Activate(ref Guid i,int x,IntPtr p,[MarshalAs(UnmanagedType.IUnknown)] out object o);
    [PreserveSig] int OpenPropertyStore(int a,out IP ps); [PreserveSig] int GetId([MarshalAs(UnmanagedType.LPWStr)] out string s); [PreserveSig] int GetState(out int s); }
  [Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  interface IP { [PreserveSig] int GetCount(out int c); [PreserveSig] int GetAt(int i,out PK k); [PreserveSig] int GetValue(ref PK k,out PV v); }
  [StructLayout(LayoutKind.Sequential)] struct PK { public Guid f; public int pid; }
  [StructLayout(LayoutKind.Explicit)] struct PV { [FieldOffset(0)] public short vt; [FieldOffset(8)] public IntPtr p; }
  [Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  interface IAC { [PreserveSig] int Initialize(int s,int f,long d,long p,IntPtr fmt,IntPtr g); [PreserveSig] int GetBufferSize(out uint n);
    [PreserveSig] int GetStreamLatency(out long l); [PreserveSig] int GetCurrentPadding(out uint n);
    [PreserveSig] int IsFormatSupported(int s,IntPtr f,out IntPtr c); [PreserveSig] int GetMixFormat(out IntPtr f); }
  static string Nm(ID d){ IP ps; if(d.OpenPropertyStore(0,out ps)!=0)return "?";
    var k=new PK{f=new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"),pid=14}; PV v;
    if(ps.GetValue(ref k,out v)!=0||v.p==IntPtr.Zero)return "?"; return Marshal.PtrToStringUni(v.p); }
  public static string Run(){ var en=(IE)(new E()); IC col; en.EnumAudioEndpoints(0,1,out col); int n; col.GetCount(out n);
    for(int i=0;i&amp;lt;n;i++){ ID d; col.Item(i,out d); var nm=Nm(d); if(nm!=null&amp;amp;&amp;amp;nm.StartsWith("Speakers")){
      var iid=new Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2"); object o; d.Activate(ref iid,1,IntPtr.Zero,out o);
      var c=(IAC)o; IntPtr f; int hr=c.GetMixFormat(out f); int sh=-1; if(hr==0) sh=c.Initialize(0,0,10000000,0,f,IntPtr.Zero);
      return "Speakers GetMixFormat=0x"+hr.ToString("X8")+"  SHARED Initialize=0x"+sh.ToString("X8"); } }
    return "No active Speakers endpoint found"; }
}
'@&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TypeDefinition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Language&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CSharp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To probe the &lt;strong&gt;microphone&lt;/strong&gt; instead, change &lt;code&gt;EnumAudioEndpoints(0, …)&lt;/code&gt; to &lt;code&gt;EnumAudioEndpoints(1, …)&lt;/code&gt; (&lt;code&gt;eCapture&lt;/code&gt;) and match &lt;code&gt;"Microphone"&lt;/code&gt; instead of &lt;code&gt;"Speakers"&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful, or hit a variant on a different machine? Drop your codec + APO stack and the &lt;code&gt;GetMixFormat&lt;/code&gt; HRESULT in the comments — the method in "Make it work on your machine" should map across vendors.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>windows</category>
      <category>powershell</category>
      <category>audio</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>gogpu/compose: Multi-Process GUI Composition in Pure Go</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Sun, 17 May 2026 10:07:38 +0000</pubDate>
      <link>https://dev.to/kolkov/gogpucompose-multi-process-gui-composition-in-pure-go-387i</link>
      <guid>https://dev.to/kolkov/gogpucompose-multi-process-gui-composition-in-pure-go-387i</guid>
      <description>&lt;p&gt;A Raspberry Pi displays a smart mirror. Clock in the top corner. Weather below. Notifications slide in from the bottom. Each panel is a separate process — if weather crashes, the clock keeps ticking.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;gogpu/compose&lt;/strong&gt;: a Pure Go library that lets independent processes render UI into offscreen buffers and ship pixels to a single compositor over Unix sockets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/gogpu/compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Desktop applications are monolithic. One process, one crash domain. A bug in the notification panel takes down the entire display.&lt;/p&gt;

&lt;p&gt;Wayland solved this at the OS level — every application is a separate client, and the compositor (KWin, Mutter, Sway) combines their surfaces. Android SurfaceFlinger does the same thing for mobile apps.&lt;/p&gt;

&lt;p&gt;But what if you need this pattern &lt;strong&gt;inside&lt;/strong&gt; your application? A dashboard where each data source is an independent process. A kiosk where third-party modules render into slots. A plugin host where untrusted code runs in its own crash domain.&lt;/p&gt;

&lt;p&gt;There was no Go library for this. You had to roll your own socket protocol, frame encoding, connection management, flow control. Every project reinvented the same plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;p&gt;compose is a two-sided library: a &lt;strong&gt;Server&lt;/strong&gt; (compositor) accepts connections, a &lt;strong&gt;Client&lt;/strong&gt; (module) publishes frames. One import, one socket path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Module side
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tmp/compose.sock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clock"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithFrameSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithFPS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnFrameRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetRGB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DrawString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"15:04:05"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PublishFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Pixels&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImageRGBA&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Height&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compositor side
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tmp/compose.sock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithMaxModules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCompression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"lz4"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pixels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"module connected: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDisconnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;uint64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"module gone: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The module renders into a pixel buffer, publishes it. The compositor receives frames, positions them on screen. Hot-plug is automatic — modules come and go without restarting the compositor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wire Protocol
&lt;/h2&gt;

&lt;p&gt;Every frame travels as a 64-byte header followed by pixel data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────┐
│ Magic "COMP" │ Version │ MsgType │ Flags                 │
│ ModuleID     │ Sequence              │ Timestamp         │
│ Width │ Height │ Stride  │ DirtyRect (x,y,w,h)           │
│ PixelFormat │ Compression │ PayloadSize │ Uncompressed   │
└──────────────────────────────────────────────────────────┘
64 bytes, little-endian, cache-line aligned
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header is fixed-size — one &lt;code&gt;io.ReadFull&lt;/code&gt; call parses it. No variable-length fields, no schema negotiation mid-stream. Encode and decode take &lt;strong&gt;under 10 ns and under 25 ns&lt;/strong&gt; respectively, with &lt;strong&gt;zero allocations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The protocol carries dirty rectangles (only the changed region's pixels), compression flags, and monotonic sequence numbers for frame ordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handshake
&lt;/h3&gt;

&lt;p&gt;When a module connects, it sends a 128-byte &lt;code&gt;HelloMsg&lt;/code&gt; (name, dimensions, preferred FPS). The compositor replies with a 128-byte &lt;code&gt;WelcomeMsg&lt;/code&gt; (assigned module ID, granted transport). Fixed-size messages — no JSON parsing, no protobuf dependency, no allocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  LZ4 Compression
&lt;/h2&gt;

&lt;p&gt;GUI pixels compress exceptionally well. Buttons, backgrounds, text labels — large flat-color regions that LZ4 handles at near-memcpy speed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frame Size&lt;/th&gt;
&lt;th&gt;Encode&lt;/th&gt;
&lt;th&gt;Decode&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;400×120 (small module)&lt;/td&gt;
&lt;td&gt;2,753 MB/s&lt;/td&gt;
&lt;td&gt;1,175 MB/s&lt;/td&gt;
&lt;td&gt;99.6% savings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1920×1080 (full HD)&lt;/td&gt;
&lt;td&gt;2,728 MB/s&lt;/td&gt;
&lt;td&gt;1,446 MB/s&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 192 KB module frame compresses to ~810 bytes. At 60 FPS, that's 47 KB/s over the socket — trivial for any transport.&lt;/p&gt;

&lt;p&gt;Compression is optional. Static modules (clock, weather) benefit most. Animated modules with random pixel patterns can disable it with a flag in the frame header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frame Delivery: Push, Pull, and Mailbox
&lt;/h2&gt;

&lt;p&gt;We researched how 9 production compositors handle frame delivery — Wayland, Android SurfaceFlinger, Chromium viz, Flutter, PipeWire, Windows DWM, macOS Core Animation, Vulkan, and X11. They all converge on the same pattern: &lt;strong&gt;producers submit whenever ready, the compositor samples the latest frame at its own cadence&lt;/strong&gt;. "Push vs pull" is a false dichotomy.&lt;/p&gt;

&lt;p&gt;compose supports both, with no mode negotiation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push&lt;/strong&gt; — module sends frames whenever data changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Stock ticker: push on every price update&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PublishFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pull&lt;/strong&gt; — module renders only when the compositor asks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Clock: render on demand (Wayland frame callback pattern)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnFrameRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PublishFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderClock&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mailbox&lt;/strong&gt; — the compositor samples latest frames on each render tick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Compositor render loop: always gets the latest frame from each module&lt;/span&gt;
&lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compositor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each module has a mailbox slot. When a module pushes faster than the compositor renders, intermediate frames are silently overwritten — latest always wins. This is the same pattern as Android's &lt;code&gt;acquireLatestBuffer()&lt;/code&gt; and Vulkan's &lt;code&gt;VK_PRESENT_MODE_MAILBOX_KHR&lt;/code&gt;. No stale frame accumulation, no FIFO backlog.&lt;/p&gt;

&lt;p&gt;The adaptive flow controller still works for pull-mode modules: after 3 consecutive missed responses, it halves the request rate. Push modules bypass it entirely — the mailbox handles everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: Enterprise internal/
&lt;/h2&gt;

&lt;p&gt;The public API has &lt;strong&gt;fewer than 15 exported declarations&lt;/strong&gt; — 5 types, 2 constructors, 5 option functions, and a handful of sentinel errors. Everything else is hidden behind &lt;code&gt;internal/&lt;/code&gt; packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;compose/                        # Public: Listen, Dial, Frame, options
├── internal/protocol/          # Wire format (100% coverage)
├── internal/codec/             # Raw + LZ4 (97% coverage)
├── internal/conn/              # Module lifecycle (98.9% coverage)
├── internal/flow/              # Pull-based pacing (100% coverage)
└── internal/transport/socket/  # Unix sockets (95.1% coverage)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dependency graph is a clean DAG — &lt;code&gt;protocol&lt;/code&gt; is the leaf (imported by all, imports nothing), &lt;code&gt;codec&lt;/code&gt;/&lt;code&gt;conn&lt;/code&gt;/&lt;code&gt;flow&lt;/code&gt; are independent, &lt;code&gt;transport/socket&lt;/code&gt; imports only &lt;code&gt;protocol&lt;/code&gt;. No cycles possible by construction.&lt;/p&gt;

&lt;p&gt;This isn't an accident. We designed compose the way &lt;code&gt;database/sql&lt;/code&gt; is designed: public &lt;code&gt;Server&lt;/code&gt;/&lt;code&gt;Client&lt;/code&gt; wrap internal implementations. Users never import sub-packages. Adding a shared memory transport (Phase 2) won't change the public API — it's a new &lt;code&gt;internal/transport/shm/&lt;/code&gt; behind an existing &lt;code&gt;WithSharedMemory()&lt;/code&gt; option.&lt;/p&gt;

&lt;h2&gt;
  
  
  By the Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source code&lt;/td&gt;
&lt;td&gt;2,960 LOC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test code&lt;/td&gt;
&lt;td&gt;6,066 LOC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;9,026 LOC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test cases&lt;/td&gt;
&lt;td&gt;179&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packages&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage (internal avg)&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header encode&lt;/td&gt;
&lt;td&gt;&amp;lt; 10 ns, 0 allocs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header decode&lt;/td&gt;
&lt;td&gt;&amp;lt; 25 ns, 0 allocs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LZ4 encode throughput&lt;/td&gt;
&lt;td&gt;2.7+ GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Socket throughput&lt;/td&gt;
&lt;td&gt;5.9+ GB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;1 (&lt;code&gt;pierrec/lz4/v4&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CGO&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;Ubuntu + macOS + Windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint (30+ rules)&lt;/td&gt;
&lt;td&gt;0 issues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;2× more test code than source code. Every internal package is independently testable with its own benchmark suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Needs This
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Smart mirrors and kiosks.&lt;/strong&gt; Independent modules (time, weather, transit, notifications) render into slots. A crash in one module doesn't affect the display.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modular dashboards.&lt;/strong&gt; Each data source is its own process, possibly developed by different teams. The compositor arranges them on screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin hosts.&lt;/strong&gt; Third-party plugins run in separate processes. The host application composites their output without trusting their code. &lt;a href="https://github.com/unxed/f4" rel="noopener noreferrer"&gt;f4&lt;/a&gt; by @unxed — a full Go reimplementation of Far Manager — already uses &lt;code&gt;--gui=gogpu&lt;/code&gt; for GPU-accelerated rendering and has an out-of-process plugin architecture (MessagePack RPC supporting Go, Python, Rust, Node.js, C++, Lua). compose could serve as the pixel transport between f4's plugin host and its GUI modules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-language UIs.&lt;/strong&gt; The wire protocol is language-agnostic. A Rust module or a Python script can participate — anything that writes RGBA to a Unix socket.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AgentNemo00/kigo" rel="noopener noreferrer"&gt;KiGo&lt;/a&gt; by @AgentNemo00 — a modular Go application — is already using offscreen rendering with multi-process composition. His feedback on v0.1.0 directly shaped the mailbox delivery model in v0.2.0: he told us he uses push, we researched 9 compositors, and shipped &lt;code&gt;Snapshot()&lt;/code&gt; two days later.&lt;/p&gt;

&lt;p&gt;These aren't hypothetical users waiting for a stable release. They're building real software on the GoGPU stack right now — and finding real bugs that make the ecosystem better. Every project that adopts GoGPU accelerates the path to enterprise grade. If you have a desktop app, a dashboard, a kiosk, a file manager, an IDE panel — there's never been a better time to try it in Pure Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Reference examples.&lt;/strong&gt; Three separate binaries — compositor, clock module, notification module — demonstrating the full multi-process workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Shared memory transport.&lt;/strong&gt; Triple-buffer ring in mmap'd memory with atomic slot states. Zero-copy pixel transfer for 60 FPS at 1080p. The public API stays the same — you just pass &lt;code&gt;WithSharedMemory()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Delta frames.&lt;/strong&gt; Send only the dirty rectangle's pixels, not the full frame. The protocol already carries dirty rects in the header — we just need the compositor-side texture cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/gogpu/compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/gogpu/compose" rel="noopener noreferrer"&gt;github.com/gogpu/compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; &lt;a href="https://github.com/gogpu/compose/blob/main/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;docs/ARCHITECTURE.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GoDoc:&lt;/strong&gt; &lt;a href="https://pkg.go.dev/github.com/gogpu/compose" rel="noopener noreferrer"&gt;pkg.go.dev/github.com/gogpu/compose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussion:&lt;/strong&gt; &lt;a href="https://github.com/orgs/gogpu/discussions/177" rel="noopener noreferrer"&gt;RFC #177&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part of the GoGPU Ecosystem
&lt;/h2&gt;

&lt;p&gt;compose is the newest member of &lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;GoGPU&lt;/a&gt; — 800K+ lines of Pure Go GPU computing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;wgpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pure Go WebGPU (Vulkan/Metal/DX12/GLES)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;naga&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shader compiler (WGSL → SPIR-V/MSL/GLSL/HLSL/DXIL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;gg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2D graphics with GPU acceleration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;ui&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Enterprise GUI toolkit (22+ widgets, 4 themes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;gogpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Application framework, windowing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/gogpu/compose" rel="noopener noreferrer"&gt;compose&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Multi-process composition (this library)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/systray" rel="noopener noreferrer"&gt;systray&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;System tray (Win32/macOS/Linux)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/audio" rel="noopener noreferrer"&gt;audio&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pure Go audio engine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Help Us Get to Enterprise Grade
&lt;/h2&gt;

&lt;p&gt;The ecosystem grows faster with every pair of eyes on it. Here's how you can help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test it.&lt;/strong&gt; Run &lt;code&gt;go get github.com/gogpu/compose&lt;/code&gt;, build a two-process prototype, tell us what breaks. Edge cases on your OS, your hardware, your use case — that's what we can't find ourselves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the API.&lt;/strong&gt; Does &lt;code&gt;Listen&lt;/code&gt;/&lt;code&gt;Dial&lt;/code&gt;/&lt;code&gt;PublishFrame&lt;/code&gt; feel right? Is the wire protocol missing a field you need? The API is still v0.x — now is the time to reshape it, before it freezes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose and discuss.&lt;/strong&gt; Open an &lt;a href="https://github.com/gogpu/compose/issues" rel="noopener noreferrer"&gt;issue&lt;/a&gt; or join the &lt;a href="https://github.com/orgs/gogpu/discussions/177" rel="noopener noreferrer"&gt;compose RFC discussion&lt;/a&gt;. Real use cases drive the roadmap — not hypotheticals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spread the word.&lt;/strong&gt; Star the repo, share this article, mention it in your Go meetup. The more developers use the ecosystem, the faster it reaches enterprise grade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute.&lt;/strong&gt; Every PR matters — from typo fixes to new transport implementations. See &lt;a href="https://github.com/gogpu/compose/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire GoGPU ecosystem — 800K+ lines of Pure Go — is built by a small team. But the libraries are production-ready enough that people are building real products on them: a &lt;a href="https://github.com/unxed/f4" rel="noopener noreferrer"&gt;Far Manager rewrite&lt;/a&gt; with GPU rendering, an &lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;ML framework&lt;/a&gt; fully migrated to our WebGPU stack, a &lt;a href="https://github.com/darkliquid/ironwail-go" rel="noopener noreferrer"&gt;Quake 1 engine port&lt;/a&gt; running on gogpu/wgpu Vulkan (&lt;a href="https://github.com/gogpu/gogpu/issues/163" rel="noopener noreferrer"&gt;demo&lt;/a&gt;), modular app platforms. The more developers who start building on GoGPU — whether it's a new project or porting an existing one — the faster the entire ecosystem matures. Your project's edge cases become our test suite. Your feature requests become our roadmap.&lt;/p&gt;

&lt;p&gt;Start building. We'll make sure the foundation holds.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building what Go "can't do." One library at a time.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>gogpu/ui v0.1.21: Enterprise Render Pipeline — Layer Tree, Damage Tracking, 0% GPU Idle</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Tue, 12 May 2026 10:23:08 +0000</pubDate>
      <link>https://dev.to/kolkov/gogpuui-v0121-enterprise-render-pipeline-layer-tree-damage-tracking-0-gpu-idle-5adm</link>
      <guid>https://dev.to/kolkov/gogpuui-v0121-enterprise-render-pipeline-layer-tree-damage-tracking-0-gpu-idle-5adm</guid>
      <description>&lt;p&gt;Two months ago we released &lt;a href="https://dev.to/kolkov/go-gui-in-2026-gogpuui-v010-22-widgets-gpu-rendering-zero-cgo-1enf"&gt;gogpu/ui v0.1.0&lt;/a&gt; — 22 widgets, 3 design systems, ~150K lines of pure Go. Since then we shipped 21 patch releases, and the rendering pipeline is unrecognizable.&lt;/p&gt;

&lt;p&gt;This post covers what changed and why it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;v0.1.0 re-rendered the entire widget tree every frame. A 48×48 spinner in one corner caused the GPU to redraw 800×600 of static content. Hover over a button? Full tree walk. Open a dropdown? Full tree walk. This was fine for demos, not for production.&lt;/p&gt;

&lt;p&gt;We studied how five frameworks solve this — Flutter, Chrome, Qt6, Android HWUI, Skia — and found the same architecture everywhere: &lt;strong&gt;Layer Tree + boundary isolation + damage tracking&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built (v0.1.14 → v0.1.21)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer Tree Compositor
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;RepaintBoundary&lt;/code&gt; widget now owns a node in a persistent Layer Tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OffsetLayer (root)
├── PictureLayer (toolbar — clean, reuse texture)
├── PictureLayer (sidebar — clean, reuse texture)
├── ClipRectLayer (scrollview viewport)
│   └── PictureLayer (content — dirty, re-record)
└── PictureLayer (spinner — dirty, re-record 48×48)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four layer types — &lt;code&gt;OffsetLayer&lt;/code&gt;, &lt;code&gt;PictureLayer&lt;/code&gt;, &lt;code&gt;ClipRectLayer&lt;/code&gt;, &lt;code&gt;OpacityLayer&lt;/code&gt; — compose the frame. Clean layers reuse their GPU texture from the previous frame. Only dirty layers re-render.&lt;/p&gt;

&lt;p&gt;This is the same pattern Flutter calls &lt;code&gt;flushPaint&lt;/code&gt; + &lt;code&gt;compositeFrame&lt;/code&gt;. We validated it against all five reference frameworks before writing a line of code.&lt;/p&gt;

&lt;h3&gt;
  
  
  0% GPU When Idle
&lt;/h3&gt;

&lt;p&gt;The frame loop checks a flat dirty set — O(1), not O(n) tree walk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasDirtyBoundaries&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NeedsRedraw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NeedsAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c"&gt;// nothing changed, skip frame entirely&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the UI is idle, the GPU does zero work. Measured: 0% GPU across all six examples (hello, signals, taskmanager, gallery, ide, modular-compositor).&lt;/p&gt;

&lt;p&gt;Previous approach walked the entire widget tree every frame to check if anything needed redraw. For 200 boundaries, the new approach is &lt;strong&gt;45× faster&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Boundary GPU Textures
&lt;/h3&gt;

&lt;p&gt;Each RepaintBoundary renders into its own offscreen MSAA texture. When a child boundary becomes dirty, only that boundary's texture is re-rendered. The compositor blits all textures in a single non-MSAA pass.&lt;/p&gt;

&lt;p&gt;A 48×48 spinner touching 2,304 pixels no longer forces the GPU to process 480,000 pixels of unchanged content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Rect Damage
&lt;/h3&gt;

&lt;p&gt;When multiple widgets are dirty in different screen regions, we don't union them into one giant rect. Each dirty rect gets its own GPU scissor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frame N: spinner (48×48) + status bar (800×24)
→ Two scissor rects, not one 800×600 rect
→ Zero pixel waste
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The damage pipeline flows through the full stack: &lt;code&gt;ui&lt;/code&gt; → &lt;code&gt;gg&lt;/code&gt; &lt;code&gt;RenderDirectWithDamageRects&lt;/code&gt; → &lt;code&gt;wgpu&lt;/code&gt; &lt;code&gt;PresentWithDamage&lt;/code&gt;. Ring buffer stores rect lists for N-buffer swapchains. Threshold at 16 rects merges to union (GDK/Sway pattern).&lt;/p&gt;

&lt;h3&gt;
  
  
  Persistent Layer Tree
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;UpdateLayerTree()&lt;/code&gt; reuses layer objects across frames instead of rebuilding the tree:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Allocs per frame (200 boundaries)&lt;/td&gt;
&lt;td&gt;613&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reduction&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;97.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Flutter calls this &lt;code&gt;addRetained&lt;/code&gt;. Android calls it &lt;code&gt;RenderNode&lt;/code&gt; reuse. We measured allocation profiles against both and matched their patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;v0.1.0&lt;/th&gt;
&lt;th&gt;v0.1.21&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lines (total / code)&lt;/td&gt;
&lt;td&gt;150K / 105K&lt;/td&gt;
&lt;td&gt;195K / 141K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;6,000&lt;/td&gt;
&lt;td&gt;7,200+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;97%&lt;/td&gt;
&lt;td&gt;97%+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packages&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU idle (static UI)&lt;/td&gt;
&lt;td&gt;5-18%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frame skip check&lt;/td&gt;
&lt;td&gt;O(n) tree walk&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;O(1)&lt;/strong&gt; flat set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocs/frame (200 boundaries)&lt;/td&gt;
&lt;td&gt;613&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spinner GPU work&lt;/td&gt;
&lt;td&gt;full window&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48×48 scissor&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Ecosystem Update
&lt;/h2&gt;

&lt;p&gt;The rendering pipeline required changes across four repositories. Here's where the ecosystem stands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;naga&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v0.17.13&lt;/td&gt;
&lt;td&gt;323K&lt;/td&gt;
&lt;td&gt;240K&lt;/td&gt;
&lt;td&gt;Shader compiler: WGSL → SPIR-V, MSL, GLSL, HLSL, DXIL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;gg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v0.46.8&lt;/td&gt;
&lt;td&gt;240K&lt;/td&gt;
&lt;td&gt;171K&lt;/td&gt;
&lt;td&gt;2D graphics: Skia-class rasterizer, GPU SDF, scene compositor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;wgpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v0.27.3&lt;/td&gt;
&lt;td&gt;211K&lt;/td&gt;
&lt;td&gt;164K&lt;/td&gt;
&lt;td&gt;Pure Go WebGPU: Vulkan, DX12, Metal, GLES, Software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;ui&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v0.1.21&lt;/td&gt;
&lt;td&gt;195K&lt;/td&gt;
&lt;td&gt;141K&lt;/td&gt;
&lt;td&gt;GUI toolkit: 22 widgets, 4 themes, Layer Tree pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;gogpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;v0.34.3&lt;/td&gt;
&lt;td&gt;61K&lt;/td&gt;
&lt;td&gt;45K&lt;/td&gt;
&lt;td&gt;App framework: windowing, input, three-mode render loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ gpucontext, gputypes, systray, audio&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;19K&lt;/td&gt;
&lt;td&gt;13K&lt;/td&gt;
&lt;td&gt;Shared interfaces, system tray, audio engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,049K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;774K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,140 files across 9 repositories&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;1M+ total lines. 774K lines of code. Zero CGO. Zero Rust. Zero C.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recent ecosystem highlights since the v0.1.0 article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First Pure Go DXIL generator&lt;/strong&gt; — naga compiles WGSL shaders directly to DXIL bytecode, eliminating the HLSL→FXC/DXC dependency. 161/170 IDxcValidator pass rate. &lt;a href="https://dev.to/kolkov/we-built-the-first-pure-go-dxil-generator-because-optimizing-the-wrong-path-wasnt-enough-35en"&gt;Article&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Born ML v0.8.0&lt;/strong&gt; migrated to gogpu/wgpu — production ML framework running on our GPU stack. 105 GPU tests pass, HRM model trained 20 epochs. &lt;a href="https://dev.to/kolkov/born-ml-v080-we-killed-our-last-dll-pure-go-gpu-is-here-2dd7"&gt;Article&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CJK text rendering&lt;/strong&gt; — script-aware hinting, exact-size rasterization, Tier 6 routing for Chinese/Japanese/Korean glyphs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LCD ClearType auto-detection&lt;/strong&gt; — Windows SPI + registry, macOS None, Linux Xft/Wayland. Per-platform subpixel layout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software backend for CI&lt;/strong&gt; — deterministic GPU without GPU hardware. Pixel-exact e2e tests prove scissor rects at HAL level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community deep-dive&lt;/strong&gt; — independent &lt;a href="https://chenxutan.com/d/1987.html" rel="noopener noreferrer"&gt;technical analysis of gogpu/wgpu&lt;/a&gt; (Chinese) covering the zero-CGO syscall architecture, Snatchable resource lifecycle, and buffer state tracking internals. Always good to see the community dig into the implementation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Foundation Is Ready
&lt;/h2&gt;

&lt;p&gt;This is the release where we stopped rebuilding and started building on top.&lt;/p&gt;

&lt;p&gt;For the past two months every release was infrastructure: retained-mode rendering, scene composition, Layer Tree, damage tracking, boundary isolation. The kind of plumbing that's invisible to users but determines whether a framework can scale to real applications.&lt;/p&gt;

&lt;p&gt;That plumbing is now in place. The render pipeline follows the same architectural patterns as Flutter, Chrome, and Qt6 — not because we copied them, but because we studied all five independently and arrived at the same conclusions. Layer Tree composition, per-boundary GPU textures, multi-rect damage, persistent allocation — these are industry-proven patterns, and they're production-ready in gogpu/ui.&lt;/p&gt;

&lt;p&gt;The ecosystem has stabilized around this architecture. naga (shader compiler), wgpu (WebGPU HAL), gg (2D graphics), and gogpu (windowing) all reached the point where API churn is minimal and releases are incremental improvements, not rewrites. Nine repositories, 1M+ lines, and the dependency chain holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means going forward:&lt;/strong&gt; the pipeline will be optimized, not rebuilt. Future releases will focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New widgets&lt;/strong&gt; — the 22 we ship today cover most use cases, but enterprise apps need more (color picker, date picker, rich text editor, tree grid)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance polish&lt;/strong&gt; — reducing GPU usage for animated widgets from 10% to &amp;lt;3%, ListView recycling, texture GC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform accessibility&lt;/strong&gt; — UIA on Windows, AT-SPI2 on Linux, NSAccessibility on macOS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer experience&lt;/strong&gt; — better docs, more examples, smoother onboarding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hard part is behind us. The interesting part is ahead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/gogpu/ui.git
&lt;span class="nb"&gt;cd &lt;/span&gt;ui/examples/gallery
go run &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four design systems ship out of the box: Material Design 3, JetBrains DevTools, Microsoft Fluent, Apple Cupertino. Switch between them at runtime in the gallery example.&lt;/p&gt;

&lt;p&gt;Backend selection via environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vulkan   go run ./examples/ide/
&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dx12     go run ./examples/ide/
&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gles     go run ./examples/ide/
&lt;span class="nv"&gt;GOGPU_GRAPHICS_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;software go run ./examples/ide/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No code changes needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help Us Get There
&lt;/h2&gt;

&lt;p&gt;gogpu/ui is at the stage where the architecture is proven but the user base is small. We need real-world testing to catch edge cases that no amount of 97% coverage will find.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test it.&lt;/strong&gt; Clone a repo, run an example, try building something with it. If it breaks — that's valuable. &lt;a href="https://github.com/gogpu/ui/issues" rel="noopener noreferrer"&gt;File an issue&lt;/a&gt;, and we'll fix it. If it works — that's valuable too. Tell us what you built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spread the word.&lt;/strong&gt; Most Go developers don't know this exists yet. A post on Reddit, a tweet, a mention in your team's Slack — it all helps. The project grows through people who try it and talk about it, not through marketing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write about it.&lt;/strong&gt; Tutorials, experience reports, comparisons, critiques — all welcome. If you build something interesting with gogpu/ui, write about the process. The ecosystem needs content from people other than us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contribute.&lt;/strong&gt; You don't need to touch the render pipeline. Documentation improvements, new examples, widget ideas, accessibility testing, CI on different hardware — there's work at every level. Check &lt;a href="https://github.com/gogpu/ui/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; or just open a discussion.&lt;/p&gt;

&lt;p&gt;The codebase is 1M+ lines of pure Go with zero CGO. The foundation is solid. What it needs now is people building on it.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://github.com/orgs/gogpu/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; · &lt;a href="https://github.com/gogpu/ui/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt; · &lt;a href="https://www.reddit.com/r/golang/" rel="noopener noreferrer"&gt;Reddit r/golang&lt;/a&gt; · &lt;a href="https://x.com" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>gui</category>
      <category>performance</category>
    </item>
    <item>
      <title>Born ML v0.8.0: We Killed Our Last .dll — Pure Go GPU Is Here</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Sun, 03 May 2026 12:41:30 +0000</pubDate>
      <link>https://dev.to/kolkov/born-ml-v080-we-killed-our-last-dll-pure-go-gpu-is-here-4mek</link>
      <guid>https://dev.to/kolkov/born-ml-v080-we-killed-our-last-dll-pure-go-gpu-is-here-4mek</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Born v0.8.0 replaces go-webgpu (Rust FFI + shared libraries) with gogpu/wgpu — pure Go WebGPU. No &lt;code&gt;.dll&lt;/code&gt;. No &lt;code&gt;.so&lt;/code&gt;. No runtime downloads. &lt;code&gt;go build&lt;/code&gt; now gives you a GPU-accelerated ML binary. We also fixed 5 critical GPU bugs and validated on real model training. Next up: DeepSeek V4 inference support.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Last Dependency
&lt;/h2&gt;

&lt;p&gt;Five months ago I &lt;a href="https://dev.to/kolkov/i-skipped-my-birthday-to-give-go-its-first-real-ml-framework-13gj"&gt;skipped my birthday&lt;/a&gt; to release Born. A few weeks later we &lt;a href="https://dev.to/kolkov/born-ml-v060-from-90-seconds-to-5-how-we-made-go-ml-training-actually-fast-19f8"&gt;made training 18x faster&lt;/a&gt; with lazy GPU evaluation. The framework was growing. Contributors were showing up. Real people were using it.&lt;/p&gt;

&lt;p&gt;But there was a problem I couldn't ignore anymore.&lt;/p&gt;

&lt;p&gt;Every time someone wanted to use GPU acceleration, the conversation went like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I run the GPU examples?"&lt;/p&gt;

&lt;p&gt;"Download wgpu-native &lt;code&gt;.dll&lt;/code&gt; for your platform, put it in your PATH..."&lt;/p&gt;

&lt;p&gt;"...I thought you said pure Go?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They were right. Born's CPU path was pure Go. But the GPU backend used &lt;a href="https://github.com/go-webgpu/webgpu" rel="noopener noreferrer"&gt;go-webgpu&lt;/a&gt; — Go bindings to Rust's wgpu-native via FFI. You needed a platform-specific shared library at runtime. On Windows, a &lt;code&gt;.dll&lt;/code&gt;. On Linux, a &lt;code&gt;.so&lt;/code&gt;. On macOS, a &lt;code&gt;.dylib&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a framework whose tagline is &lt;em&gt;"single binary deployment"&lt;/em&gt;, that was embarrassing.&lt;/p&gt;

&lt;p&gt;So we fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Earlier?
&lt;/h2&gt;

&lt;p&gt;Fair question. gogpu/wgpu existed for months before v0.8.0. Why did we ship 29 releases on go-webgpu first?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because that was the plan.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;go-webgpu wraps Rust's wgpu-native — a battle-tested GPU abstraction used by Firefox and dozens of production projects. When you're building a new ML framework from scratch, you don't want to debug your GPU backend and your tensor math at the same time. If training produces wrong gradients, is the bug in your autodiff engine or in your WebGPU implementation? With Rust wgpu-native underneath, we knew: the GPU layer works. Any bug is ours.&lt;/p&gt;

&lt;p&gt;So we built Born v0.1 through v0.7 on a proven foundation. Tensor ops, autodiff, attention, Flash Attention, speculative decoding, ONNX import, GGUF loading — all validated against a GPU backend we could trust. By v0.7.16, Born had 1,394 tests, 3 external contributors, and real model training working.&lt;/p&gt;

&lt;p&gt;Meanwhile, gogpu/wgpu was maturing through its own path — powering &lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;gogpu/gg&lt;/a&gt; (2D graphics library with GPU compute shaders), running real rendering workloads, stabilizing the Core API across Vulkan, Metal, DX12, and GLES.&lt;/p&gt;

&lt;p&gt;When both sides were proven, the migration became simple: we knew Born's code was correct, and we knew gogpu/wgpu's Core API was stable. Any bug found during migration was specifically a wgpu Go integration issue — easy to isolate, easy to fix.&lt;/p&gt;

&lt;p&gt;That's exactly what happened. Five bugs, all in resource lifecycle. All fixed in days, not weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate on proven foundation first. Swap the foundation second.&lt;/strong&gt; This is not how you move fast. This is how you move &lt;em&gt;right&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Migration
&lt;/h2&gt;

&lt;p&gt;Born v0.8.0 replaces &lt;code&gt;go-webgpu&lt;/code&gt; with &lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;gogpu/wgpu&lt;/a&gt; — a pure Go WebGPU implementation from our own &lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;GoGPU&lt;/a&gt; ecosystem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- github.com/go-webgpu/webgpu v0.4.1
&lt;/span&gt;&lt;span class="gi"&gt;+ github.com/gogpu/wgpu v0.26.8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line in &lt;code&gt;go.mod&lt;/code&gt;. 27 files changed. 1,830 additions, 1,518 deletions.&lt;/p&gt;

&lt;p&gt;What changed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;go-webgpu (before)&lt;/th&gt;
&lt;th&gt;gogpu/wgpu (after)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rust wgpu-native via FFI&lt;/td&gt;
&lt;td&gt;Pure Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CGO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (goffi)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime .dll/.so&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go build&lt;/code&gt; + download .dll&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;go build&lt;/code&gt;. Period.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulkan/Metal/DX12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Via Rust&lt;/td&gt;
&lt;td&gt;Via Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WGSL shaders&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unchanged&lt;/td&gt;
&lt;td&gt;Unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;External project&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Our project&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row matters. gogpu/wgpu isn't some random dependency — it's &lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;our project&lt;/a&gt;. When Born needs a WebGPU API change, we change it upstream. Both sides of the interface are under our control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five Bugs Nobody Told Us About
&lt;/h2&gt;

&lt;p&gt;Swapping the GPU backend is like replacing a car engine while driving. Everything looks the same from the outside, but internally the timing, resource lifecycle, and synchronization are completely different.&lt;/p&gt;

&lt;p&gt;We found five critical bugs during migration:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. PipelineLayout Freed Too Early
&lt;/h3&gt;

&lt;p&gt;Vulkan requires compute pipeline layouts to stay alive during &lt;code&gt;SetBindGroup()&lt;/code&gt;. go-webgpu's internal reference counting kept them alive. gogpu/wgpu doesn't — you own your resources.&lt;/p&gt;

&lt;p&gt;We fixed this by storing &lt;code&gt;PipelineLayout&lt;/code&gt; alongside the pipeline in our cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Lazy Ops and the Destroy Queue
&lt;/h3&gt;

&lt;p&gt;Born uses lazy evaluation — GPU ops chain without CPU sync. But when a tensor gets garbage-collected mid-chain, its buffer goes to the destroy queue. If the pending operations haven't submitted yet, the buffer is destroyed before the GPU reads it.&lt;/p&gt;

&lt;p&gt;Fix: immediate submit for lazy ops. Every operation submits its command encoder before returning.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Buffer Copy Race
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;copyGPUBuffer&lt;/code&gt; (used by &lt;code&gt;Data()&lt;/code&gt; to read results back to CPU) was queuing the copy but not submitting. The next operation might overwrite the source buffer before the copy executed.&lt;/p&gt;

&lt;p&gt;Fix: immediate submit after copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. GC vs GPU
&lt;/h3&gt;

&lt;p&gt;Go's garbage collector doesn't know about GPU resources. A &lt;code&gt;runtime.SetFinalizer&lt;/code&gt; on a tensor could fire while the GPU was still computing with that tensor's buffer.&lt;/p&gt;

&lt;p&gt;Fix: &lt;code&gt;runtime.KeepAlive()&lt;/code&gt; guards around every GPU operation that uses the tensor.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Device Cleanup Order
&lt;/h3&gt;

&lt;p&gt;When destroying the GPU device, all pending work must complete first. Without &lt;code&gt;Poll(PollWait)&lt;/code&gt; before resource destruction, Vulkan validation layers scream.&lt;/p&gt;

&lt;p&gt;Fix: explicit &lt;code&gt;Poll(PollWait)&lt;/code&gt; in &lt;code&gt;Release()&lt;/code&gt; to ensure GPU idle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;None of these bugs existed with go-webgpu.&lt;/strong&gt; They're all about resource lifecycle differences between Rust's ownership model (where wgpu-native tracks everything for you) and Go's GC-based model (where you track it yourself).&lt;/p&gt;

&lt;p&gt;After fixing all five, we ran all GPU tests and a 20-epoch model training with zero crashes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;h3&gt;
  
  
  True Single Binary
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go build &lt;span class="nt"&gt;-o&lt;/span&gt; myapp ./cmd/myapp
&lt;span class="c"&gt;# That's it. Ship the binary. GPU works.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;.dll&lt;/code&gt; downloads. No &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt;. No platform-specific install steps. The binary works on any machine with a Vulkan-capable GPU.&lt;/p&gt;

&lt;h3&gt;
  
  
  Same API, Same Shaders
&lt;/h3&gt;

&lt;p&gt;If you have existing Born code with GPU, &lt;strong&gt;nothing changes&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/born-ml/born/backend/cpu"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/born-ml/born/autodiff"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// CPU-only (always worked)&lt;/span&gt;
&lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;autodiff&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/born-ml/born/backend/webgpu"&lt;/span&gt;

&lt;span class="c"&gt;// GPU-accelerated (now pure Go!)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;webgpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAvailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;webgpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;autodiff&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;gpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WGSL shaders are unchanged. The Backend interface (52 methods) is unchanged. Your code just works — minus the &lt;code&gt;.dll&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validated on Real Training
&lt;/h3&gt;

&lt;p&gt;We didn't just run unit tests. We trained a real Hierarchical Reasoning Model (HRM) for 20 epochs on GPU. Zero crashes. Correct gradients. Same accuracy as go-webgpu.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~47K LOC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~34K LOC, 1,394 test functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ONNX operators&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend methods&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPU tests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Contributors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4 (&lt;a href="https://github.com/kolkov" rel="noopener noreferrer"&gt;@kolkov&lt;/a&gt;, &lt;a href="https://github.com/gmohmad" rel="noopener noreferrer"&gt;@gmohmad&lt;/a&gt;, &lt;a href="https://github.com/bennibbelink" rel="noopener noreferrer"&gt;@bennibbelink&lt;/a&gt;, &lt;a href="https://github.com/jsully1720" rel="noopener noreferrer"&gt;@jsully1720&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Releases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stars&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80 (organic, no marketing)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Community
&lt;/h2&gt;

&lt;p&gt;v0.8.0 isn't just about the migration. Since v0.7.0, three external contributors have landed real code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jsully1720" rel="noopener noreferrer"&gt;@jsully1720&lt;/a&gt;&lt;/strong&gt; — ONNX Equal operator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/bennibbelink" rel="noopener noreferrer"&gt;@bennibbelink&lt;/a&gt;&lt;/strong&gt; — Erf, Sign/Abs, Clamp ops (3 PRs, all full vertical slices: backend → CPU → GPU → autodiff → tests)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/gmohmad" rel="noopener noreferrer"&gt;@gmohmad&lt;/a&gt;&lt;/strong&gt; — LayerNorm, BatchMatMul broadcasting, Squeeze fix, 9 new ONNX ops, inplace mutation bug fix (5 PRs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't drive-by typo fixes. These are production-quality contributions from people who studied the codebase and followed the patterns. If you're considering contributing, look at what they did — that's the bar.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next: DeepSeek V4 Inference
&lt;/h2&gt;

&lt;p&gt;With the GPU backend stable and pure Go, we can focus on what matters: running real models.&lt;/p&gt;

&lt;p&gt;DeepSeek released &lt;a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro" rel="noopener noreferrer"&gt;V4&lt;/a&gt; on April 24, 2026 — two models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;V4-Pro&lt;/strong&gt;: 1.6 trillion params, 49B active&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;V4-Flash&lt;/strong&gt;: 284B total, 13B active — &lt;strong&gt;fits on a consumer GPU&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;V4-Flash with 13B active parameters is Born's sweet spot. It's the most capable open model that fits on a single 24GB GPU. API pricing is tied to chip availability ($1.74/M tokens, bottleneck pricing) — users want local inference alternatives.&lt;/p&gt;

&lt;p&gt;We started researching V4 architecture &lt;strong&gt;before&lt;/strong&gt; it launched — back in early April, when only the Engram paper and V3.2 sparse attention existed. We predicted V4 would combine MoE + Engram + manifold-constrained residuals + compressed sparse attention. On April 24th, the tech report confirmed all four. Two weeks head start on architecture analysis. (We do this kind of research openly — see &lt;a href="https://github.com/born-ml/born/discussions/60" rel="noopener noreferrer"&gt;Discussion #60&lt;/a&gt; for our Recurrent-Depth Transformer analysis.)&lt;/p&gt;

&lt;p&gt;Here's the full component breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MoE Routing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Top-16 sparse expert selection&lt;/td&gt;
&lt;td&gt;Also unlocks Mixtral, DBRX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MXFP4 Dequantization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FP4 expert weights with block scaling&lt;/td&gt;
&lt;td&gt;V4's native format — not INT4 GPTQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Engram&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O(1) hash-lookup factual memory&lt;/td&gt;
&lt;td&gt;Unique to DeepSeek, DRAM-resident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Three-Pool Attention&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SWA + C4 + C128 compression&lt;/td&gt;
&lt;td&gt;1M context with &amp;lt;10% throughput drop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hyper-Connections (mHC)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4D manifold-constrained residual&lt;/td&gt;
&lt;td&gt;Every transformer layer uses this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MTP Drafting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Integrated speculative decoding&lt;/td&gt;
&lt;td&gt;~2.5 tokens accepted per step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KV Cache Tiering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CPU-GPU cache with LRU eviction&lt;/td&gt;
&lt;td&gt;128K+ context on 24GB consumer GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PD-Disaggregation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prefill/Decode split serving&lt;/td&gt;
&lt;td&gt;Production throughput scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total estimate: 22-30 weeks. It's a lot. But MoE routing alone unlocks V4, Mixtral, and &lt;a href="https://allenai.org/papers/bar" rel="noopener noreferrer"&gt;BAR&lt;/a&gt; (Allen AI's modular post-training). Each component is independently valuable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The GoGPU Ecosystem
&lt;/h2&gt;

&lt;p&gt;Born's GPU backend is powered by the &lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;GoGPU&lt;/a&gt; ecosystem — pure Go GPU infrastructure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;LOC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;gogpu/gg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2D graphics with GPU compute shaders&lt;/td&gt;
&lt;td&gt;~222K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;gogpu/naga&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shader compiler (WGSL → SPIR-V, MSL, HLSL, GLSL, DXIL)&lt;/td&gt;
&lt;td&gt;~199K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;gogpu/wgpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pure Go WebGPU (Vulkan, Metal, DX12, GLES, Software)&lt;/td&gt;
&lt;td&gt;~156K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;gogpu/gogpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Graphics framework + windowing&lt;/td&gt;
&lt;td&gt;~52K&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Combined with Born's ~81K LOC, that's &lt;strong&gt;710K+ lines of pure Go GPU code&lt;/strong&gt;. No CGO. No Rust. Just &lt;code&gt;go build&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/born-ml/born
&lt;span class="nb"&gt;cd &lt;/span&gt;born
go build ./...
go &lt;span class="nb"&gt;test&lt;/span&gt; ./... &lt;span class="nt"&gt;-short&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;examples/mnist &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; go run &lt;span class="nb"&gt;.&lt;/span&gt;       &lt;span class="c"&gt;# MLP: 97.44% accuracy&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;examples/mnist-cnn &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; go run &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# CNN: 98.18% accuracy&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;examples/mnist-gpu &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; go run &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# GPU-accelerated inference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GPU examples now work with &lt;code&gt;go run&lt;/code&gt; — no &lt;code&gt;.dll&lt;/code&gt; download step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build This With Us
&lt;/h2&gt;

&lt;p&gt;Born is at an inflection point. GPU is stable. The architecture is proven. The roadmap to DeepSeek V4 is clear.&lt;/p&gt;

&lt;p&gt;We're not looking for passive users. We're looking for people who want to help build one of the best ML frameworks in the world. In Go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How you can make a difference:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File issues.&lt;/strong&gt; Found a bug? A missing operator? An edge case that breaks your model? Every issue makes Born more production-ready. Our three external contributors started exactly this way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Send PRs.&lt;/strong&gt; Missing tensor ops (TopK, Scatter — needed for MoE), CPU optimizations (the inner loops are naive — lots of low-hanging fruit), new ONNX operators, quantization infrastructure. Look at what &lt;a href="https://github.com/bennibbelink" rel="noopener noreferrer"&gt;@bennibbelink&lt;/a&gt; and &lt;a href="https://github.com/gmohmad" rel="noopener noreferrer"&gt;@gmohmad&lt;/a&gt; have done — full vertical slices, production quality. That's the standard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bring breakthrough ideas.&lt;/strong&gt; The hardest problems ahead — MoE routing, FP4 dequantization, compressed sparse attention, CPU-GPU cache tiering — are open research questions in Go. If you have insights on how to make these work efficiently in pure Go, we want to hear them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Challenge our assumptions.&lt;/strong&gt; Tell us what we're doing wrong. Tell us what's missing. The best frameworks are shaped by people who care enough to argue.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Found a bug?&lt;/strong&gt; &lt;a href="https://github.com/born-ml/born/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Have a big idea?&lt;/strong&gt; &lt;a href="https://github.com/born-ml/born/discussions/4" rel="noopener noreferrer"&gt;Feature Requests &amp;amp; Roadmap Discussion&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Questions?&lt;/strong&gt; &lt;a href="https://github.com/born-ml/born/discussions/3" rel="noopener noreferrer"&gt;Getting Started &amp;amp; FAQ&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Ready to code?&lt;/strong&gt; &lt;a href="https://github.com/born-ml/born/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing Guide&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;github.com/born-ml/born&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;v0.8.0 Release&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/born-ml/born/releases/tag/v0.8.0" rel="noopener noreferrer"&gt;Release Notes&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Documentation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://pkg.go.dev/github.com/born-ml/born" rel="noopener noreferrer"&gt;pkg.go.dev/github.com/born-ml/born&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Roadmap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/born-ml/born/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;ROADMAP.md&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Changelog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/born-ml/born/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GoGPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;github.com/gogpu&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Five months ago, Born was a birthday project with zero stars. Today it's a pure Go ML framework with GPU acceleration, 4 contributors, 49 ONNX operators, and a roadmap to run DeepSeek V4.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;.dll&lt;/code&gt;. No &lt;code&gt;.so&lt;/code&gt;. No excuses. Models are born production-ready.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go build&lt;/code&gt;. Ship. Done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Star us on GitHub: &lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;github.com/born-ml/born&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>ai</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>We Built a Pure Go System Tray Library Because Every Alternative Requires CGO, GoGPU May 2026</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Thu, 30 Apr 2026 21:10:33 +0000</pubDate>
      <link>https://dev.to/kolkov/we-built-a-pure-go-system-tray-library-because-every-alternative-requires-cgo-gogpu-may-2026-3h2i</link>
      <guid>https://dev.to/kolkov/we-built-a-pure-go-system-tray-library-because-every-alternative-requires-cgo-gogpu-may-2026-3h2i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Love the no CGO — but quickly realized there's no code?"&lt;/em&gt;&lt;br&gt;
— &lt;a href="https://github.com/gogpu/systray/issues/1" rel="noopener noreferrer"&gt;@cmilesio, gogpu/systray#1&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair point. We published the repo with just a README and a dream. Three days later: &lt;strong&gt;5,800+ lines of Pure Go&lt;/strong&gt;, three platforms, 74 tests, 84% coverage, and a working system tray icon on Windows.&lt;/p&gt;

&lt;p&gt;Today we're releasing &lt;strong&gt;&lt;a href="https://github.com/gogpu/systray" rel="noopener noreferrer"&gt;gogpu/systray v0.1.0&lt;/a&gt;&lt;/strong&gt; — the first Pure Go system tray library that works on Windows, macOS, and Linux without a C compiler.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every Go system tray library requires CGO:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;CGO?&lt;/th&gt;
&lt;th&gt;The Catch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/getlantern/systray" rel="noopener noreferrer"&gt;getlantern/systray&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;3.3K&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (macOS, Linux)&lt;/td&gt;
&lt;td&gt;AppIndicator + GTK3 on Linux, Cocoa via CGO on macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/fyne-io/systray" rel="noopener noreferrer"&gt;fyne-io/systray&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;fork&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (macOS, Linux)&lt;/td&gt;
&lt;td&gt;Same CGO deps, fork of getlantern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/nicola-tesla/systray" rel="noopener noreferrer"&gt;energye/systray&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Walk/LCL dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CGO means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need a C compiler installed (&lt;code&gt;apt install gcc&lt;/code&gt;, Xcode, MinGW)&lt;/li&gt;
&lt;li&gt;Cross-compilation breaks (&lt;code&gt;GOOS=linux&lt;/code&gt; from macOS? Good luck with CGO)&lt;/li&gt;
&lt;li&gt;Larger binaries, slower builds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CGO_ENABLED=0&lt;/code&gt; doesn't work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go is famous for "single binary, cross-compile anywhere." CGO breaks that promise.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Native APIs via Pure Go FFI
&lt;/h2&gt;

&lt;p&gt;We went platform-native without CGO:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Native API&lt;/th&gt;
&lt;th&gt;Go FFI&lt;/th&gt;
&lt;th&gt;LOC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Windows&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Shell_NotifyIconW&lt;/code&gt; (shell32.dll)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;golang.org/x/sys/windows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,027&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;macOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NSStatusBar&lt;/code&gt; / &lt;code&gt;NSStatusItem&lt;/code&gt; (AppKit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go-webgpu/goffi&lt;/code&gt; (ObjC runtime)&lt;/td&gt;
&lt;td&gt;1,385&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Linux&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;StatusNotifierItem (D-Bus SNI)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;godbus/dbus/v5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;810&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No C compiler. No shared libraries. No &lt;code&gt;dlopen&lt;/code&gt; of GTK. Just Go talking directly to the OS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Windows: Shell_NotifyIconW
&lt;/h3&gt;

&lt;p&gt;The Win32 approach is straightforward — &lt;code&gt;Shell_NotifyIconW&lt;/code&gt; has been the tray API since Windows 95. We call it via &lt;code&gt;golang.org/x/sys/windows&lt;/code&gt;, the same way the Go standard library talks to Windows.&lt;/p&gt;

&lt;p&gt;Key details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Message-only HWND&lt;/strong&gt; for callbacks (invisible, no taskbar entry)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NOTIFYICON_VERSION_4&lt;/strong&gt; for modern event dispatch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explorer crash recovery&lt;/strong&gt; — when &lt;code&gt;explorer.exe&lt;/code&gt; restarts, tray icons disappear. We listen for the &lt;code&gt;TaskbarCreated&lt;/code&gt; registered message and re-add the icon automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode auto-switching&lt;/strong&gt; — detect &lt;code&gt;WM_SETTINGCHANGE&lt;/code&gt; + &lt;code&gt;ImmersiveColorSet&lt;/code&gt;, read &lt;code&gt;SystemUsesLightTheme&lt;/code&gt; registry key, swap HICON. Your tray icon adapts when the user toggles Windows dark mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  macOS: NSStatusBar via ObjC Runtime
&lt;/h3&gt;

&lt;p&gt;This is where it gets interesting. Calling AppKit without CGO requires speaking the Objective-C runtime protocol:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;objc_getClass("NSStatusBar")&lt;/code&gt; — get the class&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;objc_msgSend(class, sel("systemStatusBar"))&lt;/code&gt; — get the shared status bar&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;objc_msgSend(statusBar, sel("statusItemWithLength:"), -1.0)&lt;/code&gt; — create a status item&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We built a minimal ObjC runtime wrapper (~490 LOC) using &lt;a href="https://github.com/go-webgpu/goffi" rel="noopener noreferrer"&gt;goffi&lt;/a&gt; — our Pure Go FFI library. Same approach we use for the Metal GPU backend in &lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;gogpu/wgpu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The killer feature on macOS: &lt;strong&gt;template icons&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;tray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTemplateIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monochromePNG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This calls &lt;code&gt;[NSImage setTemplate:YES]&lt;/code&gt;, telling macOS the icon is a monochrome mask. The OS automatically renders it white on dark menu bars, black on light ones. No dark mode handling needed — Apple does it for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Linux: D-Bus StatusNotifierItem
&lt;/h3&gt;

&lt;p&gt;Linux is the most complex platform. The "system tray" isn't a single API — it's a D-Bus protocol called &lt;a href="https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/" rel="noopener noreferrer"&gt;StatusNotifierItem&lt;/a&gt; (SNI).&lt;/p&gt;

&lt;p&gt;We implement two D-Bus interfaces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;org.kde.StatusNotifierItem&lt;/code&gt;&lt;/strong&gt; — the tray icon itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Properties: &lt;code&gt;Category&lt;/code&gt;, &lt;code&gt;Id&lt;/code&gt;, &lt;code&gt;Title&lt;/code&gt;, &lt;code&gt;Status&lt;/code&gt;, &lt;code&gt;IconPixmap&lt;/code&gt;, &lt;code&gt;ToolTip&lt;/code&gt;, &lt;code&gt;Menu&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Methods: &lt;code&gt;Activate&lt;/code&gt; (click), &lt;code&gt;SecondaryActivate&lt;/code&gt; (middle-click), &lt;code&gt;ContextMenu&lt;/code&gt; (right-click)&lt;/li&gt;
&lt;li&gt;Signals: &lt;code&gt;NewIcon&lt;/code&gt;, &lt;code&gt;NewTitle&lt;/code&gt;, &lt;code&gt;NewStatus&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;com.canonical.dbusmenu&lt;/code&gt;&lt;/strong&gt; — the context menu:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A recursive tree of menu items with labels, types, toggle states&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GetLayout&lt;/code&gt; returns the full tree, &lt;code&gt;Event&lt;/code&gt; dispatches clicks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a registration dance with &lt;code&gt;org.kde.StatusNotifierWatcher&lt;/code&gt; — plus automatic re-registration when the desktop panel restarts.&lt;/p&gt;

&lt;p&gt;The PNG→ARGB conversion is a fun detail: SNI wants ARGB32 in network byte order (big-endian), so we decode the PNG with &lt;code&gt;image/png&lt;/code&gt; and manually pack &lt;code&gt;[A, R, G, B]&lt;/code&gt; bytes.&lt;/p&gt;

&lt;p&gt;All of this via &lt;a href="https://github.com/godbus/dbus" rel="noopener noreferrer"&gt;godbus/dbus/v5&lt;/a&gt; — the canonical Pure Go D-Bus library. Zero CGO.&lt;/p&gt;




&lt;h2&gt;
  
  
  The API
&lt;/h2&gt;

&lt;p&gt;We went with a builder pattern inspired by &lt;a href="https://v3alpha.wails.io/" rel="noopener noreferrer"&gt;Wails 3&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/gogpu/systray"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tray&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;systray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;menu&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;systray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Opening..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSeparator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddCheckbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dark Mode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Toggled!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSubmenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"More..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;systray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"About"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"v1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Help"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Help!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSeparator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Quit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;tray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iconPNG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;SetDarkModeIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;darkIconPNG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;SetTooltip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"My App"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;SetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="n"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;tray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Clicked!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;tray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// blocks, pumps platform messages&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Multiple trays&lt;/strong&gt; are supported — each call to &lt;code&gt;systray.New()&lt;/code&gt; creates an independent icon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;mainTray&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;systray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appIcon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mainMenu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;statusTray&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;systray&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statusIcon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTooltip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Status: OK"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Enterprise Research
&lt;/h2&gt;

&lt;p&gt;We didn't guess at the architecture. Before writing code, we studied how the big frameworks do it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Tray Architecture&lt;/th&gt;
&lt;th&gt;Our Takeaway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qt6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;QPlatformSystemTrayIcon&lt;/code&gt; → 3 platform implementations&lt;/td&gt;
&lt;td&gt;Three-layer pattern (public API → interface → platform impl)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wails 3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;systemTrayImpl&lt;/code&gt; interface, native per-platform&lt;/td&gt;
&lt;td&gt;Builder API pattern, multiple tray support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SDL3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4 backends (AppIndicator, D-Bus, Win32, Cocoa)&lt;/td&gt;
&lt;td&gt;We chose D-Bus SNI directly, skipping AppIndicator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Electron&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nativeTheme.shouldUseDarkColors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dark mode detection via &lt;code&gt;WM_SETTINGCHANGE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GLFW&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RemovePropW&lt;/code&gt; before &lt;code&gt;DestroyWindow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Destroy pattern (avoid deadlocks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;getlantern/systray&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GetMessage&lt;/code&gt; loop, hidden WS_OVERLAPPEDWINDOW&lt;/td&gt;
&lt;td&gt;Message pump pattern (we use &lt;code&gt;HWND_MESSAGE&lt;/code&gt; instead)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The architecture follows Qt6's &lt;code&gt;QPlatformSystemTrayIcon&lt;/code&gt; pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systray.New()  →  SystemTray (public API, delegation)
                       │
                  PlatformTray (internal interface)
                       │
          ┌────────────┼────────────┐
     Win32 impl   macOS impl   Linux impl
     Shell_Notify  NSStatusBar   D-Bus SNI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Not AppIndicator on Linux?
&lt;/h2&gt;

&lt;p&gt;The tempting path: &lt;code&gt;dlopen("libayatana-appindicator3.so.1")&lt;/code&gt; and let GTK3 handle everything. That's what getlantern/systray does (via CGO).&lt;/p&gt;

&lt;p&gt;Problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pulls in GTK3 runtime&lt;/strong&gt; — gigantic dependency for a tray icon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's just a wrapper around SNI&lt;/strong&gt; — AppIndicator talks D-Bus SNI internally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Icon caching bugs&lt;/strong&gt; — AppIndicator caches icons by filename, causing stale icons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not available everywhere&lt;/strong&gt; — minimal compositors (Sway, Hyprland) don't have AppIndicator&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We cut out the middleman. D-Bus SNI directly via godbus — same protocol, no GTK, no CGO.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total:     ~5,800 lines of Pure Go (6,900 with docs/CI/configs)
Tests:     74 (84% public API coverage)
Platforms: Windows ✅, macOS ✅, Linux ✅
Deps:      golang.org/x/sys, go-webgpu/goffi, godbus/dbus/v5
CGO:       Zero. Absolutely zero.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/gogpu/systray@v0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/gogpu/systray
&lt;span class="nb"&gt;cd &lt;/span&gt;systray/examples/basic
go run &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A green icon appears in your system tray. Right-click for the menu. Toggle dark mode to see auto-switching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We need testers&lt;/strong&gt; — especially on macOS and Linux (KDE, GNOME + AppIndicator extension, XFCE, Sway). &lt;a href="https://github.com/gogpu/systray/issues" rel="noopener noreferrer"&gt;File issues&lt;/a&gt; if something doesn't work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part of GoGPU
&lt;/h2&gt;

&lt;p&gt;systray is standalone (&lt;code&gt;go get github.com/gogpu/systray&lt;/code&gt; — no gogpu dependency), but it's designed to integrate with the &lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;GoGPU ecosystem&lt;/a&gt; — 800K+ lines of Pure Go GPU code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;wgpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Pure Go WebGPU (Vulkan/Metal/DX12/GLES)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;naga&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shader compiler (WGSL → SPIR-V/MSL/GLSL/HLSL/DXIL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;gg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2D graphics (Skia-class rasterizer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;gogpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;App framework, windowing, input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;ui&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GUI toolkit (22+ widgets, Material 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/gogpu/systray" rel="noopener noreferrer"&gt;systray&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;System tray (this library)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All Pure Go. All zero CGO. All cross-platform. Four gogpu libraries are listed in &lt;a href="https://github.com/avelino/awesome-go" rel="noopener noreferrer"&gt;awesome-go&lt;/a&gt;: systray (GUI Interaction), ui (GUI Toolkits), gg (Images), gogpu (Game Development).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you build something with systray, &lt;a href="https://github.com/gogpu/gogpu/discussions" rel="noopener noreferrer"&gt;let us know&lt;/a&gt;. Star ⭐ the repo if you find it useful — it helps others discover the project.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>programming</category>
      <category>linux</category>
    </item>
    <item>
      <title>GoGPU - 790K Lines of Pure Go: Multi-Window GPU Apps, DXIL Compiler, and Why We Don't Need CGO</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Tue, 28 Apr 2026 13:14:08 +0000</pubDate>
      <link>https://dev.to/kolkov/gogpu-790k-lines-of-pure-go-multi-window-gpu-apps-dxil-compiler-and-why-we-dont-need-cgo-3i94</link>
      <guid>https://dev.to/kolkov/gogpu-790k-lines-of-pure-go-multi-window-gpu-apps-dxil-compiler-and-why-we-dont-need-cgo-3i94</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Go deserves more support in GUI development"&lt;/em&gt;&lt;br&gt;
— &lt;a href="https://www.reddit.com/r/golang/comments/1pdw9i7/go_deserves_more_support_in_gui_development/" rel="noopener noreferrer"&gt;r/golang, October 2024&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That Reddit post hit a nerve. Hundreds of upvotes, dozens of comments echoing the same frustration: Go has world-class networking, databases, and CLI tools — but when it comes to graphics and GUI, the ecosystem says "just use Electron" or "call into C."&lt;/p&gt;

&lt;p&gt;I'd been planning a Pure Go GPU stack for years. Spent the year before that post studying exactly what to build and how — dissecting Vulkan, Metal, DX12, reading wgpu source code, analyzing Qt and Flutter architectures. That Reddit thread was the final push.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;December 5, 2025&lt;/strong&gt;, the &lt;a href="https://dev.to/kolkov/gogpu-a-pure-go-graphics-library-for-gpu-programming-2j5d"&gt;first window with a triangle&lt;/a&gt; appeared on screen. Pure Go, zero CGO, Vulkan rendering.&lt;/p&gt;

&lt;p&gt;Less than five months later: &lt;strong&gt;790K lines of Pure Go&lt;/strong&gt;, &lt;strong&gt;13 repositories&lt;/strong&gt;, &lt;strong&gt;678 GitHub stars&lt;/strong&gt; across the ecosystem, and people building real software on it — from Quake engines to ML frameworks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;gg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2D graphics (Skia-class rasterizer)&lt;/td&gt;
&lt;td&gt;219K&lt;/td&gt;
&lt;td&gt;v0.43.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;naga&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Shader compiler (WGSL→SPIR-V/MSL/GLSL/HLSL/DXIL)&lt;/td&gt;
&lt;td&gt;195K&lt;/td&gt;
&lt;td&gt;v0.17.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;ui&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;GUI toolkit (22+ widgets, 4 themes, 97% coverage)&lt;/td&gt;
&lt;td&gt;171K&lt;/td&gt;
&lt;td&gt;v0.1.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;wgpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;WebGPU implementation (Vulkan/DX12/Metal/GLES/Software)&lt;/td&gt;
&lt;td&gt;145K&lt;/td&gt;
&lt;td&gt;v0.26.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;gogpu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Application framework, windowing, input&lt;/td&gt;
&lt;td&gt;50K&lt;/td&gt;
&lt;td&gt;v0.30.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/g3d" rel="noopener noreferrer"&gt;g3d&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;3D rendering (scene graph, PBR, GLTF)&lt;/td&gt;
&lt;td&gt;planned&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/compose" rel="noopener noreferrer"&gt;compose&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Jetpack Compose-style declarative UI&lt;/td&gt;
&lt;td&gt;planned&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/systray" rel="noopener noreferrer"&gt;systray&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Cross-platform system tray&lt;/td&gt;
&lt;td&gt;planned&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ 4 more&lt;/td&gt;
&lt;td&gt;gpucontext, gputypes, gg-pdf, gg-svg&lt;/td&gt;
&lt;td&gt;9K&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13 repos, Pure Go, zero CGO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~790K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;790K lines of Go. 577K of that is pure code (excluding blanks and comments). 421 commits in the last two months alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stars across the ecosystem:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gogpu&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ui&lt;/td&gt;
&lt;td&gt;211&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gg&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wgpu&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naga&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;678&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How It Started
&lt;/h2&gt;

&lt;p&gt;The gap between Go's server capabilities and its desktop capabilities has bothered me for years. Go has &lt;code&gt;net/http&lt;/code&gt; that scales to millions of connections. It has database libraries that handle petabytes. But ask "how do I draw a triangle?" and you get answers involving CGO, shared libraries, or wrapping Electron in a Go process.&lt;/p&gt;

&lt;p&gt;The existing options in 2024-2025:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;The Catch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/wailsapp/wails" rel="noopener noreferrer"&gt;Wails&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~34K&lt;/td&gt;
&lt;td&gt;Go backend + System WebView&lt;/td&gt;
&lt;td&gt;Not native rendering. Your "Go app" is HTML/CSS/JS with JSON IPC.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/fyne-io/fyne" rel="noopener noreferrer"&gt;Fyne&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~28K&lt;/td&gt;
&lt;td&gt;Go + OpenGL via CGO&lt;/td&gt;
&lt;td&gt;Requires C compiler. Custom widget look, not truly native.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/hajimehoshi/ebiten" rel="noopener noreferrer"&gt;Ebiten&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~13K&lt;/td&gt;
&lt;td&gt;Go + OpenGL/Metal/DX (purego)&lt;/td&gt;
&lt;td&gt;Game engine, not a GUI toolkit. No widget system.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cogentcore/core" rel="noopener noreferrer"&gt;Cogent Core&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~2.3K&lt;/td&gt;
&lt;td&gt;Go + Vulkan via CGO (glfw)&lt;/td&gt;
&lt;td&gt;CGO required. Large dependency tree.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/gioui/gio" rel="noopener noreferrer"&gt;Gio&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~2.1K&lt;/td&gt;
&lt;td&gt;Pure Go, immediate mode&lt;/td&gt;
&lt;td&gt;Small widget set. Pre-1.0 API. No retained mode.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every major framework either requires CGO or delegates rendering to a WebView. The only Pure Go option (Gio) is immediate-mode with a minimal widget set. Nothing offers native GPU rendering (Vulkan/Metal/DX12) without a C compiler.&lt;/p&gt;

&lt;p&gt;I spent a year studying the problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read Rust wgpu's source code to understand how a modern WebGPU implementation works&lt;/li&gt;
&lt;li&gt;Studied Qt6's RHI, GTK4's GSK renderer, Flutter's Impeller to understand GUI rendering pipelines&lt;/li&gt;
&lt;li&gt;Analyzed Vulkan, Metal, DX12, and OpenGL ES specs to map out the abstraction boundaries&lt;/li&gt;
&lt;li&gt;Designed the layered architecture on paper before writing Go code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On &lt;strong&gt;December 5, 2025&lt;/strong&gt;, the &lt;a href="https://dev.to/kolkov/gogpu-a-pure-go-graphics-library-for-gpu-programming-2j5d"&gt;first window with a triangle&lt;/a&gt; appeared on screen — rendered through &lt;a href="https://github.com/go-webgpu/webgpu" rel="noopener noreferrer"&gt;go-webgpu/webgpu&lt;/a&gt; (our Rust FFI bindings) and the gogpu framework. The Rust backend proved the architecture worked. Then, in parallel, &lt;code&gt;naga&lt;/code&gt; (shader compiler) and &lt;code&gt;wgpu&lt;/code&gt; (Pure Go WebGPU) began replacing the Rust dependencies one by one. Then came &lt;code&gt;gg&lt;/code&gt; (2D graphics), and finally &lt;code&gt;ui&lt;/code&gt; (GUI toolkit).&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Window: The Feature That Changes Everything
&lt;/h2&gt;

&lt;p&gt;Single-window apps are fine for demos. Real applications — IDEs, design tools, database clients — need multiple windows. We shipped multi-window support in gogpu v0.28.0, and it was the hardest architectural change we've made.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Every major Go GUI framework (Fyne, Gio, Ebiten) either doesn't support multi-window or hacks it with separate processes. The reason is simple: GPU devices are expensive to create, but surfaces (swapchains) are per-window. You need to share one device across many windows without data races.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Built
&lt;/h3&gt;

&lt;p&gt;We studied 7 frameworks — Qt6, GTK4, SDL3, winit, GLFW, Fyne, Gio — and wrote a 26-page Architecture Decision Record before writing a line of code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            ┌────────────────────────────────┐
            │      Shared GPU Context        │
            │  Instance / Adapter / Device   │
            │  Queue / Pipelines / Textures  │
            └───────┬──────────┬─────────┬───┘
                    │          │         │
            ┌───────┴───┐ ┌───┴────┐ ┌──┴─────────┐
            │ Window 1  │ │Window 2│ │ Window 3   │
            │ Surface   │ │Surface │ │ Surface    │
            │ Swapchain │ │Swapch. │ │ Swapchain  │
            │ Callbacks │ │Callbk. │ │ Callbacks  │
            └───────────┘ └────────┘ └────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gogpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gogpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;WithTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Main Window"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;WithSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;800&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c"&gt;// Second window — shares GPU device, gets its own surface&lt;/span&gt;
&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gogpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WindowConfig&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Tool Palette"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Height&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnDraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gogpu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// dc has its own surface, shared device&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood: &lt;code&gt;PlatformManager&lt;/code&gt; handles process-level concerns (Win32 &lt;code&gt;RegisterClass&lt;/code&gt;, Cocoa &lt;code&gt;NSApplication&lt;/code&gt;, X11 &lt;code&gt;Display&lt;/code&gt;), while each &lt;code&gt;PlatformWindow&lt;/code&gt; manages its own message pump slice, surface, and swapchain. The &lt;code&gt;WindowManager&lt;/code&gt; tracks all windows with monotonic &lt;code&gt;WindowID&lt;/code&gt; — stable across window recreation, serializable for event queues. Same pattern SDL3 uses with &lt;code&gt;SDL_GetNextObjectID()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  VSync Strategy
&lt;/h3&gt;

&lt;p&gt;Primary window runs Fifo (VSync), secondary windows run Immediate. Focus changes switch the VSync mode — the window you're looking at gets smooth frames, background windows don't waste GPU cycles. This is what Qt6 does with &lt;code&gt;QRhi&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Platform
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Multi-Window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;Vulkan, DX12&lt;/td&gt;
&lt;td&gt;✅ Win32 &lt;code&gt;CreateWindowEx&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;Metal&lt;/td&gt;
&lt;td&gt;✅ Cocoa &lt;code&gt;NSWindow&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux X11&lt;/td&gt;
&lt;td&gt;Vulkan&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;XCreateWindow&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux Wayland&lt;/td&gt;
&lt;td&gt;Vulkan&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;xdg_toplevel&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All 4 platforms support multi-window with shared GPU device. EventFocus events route correctly across windows on all platforms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event-Driven Frame Pacing
&lt;/h2&gt;

&lt;p&gt;Enterprise GUI frameworks don't render on every OS event. They render only when something visual actually changed. Our render loop had a shortcut:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;continuous&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;invalidated&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;hasEvents&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;renderFrame&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;hasEvents&lt;/code&gt; flag meant every mouse move, every platform event triggered a full render cycle — even when nothing on screen changed. Moving the mouse over a static window caused unnecessary GPU work on every frame.&lt;/p&gt;

&lt;p&gt;We researched 6 frameworks (winit, Gio, Qt6, Flutter, SDL3, Ebiten) and found they all use the same pattern: &lt;strong&gt;handlers decide when to invalidate, the render loop never guesses&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now in v0.30.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;continuous&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;invalidated&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;renderFrame&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resize and focus events call &lt;code&gt;RequestRedraw()&lt;/code&gt; explicitly. Mouse events reach the UI framework, which calls &lt;code&gt;RequestRedraw()&lt;/code&gt; only when a widget's visual state actually changes.&lt;/p&gt;

&lt;p&gt;Mouse move over static UI: &lt;strong&gt;0% GPU&lt;/strong&gt;. Hover over a button: UI calls &lt;code&gt;RequestRedraw()&lt;/code&gt;, one frame renders. Exactly how winit and Flutter work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Pure Go DXIL Generator
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/kolkov/we-built-the-first-pure-go-dxil-generator-because-optimizing-the-wrong-path-wasnt-enough-35en"&gt;We wrote about this in detail&lt;/a&gt;, but here's the update: &lt;strong&gt;naga's DXIL backend now passes 94/208 DXC golden tests&lt;/strong&gt; (45% parity with Microsoft's own compiler). The IDxcValidator pass rate is 161/170 (94.7%).&lt;/p&gt;

&lt;p&gt;This matters because Rust naga — the reference implementation maintained by Mozilla — still doesn't have a DXIL backend. There's been an &lt;a href="https://github.com/gfx-rs/wgpu/issues/4302" rel="noopener noreferrer"&gt;open issue since 2020&lt;/a&gt;. Six years later, still not implemented.&lt;/p&gt;

&lt;p&gt;We did it in Pure Go. No LLVM, no external tools. Direct DXIL bitcode generation from our shader IR.&lt;/p&gt;

&lt;p&gt;Yes, we know Microsoft is adding &lt;a href="https://devblogs.microsoft.com/directx/directx-adopting-spir-v/" rel="noopener noreferrer"&gt;SPIR-V support to DirectX 12&lt;/a&gt; in a future SDK update. But DXIL is the native DX12 shader format today, and having a direct DXIL generator means we don't depend on external toolchains or wait for Microsoft's timeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Shader Pipeline Works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WGSL source code
      ↓  (naga lexer + parser)
   naga IR (typed, validated)
      ↓  (backend selection)
  ┌───┴───┬────┬─────┬──────┐
  SPIR-V  MSL  GLSL  HLSL  DXIL
  (Vulkan)(Metal)(GLES)(DX11)(DX12)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 5 backends generate from the same IR. 330+ tests. The DXIL backend includes single-store local promotion, strength reduction, fragment signature ordering, and dead code elimination — real compiler passes, not string templating.&lt;/p&gt;




&lt;h2&gt;
  
  
  gogpu/ui: A Real GUI Toolkit
&lt;/h2&gt;

&lt;p&gt;Most Go GUI toolkits ship a handful of basic controls and call it done. We're building something closer to Qt or Flutter — a complete widget system with theming, accessibility, and enterprise-grade architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;22+ widgets&lt;/strong&gt; shipping today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Widgets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TextField, TextArea (multi-line), Checkbox, Radio, Switch, Slider, Dropdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Display&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Label, Image, Icon, ProgressBar, Badge, Chip, Divider, Card&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Navigation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Button, IconButton, FAB, AppBar, TabBar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Column, Row, Stack, ListView, ScrollView, GridView&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overlay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dialog, Snackbar, Tooltip, BottomSheet, PopupMenu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Advanced&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TreeView, DataTable, SplitView, Docking, Menu, Toolbar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;4 design systems:&lt;/strong&gt; Material Design 3, DevTools (JetBrains), Fluent (Microsoft), Cupertino (Apple). Full token-based theming — change every color, radius, elevation, font in one struct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;171K LOC | 55+ packages | 6,803 tests | 97%+ average coverage.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal-Driven Retained-Mode Compositor (ADR-007)
&lt;/h3&gt;

&lt;p&gt;The latest v0.1.14 shipped the biggest architectural change in ui: a &lt;strong&gt;signal-driven retained-mode compositor&lt;/strong&gt; that replaced the legacy hybrid pipeline.&lt;/p&gt;

&lt;p&gt;The old pipeline had a fundamental conflict: CPU pixmap was retained (persistent between frames), but GPU shapes (shadows, rounded corners) were ephemeral (re-queued every frame). On frames where nothing changed, GPU shapes disappeared — visible flicker.&lt;/p&gt;

&lt;p&gt;ADR-007 unified everything into a scene graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Widget tree
    ↓  SetNeedsRedraw() (per-widget dirty flag)
Scene.Encoding (compact display list: 9-25 bytes/command)
    ↓  RepaintBoundary (cached scene per subtree)
scene.Renderer (auto GPU/CPU selection, 64×64 tile grid)
    ↓  FlushGPUWithView (single render pass → swapchain)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every widget records into a &lt;code&gt;scene.Scene&lt;/code&gt; display list instead of drawing to a pixmap. &lt;code&gt;RepaintBoundary&lt;/code&gt; caches scene encodings per subtree — when a spinner animates, only its 48×48 tile re-renders, not the full window. Reactive signals (&lt;code&gt;coregx/signals&lt;/code&gt;) trigger &lt;code&gt;SetNeedsRedraw()&lt;/code&gt; on exactly the widgets that changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Taskmanager example GPU: &lt;strong&gt;7-18% → 0-1%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;IDE hover lag: &lt;strong&gt;eliminated&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Gallery flickering: &lt;strong&gt;fixed&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Full DrawTree via GPU pipeline every frame — no retained CPU pixmap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is still a work in progress. Complex animated widgets like classic spinners need additional optimization — the present path still does a full-surface render pass even when only a 48×48 pixel region changed. Damage-rect passthrough and sub-region compositing are the next steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  More Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Reactive state via signals (Signal, Computed, Effect, Binding)&lt;/li&gt;
&lt;li&gt;GPU-accelerated rendering via gg → wgpu&lt;/li&gt;
&lt;li&gt;Granular widget invalidation — 11 widgets use &lt;code&gt;SetNeedsRedraw + InvalidateRect&lt;/code&gt; instead of full-surface redraw&lt;/li&gt;
&lt;li&gt;Accessibility from day one: 35+ ARIA roles, screen reader announcer&lt;/li&gt;
&lt;li&gt;Full i18n: CLDR plural rules, RTL detection&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Coming Next
&lt;/h2&gt;

&lt;h3&gt;
  
  
  g3d — 3D Rendering Library
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/gogpu/g3d" rel="noopener noreferrer"&gt;gogpu/g3d&lt;/a&gt; is the Three.js of Go. Scene graph, PBR materials (metallic-roughness), GLTF 2.0 loading, directional/point/spot lights, frustum culling, instance batching. Built on wgpu, not OpenGL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cube&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewBoxGeometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;g3d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewStandardMaterial&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cube&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  compose — Declarative UI
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/gogpu/compose" rel="noopener noreferrer"&gt;gogpu/compose&lt;/a&gt; brings Jetpack Compose-style declarative UI to Go. Composable functions, automatic recomposition on state change, slot-based layout. Transport-pluggable architecture — same composable code can render to GPU window, headless image, or remote display.&lt;/p&gt;

&lt;h3&gt;
  
  
  systray — System Tray
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/gogpu/systray" rel="noopener noreferrer"&gt;gogpu/systray&lt;/a&gt; — cross-platform system tray icons. Win32 Notification Area, macOS Menu Bar Extra, Linux StatusNotifierItem/AppIndicator. Multiple trays, nested menus, notifications, dark mode icon switching. Pure Go, zero CGO.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser Target (WASM + WebGPU)
&lt;/h3&gt;

&lt;p&gt;wgpu already has the build infrastructure for WASM (platform split shipped in v0.25.5). The plan: compile to WASM, use browser's native WebGPU API via &lt;code&gt;syscall/js&lt;/code&gt;. Same app code, same shaders, runs in Chrome. Phase 0 complete, Phase 1 (navigator.gpu binding) is next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android Support
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/orgs/gogpu/discussions/31" rel="noopener noreferrer"&gt;Under active discussion&lt;/a&gt;. The architecture is ready — wgpu's Vulkan backend works, naga generates SPIR-V, gogpu's &lt;code&gt;PlatformManager&lt;/code&gt;/&lt;code&gt;PlatformWindow&lt;/code&gt; abstraction was designed with mobile in mind. What remains: &lt;code&gt;NativeActivity&lt;/code&gt; or &lt;code&gt;GameActivity&lt;/code&gt; integration, touch input, lifecycle management (suspend/resume), and the build pipeline (&lt;code&gt;gomobile&lt;/code&gt; or direct NDK).&lt;/p&gt;

&lt;h3&gt;
  
  
  ui — Major Overhaul Coming
&lt;/h3&gt;

&lt;p&gt;The GUI toolkit is getting substantial improvements. The ADR-007 retained-mode compositor was just the beginning — next up is the full scene-graph pipeline where every widget records into display lists instead of immediate-mode drawing. More widgets, better performance, production-ready accessibility. The goal: a Go GUI toolkit that you'd actually choose over Electron for a desktop app.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Users, Real Software
&lt;/h2&gt;

&lt;p&gt;This isn't a toy. People are building on GoGPU:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/darkliquid/ironwail-go" rel="noopener noreferrer"&gt;ironwail-go&lt;/a&gt;&lt;/strong&gt; by @darkliquid — a Quake 1 engine port running on gogpu/wgpu Vulkan on Wayland+niri. The first 3D game on a Pure Go GPU stack. &lt;a href="https://github.com/gogpu/gogpu/issues/163" rel="noopener noreferrer"&gt;Demo video on the project README&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/born-ml/born" rel="noopener noreferrer"&gt;Born ML&lt;/a&gt;&lt;/strong&gt; — our ML framework, migrating from Rust FFI (&lt;code&gt;go-webgpu/webgpu&lt;/code&gt;) to gogpu/wgpu. Single-binary deployment, DXIL backend for DX12 compute — things the Rust FFI path couldn't provide.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;L-System fractals&lt;/strong&gt; by @rcarlier — &lt;a href="https://github.com/gogpu/gg/issues/229" rel="noopener noreferrer"&gt;47 million points&lt;/a&gt; rendered via gg, running in WASM and CLI. "Amazing performance!"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;@jdbann&lt;/strong&gt; — contributing Metal backend fixes, testing on M4 Pro MacBook.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;@SideFx&lt;/strong&gt; — testing on Adreno mobile GPUs, helping us find driver-specific bugs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Architecture That Makes It Work
&lt;/h2&gt;

&lt;p&gt;The secret is layered independence. Each layer does one thing, doesn't know about layers above it, and can be used standalone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Application
      │
  ┌───┴────┬──────┬────────┐
  gogpu/ui  gogpu  gg  g3d    ← Application layer
  ├────────┘  │    │    │
  gpucontext ─┘    │    │     ← Shared interfaces
  │                │    │
  wgpu ────────────┘────┘     ← WebGPU implementation
  │
  naga                        ← Shader compiler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  gogpu — The Windowing &amp;amp; Application Framework
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;gogpu/gogpu&lt;/a&gt;&lt;/strong&gt; (~50K lines) is the foundation everything else builds on. Think of it as SDL or GLFW — but Pure Go, with a WebGPU renderer built in.&lt;/p&gt;

&lt;p&gt;What it provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform windowing&lt;/strong&gt; — Win32, Cocoa (AppKit), X11, Wayland. Pure Go, no CGO. All 4 platforms implemented from scratch using our &lt;a href="https://github.com/go-webgpu/goffi" rel="noopener noreferrer"&gt;goffi&lt;/a&gt; FFI library (syscall-level, no C compiler).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU lifecycle&lt;/strong&gt; — Instance → Adapter → Device → Queue → Surface. Dual backend: Pure Go (gogpu/wgpu) or Rust FFI (go-webgpu/webgpu). Backend selected at build time or runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-window&lt;/strong&gt; — shared GPU device, per-window swapchain, monotonic WindowID, focus-aware VSync (ADR-010).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event system&lt;/strong&gt; — keyboard, mouse, touch (X11 XInput2), gestures, Unicode text input (IME) on all platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-thread architecture&lt;/strong&gt; — main thread for window events (keeps the OS happy), render thread for all GPU operations (keeps the GPU fed). Modal resize/drag on Windows handled via WM_TIMER callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HiDPI/Retina&lt;/strong&gt; — per-monitor DPI, logical/physical coordinate split, &lt;code&gt;WM_DPICHANGED&lt;/code&gt; on Windows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frameless windows&lt;/strong&gt; — custom title bars with DWM shadow, hit-test regions (JetBrains Runtime pattern).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mouse grab / pointer lock&lt;/strong&gt; — locked, confined, normal modes (SDL parity) on Win32, X11, Wayland.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software backend&lt;/strong&gt; — always available, renders to GDI (Windows), XPutImage (X11), CALayer (macOS). No GPU required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;App&lt;/code&gt; implements &lt;code&gt;DeviceProvider&lt;/code&gt; — any library that accepts a &lt;code&gt;gpucontext.DeviceProvider&lt;/code&gt; can use gogpu's GPU device without importing gogpu directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Other Layers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;naga&lt;/strong&gt; (~195K lines) — shader compiler. WGSL in, SPIR-V/MSL/GLSL/HLSL/DXIL out. Used by wgpu at pipeline creation time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;wgpu&lt;/strong&gt; (~145K lines) — Pure Go WebGPU implementation. 5 HAL backends (Vulkan, DX12, Metal, GLES, Software). Resource lifecycle, validation, state tracking. The layer between your draw calls and the GPU driver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gg&lt;/strong&gt; (~219K lines) — 2D graphics. Skia-class analytic AA rasterizer, Vello-derived tile rasterizer, GPU SDF accelerator, text rendering, SVG, alpha masks. Can work standalone (no window) or with gogpu's GPU device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gpucontext&lt;/strong&gt; (~3.6K lines) — the glue. Depends only on &lt;code&gt;gputypes&lt;/code&gt;. Defines &lt;code&gt;DeviceProvider&lt;/code&gt;, &lt;code&gt;TextureView&lt;/code&gt;, &lt;code&gt;CommandEncoder&lt;/code&gt; — interfaces that let all packages share GPU resources without circular imports. The &lt;code&gt;database/sql&lt;/code&gt; of GPU programming.&lt;/p&gt;

&lt;p&gt;Every layer can be used independently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gg&lt;/code&gt; without &lt;code&gt;gogpu&lt;/code&gt; — 2D graphics without windowing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wgpu&lt;/code&gt; without &lt;code&gt;gg&lt;/code&gt; — raw WebGPU for compute or custom rendering&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;naga&lt;/code&gt; without anything — shader compilation as a library&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Research before code.&lt;/strong&gt; Every major feature starts with an Architecture Decision Record studying 5-8 enterprise implementations. The multi-window ADR studied Qt6, GTK4, SDL3, winit, GLFW, Fyne, and Gio before a line of code was written. This saved weeks of wrong turns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. CPU is core, GPU is accelerator.&lt;/strong&gt; We analyzed 8 enterprise 2D engines (Skia, Cairo, Vello, Blend2D, tiny-skia, piet, Qt RHI, Pathfinder) and found that in zero of them is CPU rasterization a "backend." It's always the core. GPU accelerates specific operations. This insight shaped gg's entire architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Zero CGO is a real competitive advantage.&lt;/strong&gt; With the Rust FFI backend, every platform needed its own &lt;code&gt;wgpu_native.dll&lt;/code&gt;/&lt;code&gt;.so&lt;/code&gt;/&lt;code&gt;.dylib&lt;/code&gt; — find it, download it, put it in the right place, hope the versions match. With Pure Go wgpu: &lt;code&gt;go build&lt;/code&gt; and it works. No shared libraries to hunt for, no linker errors, no "works on my machine." Cross-compilation just works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Enterprise references prevent enterprise bugs.&lt;/strong&gt; When we tried to add DX12 texture barriers without studying how Rust wgpu handles them, we got TDR crashes at frame 575. When we studied the reference first, we found the root cause in 10 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Small teams win through focus.&lt;/strong&gt; Our naga DXIL backend shipped before Rust naga's (open issue since 2020). Not because of more resources — because of fewer coordination costs and a clear research → design → implement pipeline. Architectural decisions compound: choosing WebGPU as the abstraction meant our Metal backend was ready when Apple deprecated OpenGL, and our shader IR was ready when DX12 needed DXIL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
go get github.com/gogpu/gogpu

&lt;span class="c"&gt;# Run the particles demo (compute + render, zero CGO)&lt;/span&gt;
&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go run github.com/gogpu/gogpu/examples/particles@latest

&lt;span class="c"&gt;# Run the multi-window demo&lt;/span&gt;
&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go run github.com/gogpu/gogpu/examples/multiwindow@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; Go 1.25+, a GPU with Vulkan/DX12/Metal/GLES support (or use Software backend for headless).&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Organization&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu" rel="noopener noreferrer"&gt;github.com/gogpu&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main Repository&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gogpu" rel="noopener noreferrer"&gt;github.com/gogpu/gogpu&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GUI Toolkit&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/ui" rel="noopener noreferrer"&gt;github.com/gogpu/ui&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2D Graphics&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/gg" rel="noopener noreferrer"&gt;github.com/gogpu/gg&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3D Library&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/g3d" rel="noopener noreferrer"&gt;github.com/gogpu/g3d&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shader Compiler&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;github.com/gogpu/naga&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Previous: DXIL Article&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/kolkov/we-built-the-first-pure-go-dxil-generator-because-optimizing-the-wrong-path-wasnt-enough-35en"&gt;dev.to/kolkov/dxil&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Previous: 100K LOC Article&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/kolkov/gogpu-from-idea-to-100k-lines-in-two-weeks-building-gos-gpu-ecosystem-3b2"&gt;dev.to/kolkov/100k&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smart Coding Methodology&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/kolkov/from-vibe-coding-to-agentic-engineering-what-karpathy-got-right-and-whats-missing-62e"&gt;dev.to/kolkov/smart-coding&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Core Is Stable — Now We Need You
&lt;/h2&gt;

&lt;p&gt;The foundational libraries — &lt;code&gt;wgpu&lt;/code&gt;, &lt;code&gt;naga&lt;/code&gt;, &lt;code&gt;gg&lt;/code&gt;, &lt;code&gt;gogpu&lt;/code&gt; — have reached a level of stability where the focus is shifting. The WebGPU implementation handles Vulkan, DX12, Metal, GLES, and Software. The shader compiler generates 5 output formats. The 2D rasterizer passes thousands of tests. The windowing framework runs on 4 platforms with multi-window support.&lt;/p&gt;

&lt;p&gt;Now the main effort goes into &lt;strong&gt;ui&lt;/strong&gt; (the GUI toolkit), &lt;strong&gt;new ecosystem libraries&lt;/strong&gt; (g3d, compose, systray), and the &lt;strong&gt;Browser backend&lt;/strong&gt; (WASM + WebGPU — same app code running in Chrome). This is where things get exciting, and this is where we need feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it. Break it. Tell us what's missing.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Found a bug? &lt;a href="https://github.com/gogpu/gogpu/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; — we respond fast.&lt;/li&gt;
&lt;li&gt;Built something with GoGPU? Share it — we love showcasing community projects.&lt;/li&gt;
&lt;li&gt;Want to write about it? Articles and &lt;a href="https://www.youtube.com/watch?v=RDE2Mkr-B80" rel="noopener noreferrer"&gt;video reviews&lt;/a&gt; help the ecosystem grow more than anything else.&lt;/li&gt;
&lt;li&gt;Have ideas for the UI toolkit? &lt;a href="https://github.com/orgs/gogpu/discussions" rel="noopener noreferrer"&gt;Join the discussions&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go deserves a real graphics ecosystem. We're building it — and it's ready for you to start building on.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
