<?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: GoGPU</title>
    <description>The latest articles on DEV Community by GoGPU (gogpu).</description>
    <link>https://dev.to/gogpu</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%2Forganization%2Fprofile_image%2F14665%2F661f8b38-f28c-40ff-8f0e-6ba52eabfddd.png</url>
      <title>DEV Community: GoGPU</title>
      <link>https://dev.to/gogpu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gogpu"/>
    <language>en</language>
    <item>
      <title>A Shader Loop That Never Ran: What GPU Readback Found in gogpu/naga, a Go WGSL Compiler</title>
      <dc:creator>Andrey Kolkov</dc:creator>
      <pubDate>Tue, 03 Nov 2026 17:07:23 +0000</pubDate>
      <link>https://dev.to/gogpu/a-shader-loop-that-never-ran-what-gpu-readback-found-in-gogpunaga-a-go-wgsl-compiler-41ja</link>
      <guid>https://dev.to/gogpu/a-shader-loop-that-never-ran-what-gpu-readback-found-in-gogpunaga-a-go-wgsl-compiler-41ja</guid>
      <description>&lt;p&gt;Here's a WGSL loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (var j = 0u; j &amp;lt; 4u; j++) {
    result[j] = j + 1u;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Until v0.20.3 in October, &lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;naga&lt;/a&gt;, our shader compiler written in Go, turned it into DXIL that never ran the loop. Microsoft's validator looked at the module and said it was valid. On an Intel and an NVIDIA GPU the loop body didn't execute once.&lt;/p&gt;

&lt;p&gt;The validator wasn't wrong. That's the interesting part.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the validator saw
&lt;/h2&gt;

&lt;p&gt;This is the DXIL that naga v0.20.2 writes for that loop, trimmed to the lines that matter, with my comments on the right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight llvm"&gt;&lt;code&gt;  &lt;span class="nv"&gt;%2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;alloca&lt;/span&gt; &lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;align&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;           &lt;span class="c1"&gt;; var j&lt;/span&gt;
  &lt;span class="k"&gt;br&lt;/span&gt; &lt;span class="kt"&gt;label&lt;/span&gt; &lt;span class="nv"&gt;%3&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nv"&gt;%5&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;load&lt;/span&gt; &lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;%2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;align&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;    &lt;span class="c1"&gt;; read j for "j &amp;lt; 4"&lt;/span&gt;
  &lt;span class="nv"&gt;%6&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;icmp&lt;/span&gt; &lt;span class="k"&gt;ult&lt;/span&gt; &lt;span class="kt"&gt;i32&lt;/span&gt; &lt;span class="nv"&gt;%5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="k"&gt;br&lt;/span&gt; &lt;span class="kt"&gt;i1&lt;/span&gt; &lt;span class="nv"&gt;%6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;label&lt;/span&gt; &lt;span class="nv"&gt;%11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;label&lt;/span&gt; &lt;span class="nv"&gt;%12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The variable &lt;code&gt;j&lt;/code&gt; gets memory, and the very first thing the loop does is read it. Nothing ever stored the &lt;code&gt;0&lt;/code&gt;. Reading uninitialized memory is legal in LLVM IR, so every instruction here is well formed, the types match, the control flow is structured. IDxcValidator checks exactly those things and reports &lt;code&gt;VALID (S_OK)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What it doesn't check is whether the module computes what the source says. No static validator does, not DXC's and not spirv-val. They answer "can a driver run this?", and the answer was yes. The driver ran it, &lt;code&gt;j&lt;/code&gt; started as whatever was in that memory, and the loop exited before its first iteration.&lt;/p&gt;

&lt;p&gt;naga v0.20.3 fixed three bugs of this kind in the DXIL backend:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The shader&lt;/th&gt;
&lt;th&gt;What the DXIL did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;for (var j = 0u; j &amp;lt; 4u; j++)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;never stored the initializer; the loop didn't run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;for (var j = i; j &amp;lt; n; j++)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an optimization pass deleted the initial store, then noticed the loop also writes &lt;code&gt;j&lt;/code&gt;; the loop started from an uninitialized &lt;code&gt;j&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;v[i] = v[i] + 1u&lt;/code&gt; on a local vector&lt;/td&gt;
&lt;td&gt;a run-time index always picked &lt;code&gt;v.x&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For this article I compiled all three shaders with v0.20.2 again. All three come back &lt;code&gt;VALID (S_OK)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our own tests missed them too. We compare naga's text output with Rust naga's and its DXIL with DXC's, and none of that asks what the shader computes. What found them was a one-off experiment: run the shaders on real GPUs through Direct3D 12 and read the buffers back.&lt;/p&gt;




&lt;h2&gt;
  
  
  What naga is
&lt;/h2&gt;

&lt;p&gt;naga reads WGSL, the WebGPU shading language, and writes SPIR-V for Vulkan, MSL for Metal, GLSL for OpenGL, HLSL for Direct3D, and DXIL for Direct3D 12. The DXIL is emitted directly, without DXC or LLVM. It's plain Go with no CGO, so it cross-compiles like any other Go package.&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;"log"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/gogpu/naga/dxil"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/gogpu/naga/hlsl"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/gogpu/naga/msl"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/gogpu/naga/spirv"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/gogpu/naga/valid"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/gogpu/naga/wgsl"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;`
@group(0) @binding(0) var&amp;lt;storage, read_write&amp;gt; result: array&amp;lt;u32&amp;gt;;

@compute @workgroup_size(1)
fn main() {
    for (var j = 0u; j &amp;lt; 4u; j++) {
        result[j] = j + 1u;
    }
}
`&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;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&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;wgsl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wgsl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseOptions&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;check&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="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;valid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;check&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="n"&gt;spv&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spirv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spirv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{}))&lt;/span&gt;   &lt;span class="c"&gt;// Vulkan&lt;/span&gt;
    &lt;span class="n"&gt;metal&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{}))&lt;/span&gt;     &lt;span class="c"&gt;// Metal&lt;/span&gt;
    &lt;span class="n"&gt;hlslSrc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hlsl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hlsl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{}))&lt;/span&gt; &lt;span class="c"&gt;// Direct3D, through FXC or DXC&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dxil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dxil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{}))&lt;/span&gt;    &lt;span class="c"&gt;// Direct3D 12, no DXC needed&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;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SPIR-V %d bytes, MSL %d bytes, HLSL %d bytes, DXIL %d bytes&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&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;spv&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;metal&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;hlslSrc&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;blob&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;metal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// must returns a backend's output and stops on its error.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&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;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;check&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&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;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&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;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With naga v0.21.2 it prints &lt;code&gt;SPIR-V 1240 bytes, MSL 917 bytes, HLSL 667 bytes, DXIL 2960 bytes&lt;/code&gt;, then the Metal source. The zero value of every options struct is the safe choice: bounds checks are on and workgroup memory is zeroed. The SPIR-V, MSL and HLSL writers also give every loop an iteration limit, because a loop the driver's compiler can prove never ends is undefined behavior, and the compiler is then free to delete it, bounds checks included. Two of those show up in the Metal output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;while(true) {
    if (metal::all(loop_bound == uint2(0u))) { break; }
    ...
    result[metal::min(unsigned(_e6), (_buffer_sizes.size0 - 0 - 4) / 4)] = _e8 + 1u;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep an eye on that &lt;code&gt;metal::min&lt;/code&gt;. It's a fix from this month, and it comes back later.&lt;/p&gt;

&lt;p&gt;naga takes its IR and much of its design from &lt;a href="https://github.com/gfx-rs/wgpu/tree/trunk/naga" rel="noopener noreferrer"&gt;Rust naga&lt;/a&gt;, the shader compiler of Rust wgpu, which is the core of WebGPU in Firefox, Servo and Deno. We run Rust naga's 172-shader WGSL test corpus, compare our output with Rust's wherever Rust writes the same format (SPIR-V only structurally so far), and record every difference with its reason. Rust naga has no DXIL backend, so for DXIL the references are DXC and Mesa. naga's main user is &lt;a href="https://github.com/gogpu/wgpu" rel="noopener noreferrer"&gt;gogpu/wgpu&lt;/a&gt;, a WebGPU implementation in Go, which compiles its WGSL shaders with naga in its default pure-Go build.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the output instead
&lt;/h2&gt;

&lt;p&gt;After v0.20.3 we turned that experiment into a permanent test corpus: small compute shaders, each paired with the exact values a GPU has to write when it runs them. It lives in &lt;a href="https://github.com/gogpu/naga/tree/main/testdata/readback" rel="noopener noreferrer"&gt;&lt;code&gt;testdata/readback&lt;/code&gt;&lt;/a&gt; and has 55 cases today, mostly loops, function calls and local variables. Each case is a &lt;code&gt;.wgsl&lt;/code&gt; file and a &lt;code&gt;.json&lt;/code&gt; file:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"desc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"for-loop counter declared in the loop header writes 1..4; the sentinel after them stays"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workgroups"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bindings"&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storage-rw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"len"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fill"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0xDEADBEEF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"expect"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fill"&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="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;Three habits made the corpus useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sentinels.&lt;/strong&gt; Every output buffer starts filled with &lt;code&gt;0xDEADBEEF&lt;/code&gt;, and the expected values say where it has to survive. A loop that runs one time too many, or a store through the wrong index, shows up as a missing sentinel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inputs from buffers.&lt;/strong&gt; Most cases read their inputs from a storage buffer, so the compiler can't fold the work away at compile time, and a few, like &lt;code&gt;collatz-lit.wgsl&lt;/code&gt; and &lt;code&gt;collatz-rt.wgsl&lt;/code&gt;, test the same computation both ways. The WebGPU conformance test suite uses the same trick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Known failures with a card.&lt;/strong&gt; A case can list a backend it currently fails on, with the issue that tracks it. The run stays readable while the bug is open, and the entry goes away with the fix.&lt;/p&gt;

&lt;p&gt;naga can't drive a GPU itself, so the work is split. naga checks that every case is well formed and compiles on every backend, and runs spirv-val, glslangValidator, dxc and IDxcValidator on the output where they're installed. wgpu runs the corpus on its GPUs before naga releases that touch a backend, with a Go workspace (&lt;code&gt;go.work&lt;/code&gt;) pointing at the naga candidate, so a change runs on real hardware before it's tagged. That runner still lives outside the wgpu repository; moving it in is planned, and then anyone can run the corpus on their own GPU.&lt;/p&gt;

&lt;p&gt;For v0.21.1 the corpus ran on eight backend and adapter pairs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Adapters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vulkan&lt;/td&gt;
&lt;td&gt;Intel Iris Xe, NVIDIA CMP 30HX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct3D 12, HLSL through FXC&lt;/td&gt;
&lt;td&gt;Intel Iris Xe, NVIDIA GTX 1660 SUPER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct3D 12, naga's DXIL&lt;/td&gt;
&lt;td&gt;Intel Iris Xe, NVIDIA GTX 1660 SUPER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenGL ES&lt;/td&gt;
&lt;td&gt;Intel Iris Xe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wgpu's software backend&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There's no Mac in that list. Metal is covered by wgpu's own tests on a macOS CI runner, which is where the third bug below turned up.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it caught
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Local arrays that forgot their initializers.&lt;/strong&gt; wgpu found this one with a vertex shader that picks its corners from local arrays. The same shape, cut down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@vertex
fn vs(@builtin(vertex_index) v: u32) -&amp;gt; @builtin(position) vec4f {
    var xs = array&amp;lt;f32, 6&amp;gt;(-1.0, 1.0, -1.0, -1.0, 1.0, 1.0);
    var ys = array&amp;lt;f32, 6&amp;gt;(-1.0, -1.0, 1.0, 1.0, -1.0, 1.0);
    return vec4f(xs[v % 6u], ys[v % 6u], 0.0, 1.0);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On naga v0.21.0 the DXIL for this has two &lt;code&gt;alloca&lt;/code&gt; instructions and not a single &lt;code&gt;store&lt;/code&gt;, so every index reads uninitialized memory, and the validator again says &lt;code&gt;VALID (S_OK)&lt;/code&gt;. wgpu's shader drew nothing on Intel. The bug was there since at least v0.17.16. Since v0.21.1 naga stores the initializers: 12 stores for this shader, checked on v0.21.2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A loop-carried variable read as if the loop ran once.&lt;/strong&gt; naga's DXIL backend turns local variables into SSA values where it can. When every access to a variable sat in one block inside a loop, and that block read the variable before writing it, the pass treated the block as running only once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var x: u32;
for (var i = 0u; i &amp;lt; n; i++) {
    x += i + 1u;
    dst[i] = x;   // want 1, 3, 6, 10. DXIL wrote 1, 2, 3, 4.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every iteration saw the value from before the loop. This one shipped in every release since v0.17.4. The corpus case for it also checks shapes the old code got right, such as variables read after the loop or updated only in branches, so the fix can't quietly break those.&lt;/p&gt;

&lt;p&gt;The v0.21.1 run compared every result with the same run on v0.21.0. 54 of the 55 cases ran on each of the eight pairs (one needs &lt;code&gt;shader-f16&lt;/code&gt;, which the runner skipped), 432 results in all. Seven changed, all on DXIL, all from fail to pass:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend and adapter&lt;/th&gt;
&lt;th&gt;v0.21.0&lt;/th&gt;
&lt;th&gt;v0.21.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct3D 12 + naga's DXIL, Intel Iris Xe&lt;/td&gt;
&lt;td&gt;50 of 54 pass&lt;/td&gt;
&lt;td&gt;54 of 54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct3D 12 + naga's DXIL, GTX 1660 SUPER&lt;/td&gt;
&lt;td&gt;51 of 54 pass&lt;/td&gt;
&lt;td&gt;54 of 54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the other six pairs&lt;/td&gt;
&lt;td&gt;unchanged&lt;/td&gt;
&lt;td&gt;unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not everything in that run was green, and both versions failed the same cases: two workgroup-memory cases on the CMP 30HX under Vulkan (not diagnosed yet; Intel passes them), one known OpenGL ES failure, and 15 of the 54 in wgpu's software backend (11 failures, 4 already known). For a release gate that's the point: nothing got worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An out-of-bounds write on Metal.&lt;/strong&gt; wgpu has hand-written readback tests too. One of them, &lt;a href="https://github.com/gogpu/wgpu/blob/main/hal/allbackends/storage_oob_readback_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;TestStorageBufferOutOfBoundsContained&lt;/code&gt;&lt;/a&gt;, writes past the end of a storage binding and then checks that the binding next to it is untouched. WGSL allows an implementation to clamp such an access, and wgpu's Metal backend asks naga to clamp every index into a buffer.&lt;/p&gt;

&lt;p&gt;On the macOS CI runner the test failed. For a plain &lt;code&gt;var&amp;lt;storage&amp;gt; a: array&amp;lt;u32&amp;gt;&lt;/code&gt;, naga's MSL used the index as it was, and the write landed in the next binding. Only a runtime-sized array at the end of a struct got clamped. v0.21.2 clamps every index into a runtime-sized array; that's the &lt;code&gt;metal::min&lt;/code&gt; from the output above. The unclamped MSL was perfectly valid MSL.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Rust naga and the spec disagree
&lt;/h2&gt;

&lt;p&gt;While fixing the Metal bug I noticed that our bound was computed from the array's stride and Rust naga's from the element's size. I switched to Rust's form and called ours a defect.&lt;/p&gt;

&lt;p&gt;It wasn't. WGSL defines how many elements a runtime-sized array has, and calls it &lt;a href="https://www.w3.org/TR/WGSL/#nruntime" rel="noopener noreferrer"&gt;NRuntime&lt;/a&gt;: the buffer binding's size minus the array's offset, divided by the stride, rounded down. &lt;code&gt;arrayLength&lt;/code&gt; must return exactly that.&lt;/p&gt;

&lt;p&gt;The two forms differ when an element is smaller than its stride and the binding ends partway through the last stride. Take &lt;code&gt;array&amp;lt;vec3&amp;lt;f32&amp;gt;&amp;gt;&lt;/code&gt;: each element is 12 bytes and the stride is 16. On a 28-byte binding the spec says there is &lt;code&gt;truncate(28 / 16) = 1&lt;/code&gt; element. Rust naga's MSL computes &lt;code&gt;1 + (28 - 12) / 16 = 2&lt;/code&gt;, because a second &lt;code&gt;vec3&lt;/code&gt; would fit in bytes 16 to 27. That's true about memory and wrong about WGSL.&lt;/p&gt;

&lt;p&gt;The correction came from the wgpu side, and wgpu's &lt;code&gt;vec3&lt;/code&gt; test now pins it: its comment quotes the spec's NRuntime and requires &lt;code&gt;arrayLength&lt;/code&gt; to follow it. v0.21.2 uses the spec's count for both the clamp and &lt;code&gt;arrayLength&lt;/code&gt;, which is what naga's HLSL, DXIL, GLSL and SPIR-V output already did. On the macOS CI runner, a 28-byte &lt;code&gt;array&amp;lt;vec3&amp;lt;u32&amp;gt;&amp;gt;&lt;/code&gt; binding now reads &lt;code&gt;arrayLength&lt;/code&gt; back as 1 (&lt;a href="https://github.com/gogpu/naga/pull/151#issuecomment-6591629882" rel="noopener noreferrer"&gt;run result&lt;/a&gt;). The difference from Rust is written down in the code, in the tests that pin the bound, and in the CHANGELOG.&lt;/p&gt;

&lt;p&gt;So this is how we decide now. Where the spec defines a value or an error, the spec decides. Rust naga decides what the spec leaves open, like the shape of the IR or the wording of an error message. When we differ from Rust, we assume the bug is ours until we've read the spec. After that it might be theirs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Run the output from the first day of a backend.&lt;/strong&gt; We had validators, golden files and Rust parity long before we had a GPU in the loop. All three check that the output looks right. Only running it checks that it is right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read a green validator as "legal", not "correct".&lt;/strong&gt; It's a gate worth having, and it says nothing about values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the expected values hard to fake.&lt;/strong&gt; Sentinels in every slot that shouldn't change, inputs from buffers, and several shapes per bug, including the ones the old code already handled.&lt;/p&gt;




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

&lt;p&gt;DXIL is opt-in in wgpu (&lt;code&gt;GOGPU_DX12_DXIL=1&lt;/code&gt;). The default Direct3D 12 path is HLSL through FXC, so DX12 users on that path were never affected by the DXIL bugs above.&lt;/p&gt;

&lt;p&gt;Before we call DXIL stable, the shaders of gg, our 2D graphics library, have to pass the validator, readback has to be green on Intel and NVIDIA, and no silent bug may be known. Two are known and open right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more shapes of the local-initializer bug: indexed vectors, arrays of arrays, zero-initialized arrays of vectors, and struct or array variables of a helper function called inside a loop;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;primitive_index&lt;/code&gt; reads wrong in fragment shaders on Intel, probably because the DXIL doesn't mark fragment system values as non-interpolated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If naga compiles one of your shaders wrong, the &lt;a href="https://github.com/gogpu/naga/tree/main/testdata/readback" rel="noopener noreferrer"&gt;corpus format&lt;/a&gt; is documented. A WGSL file plus the values it should produce is the most useful bug report we can get.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;naga&lt;/strong&gt;: &lt;a href="https://github.com/gogpu/naga" rel="noopener noreferrer"&gt;github.com/gogpu/naga&lt;/a&gt; · &lt;a href="https://pkg.go.dev/github.com/gogpu/naga@v0.21.2" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;a href="https://go.dev/blog/gopher" rel="noopener noreferrer"&gt;Go gopher&lt;/a&gt; was designed by Renée French and is licensed under &lt;a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener noreferrer"&gt;CC BY 4.0&lt;/a&gt;.&lt;/em&gt;&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/naga@v0.21.2
go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/gogpu/naga/cmd/nagac@latest
nagac &lt;span class="nt"&gt;-o&lt;/span&gt; shader.spv shader.wgsl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>go</category>
      <category>testing</category>
      <category>gamedev</category>
      <category>opensource</category>
    </item>
    <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/gogpu/gogpuui-v0154-we-built-flutters-gesture-system-in-pure-go-arena-recognizers-and-all-31bl</link>
      <guid>https://dev.to/gogpu/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>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/gogpu/gogpu-extracting-a-gpu-compositor-in-pure-go-patterns-from-flutter-and-chromium-9ig</link>
      <guid>https://dev.to/gogpu/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/gogpu/we-built-a-pure-go-3d-renderer-then-embedded-it-inside-a-gui-widget-4mo4</link>
      <guid>https://dev.to/gogpu/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>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/gogpu/gogpucompose-multi-process-gui-composition-in-pure-go-387i</link>
      <guid>https://dev.to/gogpu/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/gogpu/gogpuui-v0121-enterprise-render-pipeline-layer-tree-damage-tracking-0-gpu-idle-5adm</link>
      <guid>https://dev.to/gogpu/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>
  </channel>
</rss>
