<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Fractals on Max Bonnefin</title><link>https://bonnef.in/tags/fractals/</link><description>Recent content in Fractals on Max Bonnefin</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>max@bonnef.in (Max Bonnefin)</managingEditor><webMaster>max@bonnef.in (Max Bonnefin)</webMaster><copyright>Max Bonnefin</copyright><lastBuildDate>Fri, 28 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://bonnef.in/tags/fractals/index.xml" rel="self" type="application/rss+xml"/><item><title>GPU Fractals in the Browser: From Goroutines to Kage Shaders via WASM</title><link>https://bonnef.in/posts/fractal-kage-shaders/</link><pubDate>Fri, 28 Aug 2026 00:00:00 +0000</pubDate><author>max@bonnef.in (Max Bonnefin)</author><guid>https://bonnef.in/posts/fractal-kage-shaders/</guid><description>&lt;p&gt;I had a working fractal renderer in Go. Three fractal types, interactive zoom and pan, targeting 800×600 at 60 fps. The rendering loop spawned 600 goroutines per frame, one per scanline, each iterating every pixel through the &lt;a href="https://en.wikipedia.org/wiki/Plotting_algorithms_for_the_Mandelbrot_set#Escape_time_algorithm" class="external-link" target="_blank" rel="noopener"&gt;escape-time algorithm&lt;/a&gt; and writing RGBA bytes into a shared buffer. It worked fine on the desktop. Then I compiled it to &lt;a href="https://webassembly.org/" class="external-link" target="_blank" rel="noopener"&gt;WebAssembly&lt;/a&gt; and those 600 goroutines became cooperative tasks on a single browser thread&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. The parallelism evaporated. Single-digit frame rates.&lt;/p&gt;
&lt;p&gt;Each pixel is independent, with no data dependencies between neighbouring pixels. This is exactly the kind of work a GPU is built for. &lt;a href="https://ebitengine.org/" class="external-link" target="_blank" rel="noopener"&gt;Ebiten&lt;/a&gt; has a shader language called Kage, so I moved the escape-time calculation there. The resulting pipeline is strange enough to be worth documenting: Go source compiles to WASM for the host logic, while Kage shaders compile at runtime to WebGL for the per-pixel work.&lt;/p&gt;
&lt;h2 id="the-cpu-version"&gt;
The CPU Version
&lt;a class="heading-link" href="#the-cpu-version"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The original render loop:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;var&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;wg&lt;span style="color:#6e7681"&gt; &lt;/span&gt;sync.WaitGroup&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;wg.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Add&lt;/span&gt;(h)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;for&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;y&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;y&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&amp;lt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;h;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;y&lt;span style="color:#ff7b72;font-weight:bold"&gt;++&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;go&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;(row&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt;)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;defer&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;wg.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Done&lt;/span&gt;()&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;for&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&amp;lt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;w;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;x&lt;span style="color:#ff7b72;font-weight:bold"&gt;++&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cx,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cy&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vp.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;ScreenToComplex&lt;/span&gt;(x,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;row)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;iter&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;ft.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Iterate&lt;/span&gt;(cx,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cy,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;maxIter)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;colormap.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Map&lt;/span&gt;(iter,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;maxIter)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;offset&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(row&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;w&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;x)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;4&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;pix[offset]&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c.R&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;pix[offset&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;1&lt;/span&gt;]&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c.G&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;pix[offset&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;2&lt;/span&gt;]&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c.B&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;pix[offset&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;3&lt;/span&gt;]&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c.A&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}(y)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;wg.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Wait&lt;/span&gt;()&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At 800×600, that is 480,000 independent calculations per frame. The cost of each one depends on how long its coordinate takes to escape, up to the configured iteration limit.&lt;/p&gt;
&lt;p&gt;I measured the renderer on my Framework 13 with an i7-1360P and Intel Iris Xe graphics. The native figures are the mean of three Go benchmark runs. The browser figures come from Chromium 149 over five seconds of alternating left and right input, which invalidates the CPU cache on every frame without walking away from the fractal.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Native CPU benchmark&lt;/th&gt;
&lt;th&gt;256 iterations&lt;/th&gt;
&lt;th&gt;1024 iterations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time per redraw&lt;/td&gt;
&lt;td&gt;~11.5 ms&lt;/td&gt;
&lt;td&gt;~36.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WASM browser frame rate&lt;/th&gt;
&lt;th&gt;256 iterations&lt;/th&gt;
&lt;th&gt;1024 iterations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;~19.8 fps&lt;/td&gt;
&lt;td&gt;~8.0 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU, scalar path&lt;/td&gt;
&lt;td&gt;60 fps&lt;/td&gt;
&lt;td&gt;60 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU, double-single at &lt;code&gt;1e-10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60 fps&lt;/td&gt;
&lt;td&gt;~42.8 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The browser is capped at 60 fps by display synchronisation, so those GPU rows describe visible frame cadence rather than shader execution time. Ebiten&amp;rsquo;s reported draw-call time only measures CPU submission because the GPU work completes asynchronously. The paired arithmetic is substantially more expensive at 1024 iterations, but it still leaves the deep-zoom renderer usable. Press &lt;code&gt;G&lt;/code&gt; in the embed to compare it with the CPU path.&lt;/p&gt;
&lt;h2 id="kage"&gt;
Kage
&lt;a class="heading-link" href="#kage"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Ebiten&amp;rsquo;s shader language is called &lt;a href="https://ebitengine.org/en/documents/shader.html" class="external-link" target="_blank" rel="noopener"&gt;Kage&lt;/a&gt;&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. It looks like Go but compiles to GLSL, HLSL, or MSL depending on the platform. On WASM, it becomes WebGL shader code.&lt;/p&gt;
&lt;p&gt;Kage has restrictions that bite you immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No dynamic loop bounds. The &lt;code&gt;for&lt;/code&gt; loop needs a hardcoded upper limit compiled into the shader.&lt;/li&gt;
&lt;li&gt;There is one floating-point type, &lt;code&gt;float&lt;/code&gt;, rather than separate float32 and float64 types. The Kage specification does not guarantee its precision; on the WebGL implementations I tested, it behaves as float32.&lt;/li&gt;
&lt;li&gt;No recursion.&lt;/li&gt;
&lt;li&gt;Host values such as viewport state enter through global uniform variables.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My shader uses Kage&amp;rsquo;s three-argument fragment entry point: &lt;code&gt;Fragment(dstPos vec4, srcPos vec2, color vec4) vec4&lt;/code&gt;. You get a pixel position and return a colour. The GPU schedules one invocation for each of the 480,000 pixels.&lt;/p&gt;
&lt;p&gt;The escape-time loop in Kage:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;for&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;i&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;i&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&amp;lt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;10000&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;i&lt;span style="color:#ff7b72;font-weight:bold"&gt;++&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;float(i)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;MaxIter&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;break&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx2&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;zy2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&amp;gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;4.0&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;escaped&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#79c0ff"&gt;true&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;break&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;2.0&lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;zx&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;zy&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cy&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cx&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;iterCount&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;1.0&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;10000&lt;/code&gt; is a compiled-in ceiling. &lt;code&gt;MaxIter&lt;/code&gt;, passed as a uniform, is the actual limit. The GPU compiles this into a fixed-iteration loop with early exit. Wasteful in theory; irrelevant in practice because the GPU is doing 480,000 of these simultaneously.&lt;/p&gt;
&lt;p&gt;For colouring, I use the &lt;a href="https://iquilezles.org/articles/msetsmooth/" class="external-link" target="_blank" rel="noopener"&gt;continuous iteration count&lt;/a&gt;: &lt;code&gt;iter + 1 - log2(log2(|z|²)) / 2&lt;/code&gt;. This maps escape iterations to a smooth float value rather than discrete integers, eliminating the harsh colour banding that makes cheap fractal renderers look cheap. The colour gradient uses &lt;a href="https://en.wikipedia.org/wiki/Bernstein_polynomial" class="external-link" target="_blank" rel="noopener"&gt;Bernstein polynomial&lt;/a&gt; basis functions, same formula as the CPU version so you can toggle between renderers mid-session without a visible jump.&lt;/p&gt;
&lt;h2 id="precision"&gt;
Precision
&lt;a class="heading-link" href="#precision"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The CPU renderer uses float64 throughout. The shared viewport clamps &lt;code&gt;MinZoom&lt;/code&gt; at &lt;code&gt;1e-14&lt;/code&gt;, where the visible coordinate range spans fourteen decimal places. Float64 has 15-16 significant digits, so the CPU path retains enough precision.&lt;/p&gt;
&lt;p&gt;On the WebGL implementations I tested, Kage&amp;rsquo;s &lt;code&gt;float&lt;/code&gt; behaves as float32: about seven significant digits. Pass the viewport centre as a single float uniform, compute each pixel&amp;rsquo;s coordinate by adding a sub-pixel offset and once you zoom past about &lt;code&gt;1e-6&lt;/code&gt;, adjacent pixels begin mapping to the same value. The fractal freezes into a static grid. You&amp;rsquo;re zooming into a point the shader can no longer subdivide.&lt;/p&gt;
&lt;h3 id="double-single-emulation"&gt;
Double-Single Emulation
&lt;a class="heading-link" href="#double-single-emulation"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;I split each float64 viewport coordinate into two float32 values on the Go side.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Split&lt;/span&gt;(value&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;float64&lt;/span&gt;)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(high,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;low&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;float32&lt;/span&gt;)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;high&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;float32(value)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;low&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;float32(value&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;float64(high))&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;high,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;low&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;high&lt;/code&gt; is the float32 approximation. &lt;code&gt;low&lt;/code&gt; captures the remainder lost in conversion. Within the viewport&amp;rsquo;s coordinate range, reconstructing the pair in float64 stays within &lt;code&gt;1e-15&lt;/code&gt; of the original value.&lt;/p&gt;
&lt;p&gt;Keeping that precision in the shader requires every operation to return another pair. I represent a double-single number as a &lt;code&gt;vec2&lt;/code&gt;, with the high component in &lt;code&gt;x&lt;/code&gt; and the low component in &lt;code&gt;y&lt;/code&gt;. Addition uses an error-free transform and renormalises the result instead of collapsing it back to one float:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsNormalize&lt;/span&gt;(hi,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;lo&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;float&lt;/span&gt;)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;s&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;hi&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;lo&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;e&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;lo&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(s&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;hi)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(s,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;e)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsAdd&lt;/span&gt;(a,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;s&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b.x&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;v&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;s&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.x&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;e&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(a.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(s&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;v))&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(b.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;v)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.y&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b.y&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsNormalize&lt;/span&gt;(s,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;e)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Multiplication needs the same treatment. The constant &lt;code&gt;4097&lt;/code&gt; is &lt;code&gt;2¹² + 1&lt;/code&gt;, which splits a float32 significand into two parts so the rounding error from &lt;code&gt;a.x * b.x&lt;/code&gt; can be recovered.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsMul&lt;/span&gt;(a,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;p&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b.x&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aCon&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;4097.0&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aHi&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aCon&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(aCon&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.x)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aLo&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aHi&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;bCon&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;4097.0&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;bHi&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;bCon&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(bCon&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b.x)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;bLo&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;bHi&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;e&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;((aHi&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;bHi&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;p)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aHi&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;bLo&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aLo&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;bHi)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aLo&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;bLo&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;e&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.x&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;b.y&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.y&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;b.x&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;a.y&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;b.y&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsNormalize&lt;/span&gt;(p,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;e)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsSub&lt;/span&gt;(a,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsAdd&lt;/span&gt;(a,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;b.x,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;b.y))&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsScale&lt;/span&gt;(a&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;b&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;float&lt;/span&gt;)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;vec2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsMul&lt;/span&gt;(a,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(b,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;))&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The deep-zoom path keeps &lt;code&gt;cx&lt;/code&gt;, &lt;code&gt;cy&lt;/code&gt;, &lt;code&gt;zx&lt;/code&gt; and &lt;code&gt;zy&lt;/code&gt; as pairs through the entire escape loop. Converting back to a scalar before iteration would throw away the extra precision I had just recovered.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cx&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsAdd&lt;/span&gt;(&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(CenterHigh.x,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;CenterLow.x),&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(offsetX,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;))&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cy&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsAdd&lt;/span&gt;(&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(CenterHigh.y,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;CenterLow.y),&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(offsetY,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;))&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;zx&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;zy&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;vec2&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;for&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;i&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;0&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;i&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&amp;lt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;10000&lt;/span&gt;;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;i&lt;span style="color:#ff7b72;font-weight:bold"&gt;++&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsMul&lt;/span&gt;(zx,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy2&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsMul&lt;/span&gt;(zy,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx2.x&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;zx2.y&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;zy2.x&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;zy2.y&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&amp;gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;4.0&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;break&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsAdd&lt;/span&gt;(&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsScale&lt;/span&gt;(&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsMul&lt;/span&gt;(zx,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy),&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;2.0&lt;/span&gt;),&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cy)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zx&lt;span style="color:#6e7681"&gt; &lt;/span&gt;=&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsAdd&lt;/span&gt;(&lt;span style="color:#d2a8ff;font-weight:bold"&gt;dsSub&lt;/span&gt;(zx2,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;zy2),&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cx)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The shader only activates this more expensive path below &lt;code&gt;3.5e-7&lt;/code&gt;. At 800 pixels wide, that is roughly where the per-pixel coordinate delta starts approaching float32&amp;rsquo;s least significant bit. Above this zoom level, scalar arithmetic is faster and precise enough. Below it, the high/low pairs keep adjacent pixels distinct.&lt;/p&gt;
&lt;p&gt;The practical limit lands around &lt;code&gt;1e-10&lt;/code&gt; before artefacts creep back in. Not as deep as the CPU&amp;rsquo;s &lt;code&gt;1e-14&lt;/code&gt;, but enough for this viewer.&lt;/p&gt;
&lt;h2 id="wiring-the-renderer"&gt;
Wiring the Renderer
&lt;a class="heading-link" href="#wiring-the-renderer"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Ebiten can&amp;rsquo;t compile shaders until an active graphics context exists, which means the first frame. Try to compile during &lt;code&gt;init()&lt;/code&gt; or &lt;code&gt;NewGame()&lt;/code&gt; and you get a nil pointer deep in the OpenGL backend. The GPU renderer lazy-initialises on the first &lt;code&gt;Update()&lt;/code&gt; call, falls back to the CPU path if compilation fails, and from there passes viewport state as uniforms each draw call:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(sr&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;ShaderRenderer)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Draw&lt;/span&gt;(screen&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;ebiten.Image,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;opts&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;RenderOpts)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cxHi,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cxLo&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;precision.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Split&lt;/span&gt;(opts.CenterX)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cyHi,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cyLo&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;precision.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Split&lt;/span&gt;(opts.CenterY)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;uniforms&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;map&lt;/span&gt;[&lt;span style="color:#ff7b72"&gt;string&lt;/span&gt;]&lt;span style="color:#ff7b72"&gt;interface&lt;/span&gt;{}{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;CenterHigh&amp;#34;&lt;/span&gt;:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;[]&lt;span style="color:#ff7b72"&gt;float32&lt;/span&gt;{cxHi,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cyHi},&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;CenterLow&amp;#34;&lt;/span&gt;:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;[]&lt;span style="color:#ff7b72"&gt;float32&lt;/span&gt;{cxLo,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;cyLo},&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;Zoom&amp;#34;&lt;/span&gt;:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;float32(opts.Zoom),&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;MaxIter&amp;#34;&lt;/span&gt;:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;float32(opts.MaxIter),&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;Resolution&amp;#34;&lt;/span&gt;:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;[]&lt;span style="color:#ff7b72"&gt;float32&lt;/span&gt;{float32(opts.Width),&lt;span style="color:#6e7681"&gt; &lt;/span&gt;float32(opts.Height)},&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;drawOpts&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;ebiten.DrawRectShaderOptions{Uniforms:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;uniforms}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;screen.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;DrawRectShader&lt;/span&gt;(opts.Width,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;opts.Height,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;sr.shaders[sr.currentIndex],&lt;span style="color:#6e7681"&gt; &lt;/span&gt;drawOpts)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No pixel buffer. No goroutines. No &lt;code&gt;sync.WaitGroup&lt;/code&gt;. One &lt;code&gt;DrawRectShader&lt;/code&gt; call and the GPU handles the rest.&lt;/p&gt;
&lt;h2 id="shipping-it"&gt;
Shipping It
&lt;a class="heading-link" href="#shipping-it"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#d2a8ff;font-weight:bold"&gt;wasm&lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#79c0ff"&gt;GOOS&lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;js &lt;span style="color:#79c0ff"&gt;GOARCH&lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;wasm go build -ldflags&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;&amp;#34;-s -w&amp;#34;&lt;/span&gt; -o web/fractal.wasm ./cmd/fractal/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cp -f &lt;span style="color:#a5d6ff"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#79c0ff"&gt;$$&lt;/span&gt;&lt;span style="color:#a5d6ff"&gt;(go env GOROOT)/lib/wasm/wasm_exec.js&amp;#34;&lt;/span&gt; web/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The resulting binary is 12 MB. That&amp;rsquo;s &lt;a href="https://go.dev/wiki/WebAssembly" class="external-link" target="_blank" rel="noopener"&gt;Go&amp;rsquo;s WASM story&lt;/a&gt;: the entire runtime, garbage collector and goroutine scheduler ship in the binary regardless of whether you use them. The &lt;code&gt;-s -w&lt;/code&gt; flags strip symbols and DWARF, which saves about 3 MB compared to a debug build. The deploy gzips it to just under 3 MB over the wire. Lazy loading is not optional at that size.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;wasm_exec.js&lt;/code&gt; glue file must come from the same Go toolchain release that compiled the binary. A version mismatch produces silent failures or cryptic &lt;code&gt;Go.run&lt;/code&gt; errors with no useful stack trace. Ask me how I know.&lt;/p&gt;
&lt;p&gt;Ebiten&amp;rsquo;s WASM target manages its own canvas element, which makes embedding it inside a blog post slightly awkward. It insists on owning the full document. I therefore run the viewer in a minimal iframe rather than letting it take over the article. This is document containment, not a security boundary: the viewer is code I built and serve from the same origin, so the site trusts it in the same way it trusts its other JavaScript. &lt;a href="https://ebitengine.org/en/examples/" class="external-link" target="_blank" rel="noopener"&gt;Ebiten&amp;rsquo;s own example gallery&lt;/a&gt; also uses iframes to contain its demos.&lt;/p&gt;
&lt;p&gt;The embed is lazy-loaded behind a click. A 12 MB binary is too much to fetch speculatively on page scroll.&lt;/p&gt;
&lt;h2 id="try-it"&gt;
Try It
&lt;a class="heading-link" href="#try-it"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Click inside the viewer to give it keyboard focus. Scroll to zoom, drag to pan and press &lt;code&gt;G&lt;/code&gt; to switch between the CPU and GPU renderers.&lt;/p&gt;
&lt;div class="wasm-container" id="wasm-0-fractalwasm-container" style="max-width: 800px;"&gt;
&lt;div class="wasm-placeholder" id="wasm-0-fractalwasm-placeholder"&gt;
&lt;button type="button" class="wasm-load-btn" id="wasm-0-fractalwasm-btn" aria-describedby="wasm-0-fractalwasm-size"&gt;
&lt;svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;polygon points="5 3 19 12 5 21 5 3"&gt;&lt;/polygon&gt;
&lt;/svg&gt;
&lt;span&gt;Load interactive fractal viewer&lt;/span&gt;
&lt;span class="wasm-size-hint" id="wasm-0-fractalwasm-size"&gt;Approximately 12 MB&lt;/span&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="wasm-loading wasm-hidden" id="wasm-0-fractalwasm-loading" role="status" aria-live="polite" aria-atomic="true"&gt;
&lt;div class="wasm-spinner" aria-hidden="true"&gt;&lt;/div&gt;
&lt;span&gt;Loading WebAssembly&amp;hellip;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="wasm-error-state wasm-hidden" id="wasm-0-fractalwasm-error" role="alert"&gt;
&lt;p id="wasm-0-fractalwasm-error-message"&gt;The interactive viewer could not be loaded.&lt;/p&gt;
&lt;button type="button" class="wasm-retry-btn" id="wasm-0-fractalwasm-retry"&gt;Try again&lt;/button&gt;
&lt;/div&gt;
&lt;iframe class="wasm-iframe wasm-hidden" id="wasm-0-fractalwasm-iframe"
width="800" height="600"
title="Interactive fractal viewer"
tabindex="-1"
style="aspect-ratio: 800 / 600;"
&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;script&gt;
(function() {
var containerId = "wasm-0-fractalwasm";
var hostUrl = "\/wasm\/index.html?src=fractal.wasm";
var btn = document.getElementById(containerId + '-btn');
var retry = document.getElementById(containerId + '-retry');
var placeholder = document.getElementById(containerId + '-placeholder');
var loading = document.getElementById(containerId + '-loading');
var error = document.getElementById(containerId + '-error');
var errorMessage = document.getElementById(containerId + '-error-message');
var iframe = document.getElementById(containerId + '-iframe');
var timeoutId;
function showError(message) {
window.clearTimeout(timeoutId);
loading.classList.add('wasm-hidden');
iframe.classList.add('wasm-hidden');
iframe.setAttribute('tabindex', '-1');
errorMessage.textContent = message || 'The interactive viewer could not be loaded.';
error.classList.remove('wasm-hidden');
retry.focus();
}
function handleMessage(event) {
if (event.source !== iframe.contentWindow || event.origin !== window.location.origin) {
return;
}
if (!event.data || event.data.source !== 'wasm-canvas') {
return;
}
if (event.data.type === 'ready') {
window.clearTimeout(timeoutId);
loading.classList.add('wasm-hidden');
iframe.classList.remove('wasm-hidden');
iframe.setAttribute('tabindex', '0');
iframe.focus();
} else if (event.data.type === 'error') {
showError(event.data.message);
}
}
function loadViewer() {
placeholder.classList.add('wasm-hidden');
error.classList.add('wasm-hidden');
iframe.classList.add('wasm-hidden');
loading.classList.remove('wasm-hidden');
iframe.src = hostUrl + '&amp;attempt=' + Date.now();
timeoutId = window.setTimeout(function() {
showError('The interactive viewer took too long to start.');
}, 30000);
}
window.addEventListener('message', handleMessage);
btn.addEventListener('click', loadViewer);
retry.addEventListener('click', loadViewer);
})();
&lt;/script&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Go&amp;rsquo;s WASM target runs goroutines cooperatively on a single OS thread. There&amp;rsquo;s no true parallelism; goroutines yield at scheduling points but never run simultaneously. See the &lt;a href="https://go.dev/wiki/WebAssembly" class="external-link" target="_blank" rel="noopener"&gt;Go WebAssembly wiki&lt;/a&gt; for details on the execution model.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://ebitengine.org/en/documents/shader.html" class="external-link" target="_blank" rel="noopener"&gt;Kage documentation&lt;/a&gt;. Kage (影) means &amp;ldquo;shadow&amp;rdquo; in Japanese.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;The paired arithmetic uses Dekker splitting and Knuth&amp;rsquo;s two-sum algorithm. See Dekker, T.J. (1971) &amp;ldquo;A floating-point technique for extending the available precision&amp;rdquo; and Priest, D.M. (1991) &amp;ldquo;Algorithms for arbitrary precision floating point arithmetic.&amp;rdquo;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item></channel></rss>