<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Casey Primozic&#39;s Notes</title>
    <link>https://cprimozic.net/notes/</link>
    <description>Recent content on Casey Primozic&#39;s Notes</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Fri, 24 Apr 2026 14:44:30 -0500</lastBuildDate><atom:link href="https://cprimozic.net/notes/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>WebGL `blitFramebuffer` Performance Issues on M3 Mac</title>
      <link>https://cprimozic.net/notes/posts/webgl-blit-framebuffer-performance-investigation-m3-mac/</link>
      <pubDate>Fri, 24 Apr 2026 14:44:30 -0500</pubDate>
      <guid>https://cprimozic.net/notes/posts/webgl-blit-framebuffer-performance-investigation-m3-mac/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;background&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve recently been working on some procedural background systems for a &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;browser-based game&lt;/a&gt; I&amp;rsquo;ve been working on.&lt;/p&gt;
&lt;p&gt;The idea was to create a sort of Shadertoy-style 100% per-fragment-procedural background. The background is what displays when there&amp;rsquo;s no other actual scene object/mesh at a given pixel on the screen. So, in order to work efficiently, this pass reads from the scene depth buffer and skips all of its relatively expensive procedural logic in the case that the background won&amp;rsquo;t be visible anyway.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;background&#34;&gt;background&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve recently been working on some procedural background systems for a &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;browser-based game&lt;/a&gt; I&amp;rsquo;ve been working on.&lt;/p&gt;
&lt;p&gt;The idea was to create a sort of Shadertoy-style 100% per-fragment-procedural background. The background is what displays when there&amp;rsquo;s no other actual scene object/mesh at a given pixel on the screen. So, in order to work efficiently, this pass reads from the scene depth buffer and skips all of its relatively expensive procedural logic in the case that the background won&amp;rsquo;t be visible anyway.&lt;/p&gt;
&lt;h2 id=&#34;investigation&#34;&gt;investigation&lt;/h2&gt;
&lt;p&gt;When I was testing out the procedural background system in a live scene with other effects and postprocessing, I was noticing that there were some pretty significant performance dips. It wasn&amp;rsquo;t unplayable or anything, but my FPS dropped from a stable 120 to ~80-90 and felt somewhat jittery/hitchy as well.&lt;/p&gt;
&lt;p&gt;My first thought was that the procedural shader stuff in the background was too heavy. I started trimming different pieces and optimizing the shader code as much as I could, but nothing had any effect.&lt;/p&gt;
&lt;p&gt;At some point, I added an early return at the beginning of all the procedural stuff, essentially making the shader just return pure black. Even after doing that, the FPS didn&amp;rsquo;t move at all!&lt;/p&gt;
&lt;p&gt;This made me realize that the perf regression wasn&amp;rsquo;t related to the fancy background shader at all but instead was something about the plumbing and the way the procedural background fit into the rendering pipeline and interacted with other passes.&lt;/p&gt;
&lt;h2 id=&#34;framebuffer-blit-performance-issues&#34;&gt;framebuffer blit performance issues&lt;/h2&gt;
&lt;div class=&#34;note&#34;&gt;
After some trial and error, I discovered that the framebuffer blits I was doing to copy scene depth in and copy the procedural background output out were responsible for the vast majority of the perf regression.
&lt;/div&gt;
&lt;p&gt;This was surprising to me. I was previously under the impression that framebuffer blits were relative cheap since they were specialized and heavily-optimized hardware-level routines. This finding challenged that assumption.&lt;/p&gt;
&lt;p&gt;To be fair, I was doing a pretty high amount of blits, definitely more than necessary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 to copy the scene depth in (I&amp;rsquo;ve dealt with a lot of issues with recursive depth texture bindings in the past, so copying it to a fresh buffer was a conservative safety measure)&lt;/li&gt;
&lt;li&gt;2 to copy the output of the procedural background shader back to main scene color output buffers (my rendering pipeline uses two color buffers to differentiate normal diffuse color and special bloomed emissive colors)&lt;/li&gt;
&lt;li&gt;1 to sync the depth out to the emissive render target&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One interesting thing about these blits is that they didn&amp;rsquo;t seem to actually cause any measurable load that I could track in the devtools performance tab, even in the GPU category. It&amp;rsquo;s definitely possible that the GPU profiling just wasn&amp;rsquo;t able to accurately track that.&lt;/p&gt;
&lt;p&gt;However, another possibility is that the blits were especially expensive due to the hardware of the computer I was testing on (M3 Mac). The M3 GPU uses tile-based deferred rendering (TBDR) which has some different performance characteristics then other platforms like discrete GPUs on desktops.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible that the framebuffer blits were acting as synchronization points for the pipeline, reducing hardware utilization or causing frame pacing issues. I&amp;rsquo;m not an expert at all on low-level hardware stuff like this, so take that assessment with a grain of salt.&lt;/p&gt;
&lt;h2 id=&#34;avoiding-the-blits&#34;&gt;avoiding the blits&lt;/h2&gt;
&lt;p&gt;The main trick I used to remove the need for the two color blits was to do some hacky manipulation of the Three.JS render targets to just bind the existing output buffers directly as the color attachments of the background pass&amp;rsquo;s MRT rather than copying into them after the fact.&lt;/p&gt;
&lt;p&gt;For the depth-out blit, I used a slightly different technique: rather than copying depth into the emissive render target each frame, I aliased the scene depth texture directly as the emissive RT&amp;rsquo;s depth attachment so they share the same underlying texture.&lt;/p&gt;
&lt;p&gt;Together these got rid of three of the four blits; the one that remains is the initial copy of scene depth into a standalone buffer, which needs to stay in order to prevent the recursive depth texture binding problem on the main effect composer.&lt;/p&gt;
&lt;p&gt;This all required doing some raw WebGL operations and bypassing Three.JS for a few things. It needed to be done carefully to avoid putting the WebGL pipeline or Three.JS into a bad state.&lt;/p&gt;
&lt;p&gt;I also made some additional small optimizations like only calling &lt;code&gt;framebufferTexture2D&lt;/code&gt; to re-bind textures to the framebuffer in the case that the textures actually changed.&lt;/p&gt;
&lt;div class=&#34;good&#34;&gt;
As a result of all these changes, I was able to almost entirely mitigate the performance dip caused by this procedural background pass and get back to a stable 120FPS even with some decently expensive shader code in the background itself.
&lt;/div&gt;
&lt;h2 id=&#34;conclusion--takeaways&#34;&gt;conclusion + takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;WebGL framebuffer blits are apparently not as cheap as I thought they were - especially on certain architectures like Mac and mobile.&lt;/li&gt;
&lt;li&gt;There are opportunities to get extra performance by doing things manually in a way that top-level Three.JS APIs can&amp;rsquo;t&lt;/li&gt;
&lt;li&gt;Old and often-repeated advice, but you have to actually profile rather than making assumptions about what&amp;rsquo;s causing something to be slow&lt;/li&gt;
&lt;/ul&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Rust LLD Linker Undefined Symbol Error</title>
      <link>https://cprimozic.net/notes/posts/fixing-rust-wasm-lld-undefined-symbol/</link>
      <pubDate>Thu, 23 Apr 2026 19:04:37 -0500</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-rust-wasm-lld-undefined-symbol/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;the problem&lt;/h2&gt;
&lt;p&gt;After upgrading my Rust toolchain to the latest nightly, I started getting errors when building a WebAssembly project of mine:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;error: linking with `rust-lld` failed: exit status: 1
  |
  = note:  &amp;#34;rust-lld&amp;#34; &amp;#34;-flavor&amp;#34; &amp;#34;wasm&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;create_terrain_gen_ctx&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;free&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;gen_heightmap&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;malloc&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;set_params&amp;#34; &amp;#34;--export=__heap_base&amp;#34; &amp;#34;--export=__data_end&amp;#34; &amp;#34;-z&amp;#34; &amp;#34;stack-size=1048576&amp;#34; &amp;#34;--stack-first&amp;#34; &amp;#34;--no-demangle&amp;#34; &amp;#34;--no-entry&amp;#34; &amp;#34;&amp;lt;2 object files omitted&amp;gt;&amp;#34; &amp;#34;&amp;lt;sysroot&amp;gt;/lib/rustlib/wasm32-unknown-unknown/lib/libpanic_abort-*.rlib&amp;#34; &amp;#34;/home/casey/dream/src/viz/wasm/target/wasm32-unknown-unknown/release/deps/{libcommon-a24e0de9c9a36cad,librand_pcg-96aa495088ea06d3,librand-dd7ab4b12369f67a,librand_core-5f1e6ad6780daf68,libopensimplex_noise_rs-29c02d98fe844459,libnanoserde-5b273dd919df410e}.rlib&amp;#34; &amp;#34;&amp;lt;sysroot&amp;gt;/lib/rustlib/wasm32-unknown-unknown/lib/{libstd-*,libdlmalloc-*,libcfg_if-*,librustc_demangle-*,libstd_detect-*,libhashbrown-*,librustc_std_workspace_alloc-*,libminiz_oxide-*,libadler2-*,libunwind-*,liblibc-*,librustc_std_workspace_core-*,liballoc-*,libcore-*,libcompiler_builtins-*}.rlib&amp;#34; &amp;#34;-L&amp;#34; &amp;#34;&amp;lt;sysroot&amp;gt;/lib/rustlib/wasm32-unknown-unknown/lib/self-contained&amp;#34; &amp;#34;-o&amp;#34; &amp;#34;/home/casey/dream/src/viz/wasm/target/wasm32-unknown-unknown/release/deps/terrain.wasm&amp;#34; &amp;#34;--gc-sections&amp;#34; &amp;#34;--no-entry&amp;#34; &amp;#34;-O3&amp;#34;
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: rust-lld: error: /home/casey/dream/src/viz/wasm/target/wasm32-unknown-unknown/release/deps/terrain.terrain.1508d28c2e9bb756-cgu.0.rcgu.o: undefined symbol: log_error
          rust-lld: error: /home/casey/dream/src/viz/wasm/target/wasm32-unknown-unknown/release/deps/terrain.terrain.1508d28c2e9bb756-cgu.0.rcgu.o: undefined symbol: log_error
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The place in my code causing these issues was this:&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;the problem&lt;/h2&gt;
&lt;p&gt;After upgrading my Rust toolchain to the latest nightly, I started getting errors when building a WebAssembly project of mine:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;error: linking with `rust-lld` failed: exit status: 1
  |
  = note:  &amp;#34;rust-lld&amp;#34; &amp;#34;-flavor&amp;#34; &amp;#34;wasm&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;create_terrain_gen_ctx&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;free&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;gen_heightmap&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;malloc&amp;#34; &amp;#34;--export&amp;#34; &amp;#34;set_params&amp;#34; &amp;#34;--export=__heap_base&amp;#34; &amp;#34;--export=__data_end&amp;#34; &amp;#34;-z&amp;#34; &amp;#34;stack-size=1048576&amp;#34; &amp;#34;--stack-first&amp;#34; &amp;#34;--no-demangle&amp;#34; &amp;#34;--no-entry&amp;#34; &amp;#34;&amp;lt;2 object files omitted&amp;gt;&amp;#34; &amp;#34;&amp;lt;sysroot&amp;gt;/lib/rustlib/wasm32-unknown-unknown/lib/libpanic_abort-*.rlib&amp;#34; &amp;#34;/home/casey/dream/src/viz/wasm/target/wasm32-unknown-unknown/release/deps/{libcommon-a24e0de9c9a36cad,librand_pcg-96aa495088ea06d3,librand-dd7ab4b12369f67a,librand_core-5f1e6ad6780daf68,libopensimplex_noise_rs-29c02d98fe844459,libnanoserde-5b273dd919df410e}.rlib&amp;#34; &amp;#34;&amp;lt;sysroot&amp;gt;/lib/rustlib/wasm32-unknown-unknown/lib/{libstd-*,libdlmalloc-*,libcfg_if-*,librustc_demangle-*,libstd_detect-*,libhashbrown-*,librustc_std_workspace_alloc-*,libminiz_oxide-*,libadler2-*,libunwind-*,liblibc-*,librustc_std_workspace_core-*,liballoc-*,libcore-*,libcompiler_builtins-*}.rlib&amp;#34; &amp;#34;-L&amp;#34; &amp;#34;&amp;lt;sysroot&amp;gt;/lib/rustlib/wasm32-unknown-unknown/lib/self-contained&amp;#34; &amp;#34;-o&amp;#34; &amp;#34;/home/casey/dream/src/viz/wasm/target/wasm32-unknown-unknown/release/deps/terrain.wasm&amp;#34; &amp;#34;--gc-sections&amp;#34; &amp;#34;--no-entry&amp;#34; &amp;#34;-O3&amp;#34;
  = note: some arguments are omitted. use `--verbose` to show all linker arguments
  = note: rust-lld: error: /home/casey/dream/src/viz/wasm/target/wasm32-unknown-unknown/release/deps/terrain.terrain.1508d28c2e9bb756-cgu.0.rcgu.o: undefined symbol: log_error
          rust-lld: error: /home/casey/dream/src/viz/wasm/target/wasm32-unknown-unknown/release/deps/terrain.terrain.1508d28c2e9bb756-cgu.0.rcgu.o: undefined symbol: log_error
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The place in my code causing these issues was this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rs&#34; data-lang=&#34;rs&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;mod&lt;/span&gt; imports {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;#[link(wasm_import_module = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;env&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;extern&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;C&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;#[allow(dead_code)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;log_msg&lt;/span&gt;(msg: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;u8&lt;/span&gt;, len: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;log_error&lt;/span&gt;(msg: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;u8&lt;/span&gt;, len: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s the same pattern I&amp;rsquo;ve used to import functions from JS into my Rust WebAssembly modules since I first started working with Rust + Wasm several years ago.  I&amp;rsquo;d compile + instantiate my Wasm module with those imports at runtime like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;WebAssembly&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;compile&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;bytes&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Uint8Array&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;ArrayBuffer&lt;/span&gt;&amp;gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;then&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;WebAssembly&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instantiate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;env&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;log_error&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;ptr&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;len&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;getEngine&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;then&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;memory&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Uint8Array&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;exports&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;memory&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WebAssembly&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Memory&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;buffer&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;buf&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;memory&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;slice&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ptr&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ptr&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;len&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TextDecoder&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;decode&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;buf&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;console&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;str&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;log_msg&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;ptr&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;len&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;getEngine&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;then&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;memory&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Uint8Array&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;exports&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;memory&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WebAssembly&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Memory&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;buffer&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;buf&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;memory&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;slice&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ptr&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ptr&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;len&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TextDecoder&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;decode&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;buf&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a6e22e&#34;&gt;console&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;str&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-cause&#34;&gt;the cause&lt;/h2&gt;
&lt;p&gt;I hadn&amp;rsquo;t changed anything in that code in a long time nor had I updated my &lt;code&gt;lld&lt;/code&gt; linker, so the issue was almost certainly the updated rust toolchain.&lt;/p&gt;
&lt;p&gt;Apparently, at some point, Rust changed its behavior wrt. the default import and you now have to manually specify the Wasm import module to pull those imports instead of having it default to &lt;code&gt;env&lt;/code&gt; like before.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;the fix&lt;/h2&gt;
&lt;p&gt;I just added one line to add a magic directive to tell it to pull these imports from &lt;code&gt;env&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rs&#34; data-lang=&#34;rs&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#[cfg(target_arch = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;wasm32&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;mod&lt;/span&gt; imports {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;#[link(wasm_import_module = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;env&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;extern&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;C&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;#[allow(dead_code)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;log_msg&lt;/span&gt;(msg: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;u8&lt;/span&gt;, len: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;log_error&lt;/span&gt;(msg: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;u8&lt;/span&gt;, len: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After that, the build and functionality worked like normal.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Spotify OAuth 500 &#34;Oops! Something went wrong&#34; Error</title>
      <link>https://cprimozic.net/notes/posts/fixing-spotify-oauth-500-error-empty-state-param/</link>
      <pubDate>Tue, 07 Apr 2026 21:21:08 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-spotify-oauth-500-error-empty-state-param/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;When trying to complete the OAuth flow for my web app &lt;a href=&#34;https://spotifytrack.net/&#34;&gt;spotifytrack.net&lt;/a&gt;, I started getting sent to an error page with a 500 status code and the following unhelpful message:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Oops! Something went wrong, please try again or check out our help area.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The app had been running successfully for years with no changes to the OAuth configuration. The error was easily reproducible just by visiting the &lt;code&gt;/authorize&lt;/code&gt; URL directly. It persisted after trying incognito windows, different browsers, different devices, logging out and back in, etc.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;When trying to complete the OAuth flow for my web app &lt;a href=&#34;https://spotifytrack.net/&#34;&gt;spotifytrack.net&lt;/a&gt;, I started getting sent to an error page with a 500 status code and the following unhelpful message:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Oops! Something went wrong, please try again or check out our help area.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The app had been running successfully for years with no changes to the OAuth configuration. The error was easily reproducible just by visiting the &lt;code&gt;/authorize&lt;/code&gt; URL directly. It persisted after trying incognito windows, different browsers, different devices, logging out and back in, etc.&lt;/p&gt;
&lt;p&gt;Looking at the my service&amp;rsquo;s logs, it seemed to only be affecting my account; other users were still able to complete the OAuth flow and generate tokens successfully. Maybe other accounts were running into it as well, but I couldn&amp;rsquo;t easily tell.&lt;/p&gt;
&lt;p&gt;The authorize URL I was using looked like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;https://accounts.spotify.com/authorize?client_id=...&amp;amp;response_type=code&amp;amp;redirect_uri=...&amp;amp;scope=user-top-read&amp;amp;state=
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The issue was the empty &lt;code&gt;state&lt;/code&gt; parameter at the end of the URL.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spotify&amp;rsquo;s OAuth &lt;code&gt;/authorize&lt;/code&gt; endpoint apparently can&amp;rsquo;t handle an empty &lt;code&gt;state&lt;/code&gt; and returns a 500 error. Setting any non-empty placeholder value for &lt;code&gt;state&lt;/code&gt; makes the flow work as expected.&lt;/p&gt;
&lt;p&gt;This is definitely a bug on Spotify&amp;rsquo;s side, but that&amp;rsquo;s the workaround if you run into it.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>A Unique Performance Optimization for a 3D Geometry Language</title>
      <link>https://cprimozic.net/notes/posts/persistent-expr-memo-optimization-for-geoscript/</link>
      <pubDate>Sat, 10 Jan 2026 19:14:35 -0600</pubDate>
      <guid>https://cprimozic.net/notes/posts/persistent-expr-memo-optimization-for-geoscript/</guid>
      <description>&lt;p&gt;For the past several months, I&amp;rsquo;ve been working on a programming language called Geoscript.  It&amp;rsquo;s specialized for generating and manipulating 3D geometry for use in a Shadertoy-inspired web app called &lt;a href=&#34;https://3d.ameo.design/geotoy&#34;&gt;Geotoy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/dgf.png&#34; alt=&#34;A screenshot of the Geotoy web UI showing the editor view for a composition.  There’s a canvas view on the left showing a rippled vase-like shape, textured with a mineral-like pattern and bathed in green and blue light.  There’s a code editor on the right side containing the Geoscript source code used to generate the vase.&#34;&gt;&lt;/p&gt;</description>
      <content>&lt;p&gt;For the past several months, I&amp;rsquo;ve been working on a programming language called Geoscript.  It&amp;rsquo;s specialized for generating and manipulating 3D geometry for use in a Shadertoy-inspired web app called &lt;a href=&#34;https://3d.ameo.design/geotoy&#34;&gt;Geotoy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/dgf.png&#34; alt=&#34;A screenshot of the Geotoy web UI showing the editor view for a composition.  There’s a canvas view on the left showing a rippled vase-like shape, textured with a mineral-like pattern and bathed in green and blue light.  There’s a code editor on the right side containing the Geoscript source code used to generate the vase.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Geoscript uses a pretty simple tree-walking interpreter for its execution.  That being said, I&amp;rsquo;ve put in a decent amount of effort optimizing it for runtime speed as well as building up an optimization pipeline for the language itself.&lt;/p&gt;
&lt;div class=&#34;note&#34;&gt;While working on this, I discovered a pretty neat optimization opportunity that&#39;s uniquely suited to this use case.&lt;/div&gt;
&lt;p&gt;But first, let me give a little background on Geoscript&amp;rsquo;s optimization pipeline to give some history and context.&lt;/p&gt;
&lt;p&gt;(or just skip to the &amp;ldquo;Cross-Run Expr Cache Persistence&amp;rdquo; section below if you want)&lt;/p&gt;
&lt;h2 id=&#34;geoscript-optimization-pipeline&#34;&gt;Geoscript Optimization Pipeline&lt;/h2&gt;
&lt;p&gt;The first optimization I added for geoscript was basic constant folding.  Nothing special or unique so far; just the usual approach of finding things like &lt;code&gt;{ op: Add, lhs: Literal(1), rhs: Literal(1) }&lt;/code&gt; in the AST and replacing it with &lt;code&gt;Literal(2)&lt;/code&gt;.  I got the initial version working pretty quickly and started extending it to support more operations and data types.  As I continued, I quickly came to realize something:&lt;/p&gt;
&lt;div class=&#34;good&#34;&gt;Almost everything in a typical Geoscript program is constant and doesn&#39;t depend on any external or dynamic input&lt;/div&gt;
&lt;p&gt;Unlike programs from other languages, Geoscript doesn&amp;rsquo;t accept user input, perform DB calls, or interact with any external system at all.  There are some small exceptions to this like PRNG calls and side effects to render meshes or print debug messages.  But since the RNG is seeded, a Geoscript program is really just a pure function with zero arguments; it produces the exact same output every time it&amp;rsquo;s run.&lt;/p&gt;
&lt;p&gt;Because of this fact, once I made the constant folding smart enough, it turned out that most or even all of the program would end up getting ran during the &amp;ldquo;optimization&amp;rdquo; process.  This even applies to closures, as long as the variables they capture from the outer scope are themselves constant (it took some pretty complicated plumbing to get that part working correctly).  The AST would often get replaced with just &lt;code&gt;render(precomputed_mesh_literal)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Despite this, there weren&amp;rsquo;t really any significant user-visible changes to the way the language/interpreter worked, and there weren&amp;rsquo;t very dramatic speed-ups to program execution.  The optimizations did enable some speedups for cases like this though:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-geoscript&#34; data-lang=&#34;geoscript&#34;&gt;spheres = 0..1000
  -&amp;gt; || {
    # random `vec3` where each element is in the range [-1000, 1000]
    pos = randv(-1000, 1000)
    s = icosphere(radius=10, resolution=4)
    s | translate(pos)
  }
  | join
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In that case, the &lt;code&gt;s = icosphere(...)&lt;/code&gt; statement doesn&amp;rsquo;t depend on any input in the closure or anything other than its literal arguments.  So, the constant folding will replace it with a literal mesh in the AST.  This saves the sphere mesh from having to get re-created each iteration of the loop and also saves on memory since the underlying mesh representation is reference-counted and &lt;code&gt;translate&lt;/code&gt; just updates the transform matrix without mutating the mesh&amp;rsquo;s geometry itself.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Anyway, once I got the constant folding mostly wrapped up, I moved on to trying to add some other optimizations to the pipeline.  The optimization I chose to pursue next was Common Subexpression Elimination, which is commonly referred to as CSE.  This involves identifying identical expressions in the AST and replacing them with a single instance in a variable.  This saves the value from having to get computed multiple times and can also unlock further optimization opportunities.&lt;/p&gt;
&lt;p&gt;The first thing you need to do when implementing CSE is to set up a way to determine if two expressions (AST sub-trees) are identical.  The approach I used for that is tree-based structural hashing, which I&amp;rsquo;m pretty sure is the standard solution.  It was a little fiddly and tedious to set up, but I eventually got it working so any arbitrary AST could be deterministically hashed to a unique &lt;code&gt;u128&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The next step I planned for implementing was to walk the AST, hash each node, identify duplicates, and replace them with references to a new temporary variable I&amp;rsquo;d create.  This would require consideration for scopes and context, but if implemented correctly it would yield effective intra-expression CSE.&lt;/p&gt;
&lt;p&gt;I also wanted to see if I could implement &lt;em&gt;inter-expression&lt;/em&gt;, full-program CSE.  For example, I wanted to be able to handle cases like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-geoscript&#34; data-lang=&#34;geoscript&#34;&gt;s = icosphere(radius=10, resolution=4)

some_fn = || {
  // ...
  another_sphere = icosphere(radius=10, resolution=4)
  // ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ideally, the CSE pass would identify that those two &lt;code&gt;icosphere&lt;/code&gt; expressions were both identical and replace them both with a reference to the same pre-computed mesh.  This would only work for fully-const cases.  So if the radius of &lt;code&gt;another_sphere&lt;/code&gt; depended on an argument of &lt;code&gt;some_fn&lt;/code&gt;, for example, this optimization wouldn&amp;rsquo;t apply.&lt;/p&gt;
&lt;div class=&#34;note&#34;&gt;In order to implement this, I developed a dynamic programming approach that would memoize the evaluation of all fully constant expressions in the AST as the program is executed.&lt;/div&gt;
&lt;p&gt;So each time an expression is evaluated, it&amp;rsquo;s hashed and the resulting value is inserted into the memoization cache.  The next time an identical expression is hit, we get a cache hit and instead of running the interpreter we just clone the value out of the cache and use that.&lt;/p&gt;
&lt;h2 id=&#34;cross-run-expr-cache-persistence&#34;&gt;Cross-Run Expr Cache Persistence&lt;/h2&gt;
&lt;p&gt;It was at this point that I suddenly had a very exciting idea:&lt;/p&gt;
&lt;div class=&#34;good&#34;&gt;There&#39;s nothing stopping me from persisting this constant expression cache across multiple runs of the interpreter!&lt;/div&gt;
&lt;p&gt;Geotoy, Shadertoy are essentially live-coding environments.  As a developer working in them, you usually iterate by making one small tweak or addition, re-running the code to see the updated output, and then evaluate based on that what to change next.  The vast majority of the program usually stays exactly the same.  By making the constant expression cache persistent, we can just re-use the results from the previous run for all the unchanged portions of the program.&lt;/p&gt;
&lt;p&gt;In some cases this isn&amp;rsquo;t going to provide a lot of benefit.  If you change a variable on line 1 which is depended on by every other line after it, there will be no cache hits and this strategy won&amp;rsquo;t provide any value.  However, in very many real cases, a very high percentage of the program&amp;rsquo;s runtime can be cut out.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example from a &lt;a href=&#34;https://3d.ameo.design/geotoy/edit/57&#34;&gt;real Geotoy composition&lt;/a&gt; I was working on:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-geoscript&#34; data-lang=&#34;geoscript&#34;&gt;distance_to_circle = |p: vec3, radius: num| {
  sqrt(p.y*p.y + pow(sqrt(p.x*p.x + p.z*p.z) - radius, 2))
}

radius = 8

0..
  -&amp;gt; || randv(-radius*1.1, radius*1.1)
  | filter(|p| distance_to_circle(p, radius) &amp;lt; 2)
  | take(1550)
  | alpha_wrap(alpha=1/100, offset=1/100)
  | smooth(iterations=2)
  | simplify(tolerance=0.01)
  | render
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href=&#34;https://doc.cgal.org/latest/Alpha_wrap_3/index.html&#34;&gt;&lt;code&gt;alpha_wrap&lt;/code&gt;&lt;/a&gt; is a function from the &lt;a href=&#34;https://www.cgal.org/&#34;&gt;CGAL&lt;/a&gt; library which works kind of like a sophisticated &amp;ldquo;concave hull&amp;rdquo; algorithm.  It generates a mesh that encloses a set of arbitrary 3D points.  It&amp;rsquo;s used for repairing meshes with bad or incomplete topologies and for things like processing LiDAR point cloud data.  I&amp;rsquo;ve found that it&amp;rsquo;s also extremely useful for generating really cool-looking organic shapes from procedurally generated point clouds:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/dgg.png&#34; alt=&#34;A screenshot of a mesh generated using the code above which uses the alpha_wrap function.  It creates a shape that looks like a ring of liquid metal floating in zero gravity.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The downside of this function&amp;rsquo;s power and versatility is that it&amp;rsquo;s very expensive to run.  On my computer, that &lt;code&gt;alpha_wrap&lt;/code&gt; call takes around 900ms to run.  The more points you pass and the higher you turn up the detail level, the bigger this gets as well.&lt;/p&gt;
&lt;p&gt;Now, consider a case where you&amp;rsquo;re just tuning the &lt;code&gt;tolerance&lt;/code&gt; param of the &lt;code&gt;simplify&lt;/code&gt; function, trying to find a good balance between mesh quality and vertex count.  Previously, this would require re-running the entire program each time - including the expensive &lt;code&gt;alpha_wrap&lt;/code&gt; call - even though its output remains exactly the same.  With the persistent expression cache, that whole part of the AST will just get pulled out directly and only the changed &lt;code&gt;simplify()&lt;/code&gt; call gets run.&lt;/p&gt;
&lt;p&gt;Unsurprisingly, this translates to huge reductions in execution time and dramatic improvements to the developer experience working in Geotoy:&lt;/p&gt;
&lt;p&gt;&lt;video src=&#34;https://i.ameo.link/dgh.mp4&#34; style=&#34;width: 100%&#34; controls=&#34;controls&#34;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;h3 id=&#34;prng-handling&#34;&gt;PRNG Handling&lt;/h3&gt;
&lt;p&gt;One thing you may have noticed in the source code for that composition is that there&amp;rsquo;s a conspicuous &lt;code&gt;randv&lt;/code&gt; call right at the beginning of the pipeline.  Since this call both mutates the RNG&amp;rsquo;s state as well as relies on its hidden starting state, you might think that this would end up making the whole thing impure and non-const.  And you&amp;rsquo;d be right.&lt;/p&gt;
&lt;p&gt;This is something I had to handle explicitly in my expression cache in order to make caching it possible.  A large number of Geotoy compositions make use of RNG calls, and just turning this optimization off for all those cases seemed like a big waste to me.&lt;/p&gt;
&lt;div class=&#34;note&#34;&gt;To support caching these kinds of expressions, I include the RNG start state as part of the cache key for expressions that read or write to Geotoy&#39;s built-in PRNG.&lt;/div&gt;
&lt;p&gt;This essentially treats those expressions as pure functions of &lt;code&gt;(rng_state) -&amp;gt; (T, new_rng_state)&lt;/code&gt; instead of the usual &lt;code&gt;() -&amp;gt; T&lt;/code&gt; for most const expressions.  Since RNG is re-seeded each run and the PRNG is fully deterministic, this means that as long as everything runs in the same order, the PRNG will transition to the same states after each call and the expression cache will be hit successfully on subsequent runs.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This persistent expression memoization optimization has ended up becoming the most impactful optimization I&amp;rsquo;ve added to Geoscript by far.  It&amp;rsquo;s pretty funny since I came up with the idea for it completely out of the blue while working on CSE, which I haven&amp;rsquo;t even bothered finishing up yet.&lt;/p&gt;
&lt;p&gt;That being said, this is obviously in no way a completely novel idea I&amp;rsquo;ve invented here.  In fact, it very closely models the kinds of caching techniques used to speed up build systems like Nix and Bazel.  Instead of caching object files or software packages, it&amp;rsquo;s caching the intermediate values produced while evaluating a program&amp;rsquo;s AST.  This approach is just uniquely suited to Geoscript/Geotoy&amp;rsquo;s use case, where the same program is run repeatedly with small changes and no external or dynamic inputs.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Updating three-good-godrays for Three.JS 0.182</title>
      <link>https://cprimozic.net/notes/posts/updating-three-good-godrays-for-threejs-182/</link>
      <pubDate>Thu, 01 Jan 2026 03:18:23 -0600</pubDate>
      <guid>https://cprimozic.net/notes/posts/updating-three-good-godrays-for-threejs-182/</guid>
      <description>&lt;p&gt;I maintain the &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays&#34;&gt;&lt;code&gt;three-good-godrays&lt;/code&gt;&lt;/a&gt; library, which adds support for volumetric raymarched screen-space godrays to Three.JS.&lt;/p&gt;
&lt;p&gt;The most recent version of Three.JS (0.182.0) made some breaking changes to its depth packing and shadow internals that the library relied on, and it needed to be updated in order to work with it. I figured I&amp;rsquo;d create a little summary of what changes were needed for anyone else updating similar code for newer Three.JS versions.&lt;/p&gt;</description>
      <content>&lt;p&gt;I maintain the &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays&#34;&gt;&lt;code&gt;three-good-godrays&lt;/code&gt;&lt;/a&gt; library, which adds support for volumetric raymarched screen-space godrays to Three.JS.&lt;/p&gt;
&lt;p&gt;The most recent version of Three.JS (0.182.0) made some breaking changes to its depth packing and shadow internals that the library relied on, and it needed to be updated in order to work with it. I figured I&amp;rsquo;d create a little summary of what changes were needed for anyone else updating similar code for newer Three.JS versions.&lt;/p&gt;
&lt;p&gt;Most of my fixes are implemented in this commit: &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays/commit/2295609acb7cadbf9415edd51bcebbab766012d5&#34;&gt;https://github.com/Ameobea/three-good-godrays/commit/2295609acb7cadbf9415edd51bcebbab766012d5&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Most of the breaking changes in Three.JS can be traced back to this PR: &lt;a href=&#34;https://github.com/mrdoob/three.js/pull/32303&#34;&gt;https://github.com/mrdoob/three.js/pull/32303&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This PR is described as &amp;ldquo;modernizes the WebGLRenderer shadow mapping with several significant improvements&amp;rdquo; and it makes several distinct changes that touch many different parts of Three.JS&amp;rsquo;s internals.&lt;/p&gt;
&lt;h2 id=&#34;cubemaps-for-pointlights&#34;&gt;Cubemaps for &lt;code&gt;PointLight&lt;/code&gt;s&lt;/h2&gt;
&lt;p&gt;The first relevant change is &amp;ldquo;Native Cube Depth Texture Support for PointLight Shadows&amp;rdquo;. Previously, Three.JS used a texture atlas kind of approach that stored the shadow data for the point light in a normal 2D texture. This then had to be manually transformed to get the relevant shadow data out, which my code handled.&lt;/p&gt;
&lt;p&gt;Version 0.182 changes it to using an actual cubemap texture. I updated &lt;code&gt;three-good-godrays&lt;/code&gt; to check for this case and use the built-in cubemap read functions to deal with it in that case:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// this `USE_CUBE_SHADOWMAP` is set on the JS side if a cubemap texture is found&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#if defined(USE_CUBE_SHADOWMAP)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; lightToPos &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; worldPos &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; lightPos;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; lightDist &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; length(lightToPos);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; shadowMapDepth &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; textureCube(shadowMap, lightToPos).r;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; depth &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; lightCameraNear &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; (lightCameraFar &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; lightCameraNear) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; shadowMapDepth;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec2&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt;(lightDist &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; depth &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.005&lt;/span&gt;), lightDist);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;simpler-depth-packing&#34;&gt;Simpler Depth Packing&lt;/h2&gt;
&lt;p&gt;Previously, Three.JS&amp;rsquo;s &lt;code&gt;BasicDepthPacking&lt;/code&gt; used a custom function that packed the depth value into the full RGBA range to help improve precision. This new version switches to a simpler scheme that just uses a single value. I&amp;rsquo;m not sure what the reason for this is, but anyway my code had to change.&lt;/p&gt;
&lt;p&gt;I checked if the Three.JS release was &amp;gt;= 182 and if so use the simpler depth packing which is just &lt;code&gt;1.0 - depth&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// `USE_UNPACKED_DEPTH` is set if Three.JS version &amp;gt;= 182&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#if defined( USE_UNPACKED_DEPTH )&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;#&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;defined&lt;/span&gt;(IS_DIRECTIONAL_LIGHT)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; depth &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; packedDepth.x;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;#&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// packing uses: gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; depth &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1.&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; packedDepth.x;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;#&lt;/span&gt;endif
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; depth &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; unpackRGBAToDepth(packedDepth);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#endif&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;orthographiccamera-now-default-in-postprocessing-for-pass&#34;&gt;&lt;code&gt;OrthographicCamera&lt;/code&gt; Now Default in &lt;code&gt;postprocessing&lt;/code&gt; for &lt;code&gt;Pass&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The final breaking change that I needed to deal with wasn&amp;rsquo;t in Three.JS itself but rather the &lt;a href=&#34;https://github.com/pmndrs/postprocessing&#34;&gt;&lt;code&gt;postprocessing&lt;/code&gt;&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;It was introduced in this commit: &lt;a href=&#34;https://github.com/pmndrs/postprocessing/commit/443043c816ba7e1c3001a615e6a36648d234a1ef&#34;&gt;https://github.com/pmndrs/postprocessing/commit/443043c816ba7e1c3001a615e6a36648d234a1ef&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This seems to impact postprocessing versions &amp;gt;= 6.38.&lt;/p&gt;
&lt;p&gt;That commit supposedly fixes an exception that happens when using Three.JS&amp;rsquo;s new reversed depth mode - which is supposedly an alternative to logarithmic depth buffer that offers better precision + performance. Anyway, it changes the default camera passed to the &lt;code&gt;Pass&lt;/code&gt; constructor from a simple &lt;code&gt;THREE.Camera&lt;/code&gt; to a &lt;code&gt;THREE.OrthographicCamera&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It was tricky to track this down, but the fix was as simple as just explicitly passing a &lt;code&gt;new THREE.Camera()&lt;/code&gt; instead of relying on the new default:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;-    super(&amp;#39;GodraysCompositorPass&amp;#39;);
+    super(&amp;#39;GodraysCompositorPass&amp;#39;, undefined, new THREE.Camera());
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It was rather annoying making these fixes and getting everything working, but at least it worked and &lt;code&gt;three-good-godrays&lt;/code&gt; now fully works with the latest versions of both Three.JS and postprocessing.&lt;/p&gt;
&lt;p&gt;Usually these updates aren&amp;rsquo;t that bad luckily, and most of the time there&amp;rsquo;s nothing that needs to be updated at all.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Generating 3D Meshes From Text</title>
      <link>https://cprimozic.net/notes/posts/generating-3d-meshes-from-text/</link>
      <pubDate>Thu, 27 Nov 2025 23:41:35 -0600</pubDate>
      <guid>https://cprimozic.net/notes/posts/generating-3d-meshes-from-text/</guid>
      <description>&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/de8.png&#34; alt=&#34;A screenshot of a 3D mesh which shows the text “Text to Mesh”.  It is split with a diagonal line through the middle, and the right side is slightly thicker.  It is textured with a tan concrete/stone like texture and rendered with shadows and lighting.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I recently had a desire to convert text to 3D meshes that I could render and manipulate as part of my &lt;a href=&#34;https://3d.ameo.design/geotoy&#34;&gt;Geotoy&lt;/a&gt; project and Geoscript language. I did some research into tools and libraries that could solve different pieces of this, and I put together a pipeline that implements the whole thing - yielding nice, 2-manifold 3D meshes with arbitrary fonts, text styles, and more.&lt;/p&gt;</description>
      <content>&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/de8.png&#34; alt=&#34;A screenshot of a 3D mesh which shows the text “Text to Mesh”.  It is split with a diagonal line through the middle, and the right side is slightly thicker.  It is textured with a tan concrete/stone like texture and rendered with shadows and lighting.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I recently had a desire to convert text to 3D meshes that I could render and manipulate as part of my &lt;a href=&#34;https://3d.ameo.design/geotoy&#34;&gt;Geotoy&lt;/a&gt; project and Geoscript language. I did some research into tools and libraries that could solve different pieces of this, and I put together a pipeline that implements the whole thing - yielding nice, 2-manifold 3D meshes with arbitrary fonts, text styles, and more.&lt;/p&gt;
&lt;p&gt;This post gives an overview of the whole setup and aims to give anyone else looking to implement something similar everything they need to get it working themselves.&lt;/p&gt;
&lt;h2 id=&#34;svg-text-to-path&#34;&gt;&lt;code&gt;svg-text-to-path&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The first part of the setup uses a JavaScript library called &lt;a href=&#34;https://github.com/paulzi/svg-text-to-path&#34;&gt;&lt;code&gt;svg-text-to-path&lt;/code&gt;&lt;/a&gt;. It handles taking arbitrary input text and font params and generating a SVG which contains paths that match the text as closely as possible.&lt;/p&gt;
&lt;p&gt;Internally, this library handles both fetching + loading the user-specified font as well as performing the text-&amp;gt;path conversion itself. It supports different backends for each of these steps.&lt;/p&gt;
&lt;p&gt;For my use case, I made use of the Google Fonts provider. It was easy to set up and only requires a Google Fonts API key, which can be generated for free. This allows me to use almost any font on Google Fonts to create my meshes. Some failed to load, but only a few and they seemed to be more obscure ones, and I didn&amp;rsquo;t bother to dig into why.&lt;/p&gt;
&lt;p&gt;For the text-&amp;gt;path conversion, &lt;code&gt;svg-text-to-path&lt;/code&gt; defaults to using the &lt;a href=&#34;https://github.com/foliojs/fontkit&#34;&gt;fontkit&lt;/a&gt; backend. Fontkit is another pure JavaScript library that implements a font engine. I didn&amp;rsquo;t look into it too deeply, but it seems feature rich and has support for many advanced font features.&lt;/p&gt;
&lt;p&gt;For my use case, my app runs in the browser. I could have used &lt;code&gt;svg-text-to-path&lt;/code&gt; directly within it to generate these paths. However, this text-&amp;gt;mesh feature isn&amp;rsquo;t core to my use case and I didn&amp;rsquo;t want to bloat the app with it. I also wanted to make it as easy as possible for users to set up, and wanted to be able to use my Google Fonts API key in a secure way.&lt;/p&gt;
&lt;p&gt;So, I opted to create a tiny little backend service to take input text + params and return the generated path as a string. It&amp;rsquo;s a very minimal &lt;a href=&#34;https://bun.com&#34;&gt;Bun&lt;/a&gt; webserver using Bun&amp;rsquo;s built-in &lt;code&gt;Bun.serve&lt;/code&gt;. It exposes a single HTTP/JSON endpoint.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;svg-text-to-path&lt;/code&gt; also includes a minimal built-in webserver, but I opted to create my own so that I could set up some custom caching and post-process the generated SVG to just extract the path. I opted to use an LLM to scaffold out most of this app and it worked pretty well. I feel like this kind of low-stakes one-off/standalone app is an ideal use case for them.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the source code if you&amp;rsquo;re interested, but I promise it&amp;rsquo;s nothing special: &lt;a href=&#34;https://github.com/Ameobea/sketches-3d/tree/main/geoscript_backend/text-to-path&#34;&gt;https://github.com/Ameobea/sketches-3d/tree/main/geoscript_backend/text-to-path&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Anyway, the output of this is an SVG path which encodes a sequence of draw commands used to generate the text like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;path&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;M5.86 24L5.86 9.53L1.15 9.53L1.15 7.2L13 7.2L13 9.53L8.28 9.53L8.28 24ZM18.8 24.29Q17.09 24.29 15.85 23.47 ....&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;lyon&#34;&gt;lyon&lt;/h2&gt;
&lt;p&gt;Now that I had the path generation working, I needed a way to turn it into triangles for the mesh. Luckily, the excellent &lt;a href=&#34;https://docs.rs/lyon/latest/lyon/&#34;&gt;&lt;code&gt;lyon&lt;/code&gt;&lt;/a&gt; Rust libraries (which I&amp;rsquo;ve used several times in the past for various projects) solve this problem perfectly.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://docs.rs/lyon_extra/latest/lyon_extra/&#34;&gt;&lt;code&gt;lyon_extra&lt;/code&gt;&lt;/a&gt; crate includes an SVG path parser which handles parsing that path into the underlying draw commands.&lt;/p&gt;
&lt;p&gt;Then, the &lt;a href=&#34;https://docs.rs/lyon_tessellation/latest/lyon_tessellation/&#34;&gt;&lt;code&gt;lyon_tessellation&lt;/code&gt;&lt;/a&gt; crate takes those draw commands and converts them into triangles. It handles all the hard parts and edge cases with concave shapes, hollow inner areas, discretizing bezier curves, and everything else.&lt;/p&gt;
&lt;p&gt;I implemented a tiny WebAssembly wrapper that takes the input path and returns vertex and index buffers: &lt;a href=&#34;https://github.com/Ameobea/sketches-3d/blob/main/src/viz/wasm/path_tessellate/src/lib.rs&#34;&gt;https://github.com/Ameobea/sketches-3d/blob/main/src/viz/wasm/path_tessellate/src/lib.rs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There is a little bit of extra stuff for handling custom scaling, but other than that it&amp;rsquo;s really just a very thin wrapper over &lt;code&gt;lyon&lt;/code&gt; functionality.&lt;/p&gt;
&lt;p&gt;One note here is that I had to change the default &lt;code&gt;FillTessellator&lt;/code&gt; options to set the &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/fill-rule&#34;&gt;&lt;code&gt;fill-rule&lt;/code&gt;&lt;/a&gt; to non-zero, which I believe is the default for SVGs. This fixes the output for some fonts that contain self-intersecting paths, going from this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/de6.png&#34; alt=&#34;A screenshot of a mesh generated from some text containing the letters “agB”.  There are some artifacts and gaps in the letters that appear to be in areas where the different parts of the strokes that form the letters intersect each other.&#34;&gt;&lt;/p&gt;
&lt;p&gt;to this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/de7.png&#34; alt=&#34;A screenshot of a mesh generated from some text containing the letters “agB”.  The letters appear well-formed with no artifacts or gaps missing in areas where the different parts of the strokes that form the letters intersect each other.&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;extrusion&#34;&gt;extrusion&lt;/h2&gt;
&lt;p&gt;So at this point, I had two buffers containing vertices and indices defining a 2D mesh matching the path for the text. The only part that remains is extruding it into 3D. This is a pretty straight-forward and common operation to do on a triangle mesh.&lt;/p&gt;
&lt;p&gt;To start, you first convert all the vertices from 2D to 3D by filling in the new axis with zeroes (so like (5, 10) -&amp;gt; (5, 0, 10)).&lt;/p&gt;
&lt;p&gt;Then, you flip the winding order of all the triangles in your mesh. WebGL and almost all other rendering systems use counter-clockwise winding orders, and that defines which direction the triangle is visible from. To flip them, you can just swap the first and third index of each triangle in the index buffer like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;1,2,3,5,7,9,1,4,2

to

3,2,1,9,7,5,2,4,1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, you create a duplicate of each of the vertices offset &lt;code&gt;n&lt;/code&gt; units in the new axis (so like (5, 0, 10) -&amp;gt; (5, 2, 10)).&lt;/p&gt;
&lt;p&gt;Then, join those new vertices with triangles but in the original (unflipped) winding order. That will make the top and the bottom face in opposite directions - the top facing up and the bottom facing down.&lt;/p&gt;
&lt;p&gt;Finally, you generate triangle strips to join the border edges of the top and bottom faces. A border edge is any edge that is only part of exactly one face. Usually a graph representation like a half-edge data structure is used when working with meshes, which helps with this part.&lt;/p&gt;
&lt;p&gt;The result should look something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/de9.png&#34; alt=&#34;A screenshot a mesh representing the 3D letter O, rendered with a magenta wireframe and viewed edge-on.  The triangle strips used to bridge the top and bottom face together are clearly visible.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s my source code if you&amp;rsquo;re interested, but note that it&amp;rsquo;s using my own internal mesh representation: &lt;a href=&#34;https://github.com/Ameobea/sketches-3d/blob/main/src/viz/wasm/geoscript/src/mesh_ops/extrude.rs&#34;&gt;https://github.com/Ameobea/sketches-3d/blob/main/src/viz/wasm/geoscript/src/mesh_ops/extrude.rs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you did everything correctly and took care to keep track of the vertex indices carefully to avoid creating duplicate vertices at the same position, the resulting mesh should be well-formed and 2-manifold/watertight. This is a very important topological property and is a requirement for a variety of other mesh processing algorithms including CSG (constructive solid geometry).&lt;/p&gt;
&lt;p&gt;The fact that the output meshes are manifold means that they can be combined with other meshes using boolean operations or sent through additional processing like smoothing. I&amp;rsquo;m not 100% positive that all paths generated from all glyphs using all fonts will end up producing manifold outputs, but everything I tested did.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;conclusion&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s it! After all of this, the output is a set of vertices and indices that define a 3D mesh representing the input text.&lt;/p&gt;
&lt;p&gt;I integrated this functionality into my Geoscript language as a builtin function:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/de4.png&#34; alt=&#34;A screenshot of a 3D mesh containing the text “arbitrary text” rendered with Geoscript/Geotoy.  It has been sliced through the middle to demonstrate the fact that the mesh is 2-manifold.  The geoscript source code used to produce it is included at the bottom.  It’s using the “Story Script” font from Google Fonts.&#34;&gt;&lt;/p&gt;
&lt;p&gt;There are few steps to manage, but the powerful libraries under the hood (&lt;code&gt;svg-text-to-path&lt;/code&gt;, &lt;code&gt;fontkit&lt;/code&gt;, and &lt;code&gt;lyon&lt;/code&gt;) handle all the complex stuff and heavy lifting.&lt;/p&gt;
&lt;p&gt;Even though some of the critical libraries are in JavaScript and the fact that the generation happens on a remote webserver, I&amp;rsquo;ve found that for the (relatively short) text I convert it works quite fast - fast enough to work on-demand without waiting.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also not yet found any fonts that produce broken output or buggy meshes. It even works for complicated non-English scripts:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/dea.png&#34; alt=&#34;A screenshot of a green mesh representing the Japanese character 芽 rendered using the “Yuji Mai” font&#34;&gt;&lt;/p&gt;
&lt;p&gt;It was a fun little side-quest and I&amp;rsquo;m very happy with the results overall.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Linux Perf Inject Hanging</title>
      <link>https://cprimozic.net/notes/posts/fixing-linux-perf-inject-hanging/</link>
      <pubDate>Tue, 18 Nov 2025 11:38:39 -0600</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-linux-perf-inject-hanging/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was working on optimizing some WebAssembly, and wanted to use the Linux &lt;code&gt;perf&lt;/code&gt; utility to profile Google Chrome to analyze where the most time was getting spent in my code at the assembly level.&lt;/p&gt;
&lt;p&gt;I was following a guide to do this: &lt;a href=&#34;https://v8.dev/docs/linux-perf&#34;&gt;https://v8.dev/docs/linux-perf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There is one step that involves processing some artifacts generated during the profiling process. This involves running the following command:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;perf inject --jit --input=perf.data --output=perf.data.jitted;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I ran this command, it would start but then hang indefinitely. It seemed to be making no progress even after several minutes of running, and there was no noticeable CPU activity to indicate it was doing work.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was working on optimizing some WebAssembly, and wanted to use the Linux &lt;code&gt;perf&lt;/code&gt; utility to profile Google Chrome to analyze where the most time was getting spent in my code at the assembly level.&lt;/p&gt;
&lt;p&gt;I was following a guide to do this: &lt;a href=&#34;https://v8.dev/docs/linux-perf&#34;&gt;https://v8.dev/docs/linux-perf&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There is one step that involves processing some artifacts generated during the profiling process. This involves running the following command:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;perf inject --jit --input=perf.data --output=perf.data.jitted;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;When I ran this command, it would start but then hang indefinitely. It seemed to be making no progress even after several minutes of running, and there was no noticeable CPU activity to indicate it was doing work.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;I eventually figured out that &lt;code&gt;perf&lt;/code&gt; has a &lt;code&gt;-v&lt;/code&gt; flag which enables verbose logging. When I re-ran the &lt;code&gt;perf inject&lt;/code&gt; command with this flag included, I saw that &lt;code&gt;perf&lt;/code&gt; was trying to download hundreds (maybe more) of debug info files for some of the symbols in the binary I was profiling.&lt;/p&gt;
&lt;p&gt;I was running a normal out-of-the box Google Chrome browser without any debug info attached. It&amp;rsquo;s possible that this wouldn&amp;rsquo;t happen in the case that a profiling build of Chrome was used that included debug symbols built-in.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if these requests were timing out, getting throttled/rate-limited, or if they were just taking a long time to return. However, the result was that they were being processed very slowly and I had no idea if/when they would finish.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;For my particular case, I didn&amp;rsquo;t have any need to download this debug info, whether it was even working or not; the code I was interested in was JIT-compiled WebAssembly.&lt;/p&gt;
&lt;p&gt;I found that this automatic debug symbol downloading was controlled by an environment variable: &lt;code&gt;DEBUGINFOD_URLS&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;On my system, this was set to &lt;code&gt;https://debuginfod.debian.net&lt;/code&gt; by default.&lt;/p&gt;
&lt;p&gt;I was able to fix the problem by re-running the &lt;code&gt;perf inject&lt;/code&gt; command with the environment variable cleared like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;DEBUGINFOD_URLS= perf inject -v --jit --input=perf.data --output perf.data.jitted
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This fix made the &lt;code&gt;perf inject&lt;/code&gt; command finish in less than a second, yielding me a &lt;code&gt;perf.data.jitted&lt;/code&gt; file that I could then analyze directly with &lt;code&gt;perf report&lt;/code&gt; like normal.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Compiling the Boundary-First-Flattening Library to Wasm</title>
      <link>https://cprimozic.net/notes/posts/compiling-boundary-first-flattening-to-wasm/</link>
      <pubDate>Mon, 04 Aug 2025 01:47:56 -0500</pubDate>
      <guid>https://cprimozic.net/notes/posts/compiling-boundary-first-flattening-to-wasm/</guid>
      <description>&lt;p&gt;Here is an account of the process I developed to get the &lt;a href=&#34;https://github.com/GeometryCollective/boundary-first-flattening&#34;&gt;&lt;code&gt;boundary-first-flattening&lt;/code&gt;&lt;/a&gt; library building for use on the web via WebAssembly.&lt;/p&gt;
&lt;p&gt;Boundary First Flattening (I refer to it as BFF throughout this article) is a powerful algorithm and library for &amp;ldquo;surface parameterization&amp;rdquo; - or projecting 3D surfaces into 2D. It also includes built-in support for other parts of a full UV unwrapping pipeline like bin-packing texture islands into a square. I was using it for my &lt;a href=&#34;https://3d.ameo.design/geotoy&#34;&gt;Geotoy&lt;/a&gt; project - a browser-based, Shadertoy-inspired web app for procedural geometry.&lt;/p&gt;</description>
      <content>&lt;p&gt;Here is an account of the process I developed to get the &lt;a href=&#34;https://github.com/GeometryCollective/boundary-first-flattening&#34;&gt;&lt;code&gt;boundary-first-flattening&lt;/code&gt;&lt;/a&gt; library building for use on the web via WebAssembly.&lt;/p&gt;
&lt;p&gt;Boundary First Flattening (I refer to it as BFF throughout this article) is a powerful algorithm and library for &amp;ldquo;surface parameterization&amp;rdquo; - or projecting 3D surfaces into 2D. It also includes built-in support for other parts of a full UV unwrapping pipeline like bin-packing texture islands into a square. I was using it for my &lt;a href=&#34;https://3d.ameo.design/geotoy&#34;&gt;Geotoy&lt;/a&gt; project - a browser-based, Shadertoy-inspired web app for procedural geometry.&lt;/p&gt;
&lt;p&gt;Getting this library compiled to Wasm was a very tricky process, mostly because BFF relies on some underlying linear algebra and math libraries to function. These both require some special tweaks and modifications of their own to compile to Wasm. Specifically, it uses &lt;a href=&#34;https://github.com/DrTimothyAldenDavis/SuiteSparse&#34;&gt;SuiteSparse&lt;/a&gt; - which in turn requires a &lt;a href=&#34;https://www.netlib.org/blas/&#34;&gt;BLAS&lt;/a&gt; + &lt;a href=&#34;https://www.netlib.org/lapack/&#34;&gt;LAPACK&lt;/a&gt; implementation.&lt;/p&gt;
&lt;h2 id=&#34;building-openblas-for-wasm&#34;&gt;Building OpenBLAS for Wasm&lt;/h2&gt;
&lt;p&gt;I was lucky here in that someone else had already done most of the hard work for compiling a BLAS implementation (OpenBLAS) to Wasm. It was done to support a Wasm version of an open-source speech recognition project called &lt;a href=&#34;https://github.com/kaldi-asr/kaldi&#34;&gt;Kaldi&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;They were kind enough to provide some pretty detailed notes: &lt;a href=&#34;https://github.com/msqr1/kaldi-wasm2&#34;&gt;https://github.com/msqr1/kaldi-wasm2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;These include some instructions compiling OpenBLAS for Wasm specifically. I followed them mostly, but had to tweak the compilation flags a bit in order to include LAPACK in the build as well (which is required for SuiteSparse).&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an overview:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clone &lt;a href=&#34;https://github.com/OpenMathLib/OpenBLAS&#34;&gt;OpenBLAS&lt;/a&gt; and check out commit &lt;code&gt;5ef8b19&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Apply &lt;a href=&#34;https://github.com/msqr1/kaldi-wasm2/tree/main/patches/openblas&#34;&gt;three small patches&lt;/a&gt; to trick the RISC64 target into working with Emscripten&lt;/li&gt;
&lt;li&gt;Source your Emscripten &lt;code&gt;emsdk&lt;/code&gt; installation, then build with this:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;make CC&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;emcc FC&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;emcc HOSTCC&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;gcc &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    TARGET&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;RISCV64_GENERIC &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ONLY_CBLAS&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; NOFORTRAN&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; NO_LAPACK&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; NO_LAPACKE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        C_LAPACK&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; BUILD_WITHOUT_LAPACK&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; USE_THREAD&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        BUILD_BFLOAT16&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; BUILD_COMPLEX16&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; BUILD_COMPLEX&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         CFLAGS&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-O3 -ffast-math -msimd128 -mavx&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This should run for a while and then spit out a &lt;code&gt;.a&lt;/code&gt; file containing the built library in the current directory.&lt;/p&gt;
&lt;p&gt;I end up getting errors at the end like &lt;code&gt;unable to find library -lgfortran&lt;/code&gt;, but it seems to be late enough in the build that it doesn&amp;rsquo;t matter. As long as you wind up with a &lt;code&gt;libopenblas_riscv64_generic-r0.3.28.a&lt;/code&gt; file in the openblas root dir, everything should be good.&lt;/p&gt;
&lt;p&gt;Install with:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;mkdir /home/YOURUSERNAME/blas-build
PREFIX=/home/YOURUSERNAME/blas-build NO_SHARED=1 make install
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(Note that &lt;code&gt;~&lt;/code&gt; doesn&amp;rsquo;t seem to work in paths in CMake as a shorthand for home directory; you have to provide the full explicit path in order for it to work.)&lt;/p&gt;
&lt;h2 id=&#34;building-suitesparse&#34;&gt;Building SuiteSparse&lt;/h2&gt;
&lt;p&gt;As I mentioned before, SuiteSparse depends on both BLAS and LAPACK. The OpenBLAS build from before (as configured) includes LAPACK symbols as well.&lt;/p&gt;
&lt;p&gt;However, try as I might, I couldn&amp;rsquo;t get CMake&amp;rsquo;s built-in support for finding and configuring BLAS to work. I ended up having to update the CMakeLists.txt and add these lines right before &lt;code&gt;if ( SUITESPARSE_USE_SYSTEM_GRAPHBLAS )&lt;/code&gt; (line 116):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-cmake&#34; data-lang=&#34;cmake&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;BLAS_LIBRARIES&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/home/YOURUSERNAME/blas-build/lib/libopenblas.a&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;set(&lt;span style=&#34;color:#e6db74&#34;&gt;LAPACK_LIBRARIES&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;${&lt;/span&gt;BLAS_LIBRARIES&lt;span style=&#34;color:#f92672&#34;&gt;}&lt;/span&gt;)&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;^ This overrides the default behavior of trying to search all over for any kind of valid BLAS/LAPACK implementation it can find and points it directly to the one we just built instead.&lt;/p&gt;
&lt;p&gt;I also had to apply this additional patch to get rid of some conflicting symbols that were getting re-defined for some reason from OpenBLAS/LAPACK:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;diff --git a/SuiteSparse_config/SuiteSparse_config.h b/SuiteSparse_config/SuiteSparse_config.h
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;index 7d7d3f3f6..b073183f1 100644
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;--- a/SuiteSparse_config/SuiteSparse_config.h
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+++ b/SuiteSparse_config/SuiteSparse_config.h
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;@@ -612,11 +612,11 @@ int SuiteSparse_version     // returns SUITESPARSE_VERSION
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; #define SUITESPARSE_BLAS_DSCAL      SUITESPARSE_BLAS ( dscal  , DSCAL  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; #define SUITESPARSE_BLAS_DNRM2      SUITESPARSE_BLAS ( dnrm2  , DNRM2  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-#define SUITESPARSE_LAPACK_DPOTRF   SUITESPARSE_BLAS ( dpotrf , DPOTRF )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-#define SUITESPARSE_LAPACK_DLARF    SUITESPARSE_BLAS ( dlarf  , DLARF  )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-#define SUITESPARSE_LAPACK_DLARFG   SUITESPARSE_BLAS ( dlarfg , DLARFG )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-#define SUITESPARSE_LAPACK_DLARFT   SUITESPARSE_BLAS ( dlarft , DLARFT )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-#define SUITESPARSE_LAPACK_DLARFB   SUITESPARSE_BLAS ( dlarfb , DLARFB )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+// #define SUITESPARSE_LAPACK_DPOTRF   SUITESPARSE_BLAS ( dpotrf , DPOTRF )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+// #define SUITESPARSE_LAPACK_DLARF    SUITESPARSE_BLAS ( dlarf  , DLARF  )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+// #define SUITESPARSE_LAPACK_DLARFG   SUITESPARSE_BLAS ( dlarfg , DLARFG )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+// #define SUITESPARSE_LAPACK_DLARFT   SUITESPARSE_BLAS ( dlarft , DLARFT )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+// #define SUITESPARSE_LAPACK_DLARFB   SUITESPARSE_BLAS ( dlarfb , DLARFB )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; // double complex
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; #define SUITESPARSE_BLAS_ZTRSV      SUITESPARSE_BLAS ( ztrsv  , ZTRSV  )
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This isn&amp;rsquo;t necessary to get the build to work, but it&amp;rsquo;s necessary to prevent runtime errors when using the final boundary-first-flattening library.&lt;/p&gt;
&lt;p&gt;Now, SuiteSparse can be built with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mkdir build &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; cd build
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;emcmake cmake -DCMAKE_BUILD_TYPE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Release -DCMAKE_C_COMPILER&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;emcc -DCMAKE_CXX_COMPILER&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;em++ -DEMSCRIPTEN&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;True -DBUILD_SHARED_LIBS&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF -DBUILD_STATIC_LIBS&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ON -DCHOLMOD_USE_CUDA&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF -DSUITESPARSE_USE_CUDA&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-DSUITESPARSE_ENABLE_PROJECTS=suitesparse_config;amd;colamd;cholmod&amp;#34;&lt;/span&gt; -DSUITESPARSE_USE_FORTRAN&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF -DBLA_STATIC&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ON -DBLAS_VENDOR&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OpenBLAS -DBLA_VENDOR&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OpenBLAS -DSUITESPARSE_USE_OPENMP&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF -DCMAKE_FIND_DEBUG_MODE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF -DBLA_F95&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF ..
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;emmake make -j12
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At some point, the build will freak out and break with errors about undefined symbols and conflicting function declarations. HOWEVER, it should have gotten far enough along to produce some nice .a files in places like &lt;code&gt;build/AMD/libamd.a&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These are what we&amp;rsquo;re really after, and as long as you have a &lt;code&gt;build/CHOLMOD/libcholmod.a&lt;/code&gt; you should be golden.&lt;/p&gt;
&lt;p&gt;Once the build finishes, the output static library .a files need to be moved somewhere that Emscripten can find them.&lt;/p&gt;
&lt;p&gt;I discovered that emscripten will look at this location in its search path, which is nice and isolated:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/home/YOURUSERNAME/emsdk/upstream/emscripten/cache/sysroot/usr/local/lib/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I created that directory and then copied the following files into there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/home/YOURUSERNAME/blas-build/lib/libopenblas.a&lt;/code&gt; to &lt;code&gt;libopenblas.a&lt;/code&gt; and also to &lt;code&gt;liblapack.a&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;it should be duplicated to both locations, as this library contains symbols for both BLAS and LAPACK but the build system looks for both separately&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SuiteSparse/build/AMD/libamd.a&lt;/code&gt; to &lt;code&gt;libAMD.a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SuiteSparse/build/CAMD/libcamd.a&lt;/code&gt; to &lt;code&gt;libcamd.a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SuiteSparse/build/CCOLAMD/libccolamd.a&lt;/code&gt; to &lt;code&gt;libccolamd.a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SuiteSparse/build/CHOLMOD/libcholmod.a&lt;/code&gt; to &lt;code&gt;libcholmod.a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SuiteSparse/build/SuiteSparse_config/libsuitesparseconfig.a&lt;/code&gt; to &lt;code&gt;libsuitesparseconfig.a&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;building-boundary-first-flattening&#34;&gt;Building &lt;code&gt;boundary-first-flattening&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;I had to make some changes to the CMake config for the BFF library itself to get it building with Wasm.&lt;/p&gt;
&lt;p&gt;(Full diff I applied: &lt;a href=&#34;https://github.com/GeometryCollective/boundary-first-flattening/commit/680a2fd384f736b7300f73126796dd0001970294&#34;&gt;https://github.com/GeometryCollective/boundary-first-flattening/commit/680a2fd384f736b7300f73126796dd0001970294&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;I edited &lt;code&gt;cmake/FindSuiteSparse.cmake&lt;/code&gt; and updated the list of suitesparse libraries to this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-cmake&#34; data-lang=&#34;cmake&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## Default behavior if user doesn&amp;#39;t use the COMPONENTS flag in find_package(SuiteSparse ...) command
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;if(&lt;span style=&#34;color:#e6db74&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;SuiteSparse_FIND_COMPONENTS&lt;/span&gt;)&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	list(&lt;span style=&#34;color:#e6db74&#34;&gt;APPEND&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;SuiteSparse_FIND_COMPONENTS&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;AMD&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;CAMD&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;CCOLAMD&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;COLAMD&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;CHOLMOD&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;suitesparseconfig&lt;/span&gt;)  &lt;span style=&#34;color:#75715e&#34;&gt;## suitesparse and metis are not searched by default (special case)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;endif()&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This removes some of the libraries which aren&amp;rsquo;t needed (and aren&amp;rsquo;t actually built with the given config) and adds in the &lt;code&gt;suitesparseconfig&lt;/code&gt; library explicitly which seemed to be missing.&lt;/p&gt;
&lt;p&gt;I also edited the root &lt;code&gt;CMakeLists.txt&lt;/code&gt; file to add these three lines before the &lt;code&gt;# suitesparse&lt;/code&gt; section:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-cmake&#34; data-lang=&#34;cmake&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;include_directories(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/home/casey/SuiteSparse/CHOLMOD/Include&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;include_directories(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/home/casey/SuiteSparse/SuiteSparse_config&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;include_directories(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/home/YOURUSERNAME/blas-build/include&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;^ these just help the compiler find the header files for SuiteSparse and BLAS/LAPACK that it needs in order to build.&lt;/p&gt;
&lt;h3 id=&#34;code-fixes&#34;&gt;Code Fixes&lt;/h3&gt;
&lt;p&gt;There were also a few fixes I had to make to the BFF source code itself to make it work.&lt;/p&gt;
&lt;p&gt;I had to update several files to add imports for &lt;code&gt;#include &amp;lt;cstdint&amp;gt;&lt;/code&gt; (this was required for me to even be able to build the library without emscripten). I imagine it has something to do with C++ compiler versions.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s one issue in the &lt;code&gt;boundary-first-flattening&lt;/code&gt; library that caused runtime issues when compiling for 32 bit targets (like Wasm). It was caused by assuming that &lt;code&gt;size_t&lt;/code&gt; matches the integer sizes used by some underlying SuiteSparse &lt;code&gt;SparseMatrix&lt;/code&gt;es.&lt;/p&gt;
&lt;p&gt;In reality, those SuiteSparse matrix indices were always 64 bits even on 32-bit targets, but &lt;code&gt;size_t&lt;/code&gt; was dropping to 32. This predictably created a lot of issues when casting pointers and copying memory around.&lt;/p&gt;
&lt;p&gt;I fixed it by patching this file: &lt;a href=&#34;https://github.com/GeometryCollective/boundary-first-flattening/commit/680a2fd384f736b7300f73126796dd0001970294#diff-1d5499e970a46aba46111951b7a9f6b526989e47973b06d12d6fdd02f0826e76&#34;&gt;https://github.com/GeometryCollective/boundary-first-flattening/commit/680a2fd384f736b7300f73126796dd0001970294#diff-1d5499e970a46aba46111951b7a9f6b526989e47973b06d12d6fdd02f0826e76&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;building&#34;&gt;Building&lt;/h3&gt;
&lt;p&gt;Once those CMake config files and code changes were applied, I built the library like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mkdir build &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; cd build
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# This configures it to just build a `libbff.a` file.  This might be all that&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# you need for your specific use case.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;emcmake cmake -DBFF_BUILD_GUI&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF -DBFF_BUILD_CLI&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF -DCMAKE_BUILD_TYPE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Release ..
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# You can also configure it to compile the CLI to Wasm, but this has limited utility:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;emcmake cmake -DBFF_BUILD_GUI&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;OFF -DBFF_BUILD_CLI&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;ON -DCMAKE_BUILD_TYPE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;Release ..
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Then run the build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;emmake make -j12
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;custom-wrapper-library&#34;&gt;Custom Wrapper Library&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s not a ton of value in having the raw CLI itself compiled to Wasm, and the &lt;code&gt;libbff.a&lt;/code&gt; file exposes a C++ interface that isn&amp;rsquo;t directly exposed to Wasm.&lt;/p&gt;
&lt;p&gt;A more useful solution is to create a custom wrapper library that exposes some high-level bindings to JS.&lt;/p&gt;
&lt;p&gt;I set up one that handles this. It exposes a single &lt;code&gt;unwrapUVs&lt;/code&gt; function which takes input mesh positions and vertices along with a few params controlling the BFF algorithm. It then computes the projection and returns a class holding the unwrapped UVs and new vertices and indices.&lt;/p&gt;
&lt;p&gt;You can find the full implementation in this commit: &lt;a href=&#34;https://github.com/GeometryCollective/boundary-first-flattening/commit/680a2fd384f736b7300f73126796dd0001970294#diff-67e14663983ede25ad744af48bbd4faff5ba01e893c956c0e49d10a4cc25eefd&#34;&gt;https://github.com/GeometryCollective/boundary-first-flattening/commit/680a2fd384f736b7300f73126796dd0001970294#diff-67e14663983ede25ad744af48bbd4faff5ba01e893c956c0e49d10a4cc25eefd&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;One thing to note is that I have a little extra script in the &lt;code&gt;Justfile&lt;/code&gt; to inject a line into the generated JS module so it can be import as an ES Module.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The only other piece I have is a little TypeScript shim to handle the async initialization of the UV unwrap module, move data in and out of Wasm, and handle errors: &lt;a href=&#34;https://github.com/Ameobea/sketches-3d/blob/main/src/viz/wasm/uv_unwrap/uvUnwrap.ts&#34;&gt;https://github.com/Ameobea/sketches-3d/blob/main/src/viz/wasm/uv_unwrap/uvUnwrap.ts&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in running this yourself, I&amp;rsquo;ve included the whole thing with the Emscripten-generated Wasm and JS file along with the TypeScript shim here: &lt;a href=&#34;https://github.com/Ameobea/sketches-3d/tree/main/src/viz/wasm/uv_unwrap&#34;&gt;https://github.com/Ameobea/sketches-3d/tree/main/src/viz/wasm/uv_unwrap&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;results&#34;&gt;Results&lt;/h2&gt;
&lt;p&gt;After all that work, you&amp;rsquo;re rewarded with a fully-functional BFF implementation that can handle the whole process of generating valid UVs for arbitrary input meshes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/d7v.png&#34; alt=&#34;A screenshot of a mesh rendered in Geotoy. It looks like a sphere with some holes drilled in it that has been split in half sideways. The sphere is textured with a debug texture showing UV coordinates across different parts of its surface&#34;&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m afraid I won&amp;rsquo;t be able to provide support to people if they run into trouble with this method or have issues getting this to work for their own use cases. As you can probably see, this whole thing is quite brittle and required a ton of hacks and workarounds to get working.&lt;/p&gt;
&lt;p&gt;That being said, I hope this writeup serves as a decent guide or starting point and helps people make use of the amazing &lt;code&gt;boundary-first-flattening&lt;/code&gt; library in the browser.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Google Chrome Using Wrong GPU on Linux</title>
      <link>https://cprimozic.net/notes/posts/fixing-google-chrome-using-wrong-gpu-on-linux/</link>
      <pubDate>Fri, 01 Aug 2025 19:53:24 -0500</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-google-chrome-using-wrong-gpu-on-linux/</guid>
      <description>&lt;p&gt;Well, here&amp;rsquo;s another chapter in the saga of graphics and GPU issues with Google Chrome on Linux.&lt;/p&gt;
&lt;p&gt;I have a 7900 XTX GPU, and I think at least part of these issues are a result of AMD GPU drivers being pretty messed up on Linux.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;One day after updating my packages and rebooting my system, WebGL web apps were running at very low FPS in my Google Chrome browser.  It was &lt;em&gt;really&lt;/em&gt; slow - like sub-10 FPS when I usually get 165.&lt;/p&gt;</description>
      <content>&lt;p&gt;Well, here&amp;rsquo;s another chapter in the saga of graphics and GPU issues with Google Chrome on Linux.&lt;/p&gt;
&lt;p&gt;I have a 7900 XTX GPU, and I think at least part of these issues are a result of AMD GPU drivers being pretty messed up on Linux.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;One day after updating my packages and rebooting my system, WebGL web apps were running at very low FPS in my Google Chrome browser.  It was &lt;em&gt;really&lt;/em&gt; slow - like sub-10 FPS when I usually get 165.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;After some debugging, I realized that my integrated GPU was being used instead of my dedicated GPU.  I figured this out using the &lt;code&gt;radeontop&lt;/code&gt; application, which has an argument to pick which GPU you&amp;rsquo;re recording performance for.  My integrated GPU had zero usage, but the integrated GPU was maxed out.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I eventually noticed this line in the output from the &lt;code&gt;google-chrome&lt;/code&gt; process:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[14533:14533:0801/194836.930457:ERROR:ui/ozone/platform/wayland/gpu/wayland_surface_factory.cc:233] &#39;--ozone-platform=wayland&#39; is not compatible with Vulkan. Consider switching to &#39;--ozone-platform=x11&#39; or disabling Vulkan&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I then tried launching Chrome with this argument:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;google-chrome --ozone-platform=x11
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that fixed the problem.  It went back to using my discrete GPU and WebGL performance went back to normal.&lt;/p&gt;
&lt;p&gt;I use Wayland for my desktop environment.  I don&amp;rsquo;t know why this is necessary, but whatever I guess.&lt;/p&gt;
&lt;p&gt;I do have Vulkan enabled in &lt;code&gt;chrome://flags&lt;/code&gt; but that&amp;rsquo;s required for me to avoid other issues I get with my max FPS getting limited.&lt;/p&gt;
&lt;p&gt;Note that I tried setting &amp;ldquo;Preferred Ozone platform&amp;rdquo; to X11 in &lt;code&gt;chrome://flags&lt;/code&gt; but it had no effect.  The fix only worked when using the argument when launching Chrome.&lt;/p&gt;
&lt;p&gt;Anyway, this exact of flags and configs makes Chrome work well for me on my system.  I dread the day when one something gets changed to break this delicate balance.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Sveltekit `fetch` error `ERR_SSL_WRONG_VERSION_NUMBER` with NGINX reverse proxy</title>
      <link>https://cprimozic.net/notes/posts/fixing-sveltekit-fetch-err_ssl_wrong_version_number/</link>
      <pubDate>Tue, 08 Jul 2025 02:51:18 -0500</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-sveltekit-fetch-err_ssl_wrong_version_number/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I am using SvelteKit and importing a WebAssembly module on the server side inside a &lt;code&gt;+page.server.ts&lt;/code&gt; file like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WasmPromise&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;src/viz/wasmComp/geoscript_repl&amp;#39;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;then&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt;.&lt;span style=&#34;color:#66d9ef&#34;&gt;default&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/geoscript_repl_bg.wasm&amp;#39;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It works when I run for local development, but it fails when running on my VPS in production. Some notes about my deployment environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I&amp;rsquo;m Sveltekit&amp;rsquo;s &lt;code&gt;adapter-node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;m deploying the Sveltekit app inside Docker behind an NGINX reverse proxy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This Wasm module as built using &lt;code&gt;wasm-bindgen&lt;/code&gt; for Rust, and it needs the path to the Wasm to be explicitly provided since it&amp;rsquo;s running on the server side rather than the client.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I am using SvelteKit and importing a WebAssembly module on the server side inside a &lt;code&gt;+page.server.ts&lt;/code&gt; file like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WasmPromise&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;src/viz/wasmComp/geoscript_repl&amp;#39;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;then&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt;.&lt;span style=&#34;color:#66d9ef&#34;&gt;default&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/geoscript_repl_bg.wasm&amp;#39;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;engine&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It works when I run for local development, but it fails when running on my VPS in production. Some notes about my deployment environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I&amp;rsquo;m Sveltekit&amp;rsquo;s &lt;code&gt;adapter-node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;m deploying the Sveltekit app inside Docker behind an NGINX reverse proxy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This Wasm module as built using &lt;code&gt;wasm-bindgen&lt;/code&gt; for Rust, and it needs the path to the Wasm to be explicitly provided since it&amp;rsquo;s running on the server side rather than the client.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;fetch(&#39;/geoscript_repl_bg.wasm&#39;)&lt;/code&gt; call was the one that was failing. This is the error that I got in the Sveltekit logs:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;TypeError: fetch failed
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at Object.fetch (node:internal/deps/undici/undici:11372:11)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async fetch (file:///app/build/server/index.js:4467:80) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  cause: [Error: 40AC6882457F0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:354:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ] {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    library: &amp;#39;SSL routines&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    reason: &amp;#39;wrong version number&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    code: &amp;#39;ERR_SSL_WRONG_VERSION_NUMBER&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;The issue seems to be due to the fact that my NGINX server was setting the &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; header to &lt;code&gt;https&lt;/code&gt;. This is correct behavior, but the Sveltekit node adapter was setting the protocol on the internal request for the Wasm module to HTTPs because of it.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the code responsible for that: &lt;a href=&#34;https://github.com/sveltejs/kit/blob/96ce0f9735c4d1a1d467c1f9641e91b5858eea93/packages/adapter-node/src/handler.js#L185&#34;&gt;https://github.com/sveltejs/kit/blob/96ce0f9735c4d1a1d467c1f9641e91b5858eea93/packages/adapter-node/src/handler.js#L185&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;Luckily, there&amp;rsquo;s a way to override this auto-detection behavior. By setting an &lt;code&gt;ORIGIN&lt;/code&gt; environment variable when running the Sveltekit server, I can force these internal requests to get made back to localhost with HTTP rather than HTTPS:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ORIGIN=&amp;quot;http://127.0.0.1:5814&amp;quot; node ./build/index.js&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This successfully resolved the issue for me, and my server-side Wasm worked in production.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing ltree syntax error in Postgres 15</title>
      <link>https://cprimozic.net/notes/posts/ltree-syntax-error/</link>
      <pubDate>Wed, 02 Jul 2025 16:06:30 -0500</pubDate>
      <guid>https://cprimozic.net/notes/posts/ltree-syntax-error/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was recently working with some Postgres tables that have an &lt;code&gt;ltree&lt;/code&gt; column.  I had written tests for my new feature that made use of them and all the tests passed locally.&lt;/p&gt;
&lt;p&gt;However, the tests were failing in CI with a vague error:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ERROR: ltree syntax error at character 9 (SQLSTATE 42601)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The query that was producing this error was quite simple as well:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;INSERT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;INTO&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;my_table&amp;#34;&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;path&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;VALUES&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;22e8f4e1-437f-448b-a8fb-0d7fbed8de7a&amp;#39;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;foo&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this case, the &lt;code&gt;path&lt;/code&gt; column was of type &lt;code&gt;ltree&lt;/code&gt;.  Running that exact query locally worked fine and I was confused as to why it wasn&amp;rsquo;t working in CI.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was recently working with some Postgres tables that have an &lt;code&gt;ltree&lt;/code&gt; column.  I had written tests for my new feature that made use of them and all the tests passed locally.&lt;/p&gt;
&lt;p&gt;However, the tests were failing in CI with a vague error:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ERROR: ltree syntax error at character 9 (SQLSTATE 42601)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The query that was producing this error was quite simple as well:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;INSERT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;INTO&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;my_table&amp;#34;&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;path&amp;#34;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;VALUES&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;22e8f4e1-437f-448b-a8fb-0d7fbed8de7a&amp;#39;&lt;/span&gt;,&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;foo&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this case, the &lt;code&gt;path&lt;/code&gt; column was of type &lt;code&gt;ltree&lt;/code&gt;.  Running that exact query locally worked fine and I was confused as to why it wasn&amp;rsquo;t working in CI.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;It turns out that I was running a more modern version of Postgres locally (17.5) than what was being run in CI (15).&lt;/p&gt;
&lt;p&gt;In older versions of Postgres including 15, there is a limitation that values inserted into &lt;code&gt;ltree&lt;/code&gt; columns can&amp;rsquo;t contain the &lt;code&gt;-&lt;/code&gt; character.  This limitation was removed in more recent versions.&lt;/p&gt;
&lt;p&gt;I was able to work around this issue by just stripping all non-alphanumeric characters out of my UUIDs before inserting them into the column.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Google Chrome WebGL Low Frame Rate After Turning Monitors Off on Linux</title>
      <link>https://cprimozic.net/notes/posts/fixing-google-chrome-webgl-frame-rate-resetting-after-turning-monitors-off/</link>
      <pubDate>Wed, 14 May 2025 15:00:57 -0500</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-google-chrome-webgl-frame-rate-resetting-after-turning-monitors-off/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I have two monitors: my main one at 165hz and a side monitor at 60hz. When I first boot up my computer, I&amp;rsquo;m able to run web-based games and visualizations at full 165hz on my main monitor.&lt;/p&gt;
&lt;p&gt;However, after I turn my monitors off for the night and then get back on the next morning, all my WebGL-based applications are locked to 60 FPS or lower (and they feel stuttery and generally worse than even what I&amp;rsquo;d expect from stable 60 FPS)&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I have two monitors: my main one at 165hz and a side monitor at 60hz. When I first boot up my computer, I&amp;rsquo;m able to run web-based games and visualizations at full 165hz on my main monitor.&lt;/p&gt;
&lt;p&gt;However, after I turn my monitors off for the night and then get back on the next morning, all my WebGL-based applications are locked to 60 FPS or lower (and they feel stuttery and generally worse than even what I&amp;rsquo;d expect from stable 60 FPS)&lt;/p&gt;
&lt;h3 id=&#34;attempted-fixes&#34;&gt;Attempted Fixes&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve tried closing all chrome windows/tabs and re-starting it, tried using a different version of chrome (google-chrome-unstable and I even tried Microsoft Edge), tried launching chrome with a variety of different flags and settings, but nothing works. The only fix is to log out and log back in from scratch, which is a hassle since I have to set everything back up.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m running Wayland. If I log in with X, then things run at like 40FPS average from the start. I&amp;rsquo;m using an AMD GPU with amdgpu drivers.&lt;/p&gt;
&lt;p&gt;Other non-browser graphics apps work fine and run at the correct frame rate. Firefox doesn&amp;rsquo;t have this particular issue, but there are other issues I run into with input handling among other things that make me really want to avoid using it here unless necessary.&lt;/p&gt;
&lt;p&gt;I tried launching Chrome with a variety of different flags including various combinations of &lt;code&gt;--ozone-platform=wayland&lt;/code&gt;, &lt;code&gt;--enable-features=UseOzonePlatform&lt;/code&gt;, &lt;code&gt;--use-angle=gl&lt;/code&gt;, and &lt;code&gt;--enable-features=VaapiVideoDecodeLinuxGL&lt;/code&gt; but all of these resulted in either no change or complete loss of hardware acceleration for WebGL.&lt;/p&gt;
&lt;h2 id=&#34;the-real-fix&#34;&gt;The Real Fix&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;After messing around for a while, I discovered that I could fix the problem entirely by enabling Vulkan in the Chrome settings via &lt;a href=&#34;#ZgotmplZ&#34;&gt;chrome://flags/&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/d1h.png&#34; alt=&#34;A screenshot of the Google Chrome settings showing the Vulkan option toggled to enabled.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I could have sworn that this was broken in the past so I wasn&amp;rsquo;t able to do it, but now it works fine and completely fixed my issue.&lt;/p&gt;
&lt;p&gt;EDIT 2025-05-18:&lt;/p&gt;
&lt;p&gt;After some time, this fix stopped worked.  I tried a bunch of stuff and what finally worked was swapping which monitor was &amp;ldquo;Primary&amp;rdquo; in my system settings:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/d1v.png&#34; alt=&#34;A screenshot of my Linux system display settings showing the primary monitor toggle&#34;&gt;&lt;/p&gt;
&lt;p&gt;After I set my higher-FPS monitor to be primary and rebooted Google Chrome, my WebGL went back to working at 165 FPS again.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing WHEA_UNCORRECTABLE_ERROR Bluescreen with Windows &#43; Linux Dual Boot</title>
      <link>https://cprimozic.net/notes/posts/fixing-whea-uncorrectable-error-windows-linux-dual-boot/</link>
      <pubDate>Tue, 15 Apr 2025 14:50:19 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-whea-uncorrectable-error-windows-linux-dual-boot/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I dual-boot multiple Linux installs along with Windows.  At some point, possibly coinciding with an update to one of my Linux installs, my Windows partition failed to boot with a bluescreen error showing a &lt;code&gt;WHEA_UNCORRECTABLE_ERROR&lt;/code&gt; error message.  I tried booting multiple times with no success.&lt;/p&gt;
&lt;p&gt;Most info I found online about this error seemed to indicate that it was related to hardware issues, but I hadn&amp;rsquo;t made any hardware changes recently and my Linux installs continued to boot and work fine.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I dual-boot multiple Linux installs along with Windows.  At some point, possibly coinciding with an update to one of my Linux installs, my Windows partition failed to boot with a bluescreen error showing a &lt;code&gt;WHEA_UNCORRECTABLE_ERROR&lt;/code&gt; error message.  I tried booting multiple times with no success.&lt;/p&gt;
&lt;p&gt;Most info I found online about this error seemed to indicate that it was related to hardware issues, but I hadn&amp;rsquo;t made any hardware changes recently and my Linux installs continued to boot and work fine.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;My boot setup is a bit unique since the Linux partition I use primarily isn&amp;rsquo;t the one that manages the bootloader.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To fix the problem, I booted into the Linux install that controls the bootloader and ran &lt;code&gt;sudo update-grub&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After doing that and rebooting, my Windows partition booted and worked fine without any further changes necessary.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Getting Kinematic Object Movement Interpolation Working in Bullet Physics/ammo.js</title>
      <link>https://cprimozic.net/notes/posts/getting-kinematic-object-movement-interpolation-working-in-bullet-physics-ammojs/</link>
      <pubDate>Fri, 14 Mar 2025 16:07:46 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/getting-kinematic-object-movement-interpolation-working-in-bullet-physics-ammojs/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m working on a browser-based &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;game engine&lt;/a&gt; that uses Three.JS for rendering and a WebAssembly port of the Bullet physics engine called &lt;a href=&#34;https://github.com/kripken/ammo.js&#34;&gt;ammo.js&lt;/a&gt; for its physics engine and character controller.&lt;/p&gt;
&lt;p&gt;Bullet is a very old physics engine and Ammo.JS is more or less a direct copy of it with a few changes to support usage in the browser. There are a lot of rough edges and things you have to figure out yourself in order to use it.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m working on a browser-based &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;game engine&lt;/a&gt; that uses Three.JS for rendering and a WebAssembly port of the Bullet physics engine called &lt;a href=&#34;https://github.com/kripken/ammo.js&#34;&gt;ammo.js&lt;/a&gt; for its physics engine and character controller.&lt;/p&gt;
&lt;p&gt;Bullet is a very old physics engine and Ammo.JS is more or less a direct copy of it with a few changes to support usage in the browser. There are a lot of rough edges and things you have to figure out yourself in order to use it.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;One problem I ran into was getting interpolated position changes to work for kinematic objects. Let&amp;rsquo;s way that your game&amp;rsquo;s animation frame is firing at 60FPS and you have the Bullet physics engine configured to run at a static 180hz tick rate. This means that for every animation frame, there will be 3 simulated physics engine ticks.&lt;/p&gt;
&lt;p&gt;I had some kinematic objects that I controlled manually from the JS side. I computed and set the positions of the objects each animation frame, applying the new position to both Three.JS and ammo.js/bullet simultaneously. I set their transforms in bullet via the &lt;code&gt;btMotionState&lt;/code&gt; interface which, according to code comments and docs, should help support interpolated movement and continuous collision detection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I was running into problems where fast-moving kinematic objects would clip through the player or otherwise bug out when colliding.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No matter how high I turned up the physics engine tick rate, the problem wouldn&amp;rsquo;t improve. Theoretically, at some point turning the tick rate high enough should resolve the issues since the movement between each internal tick will eventually be small enough to prevent the clipping.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;It turns out that continuous collision detection (referred to as CCD in bullet code and docs) is complicated and doesn&amp;rsquo;t really work by default.&lt;/p&gt;
&lt;p&gt;For starters, I was using the &lt;code&gt;btDiscreteCollisionWorld&lt;/code&gt; for my game engine. This has &amp;ldquo;discrete&amp;rdquo; in the name, so I guess it&amp;rsquo;s not really much of a surprise that it doesn&amp;rsquo;t do much continuous stuff out of the box. For what it&amp;rsquo;s worth, there is no continuous collision world that I could find which is shipped with ammo.js.&lt;/p&gt;
&lt;p&gt;After much digging through the code, I found some other places where continuous collision detection is used when solving individual collisions between objects. If an object has an associated &lt;code&gt;btMotionState&lt;/code&gt;, the new position of the object is queried at the beginning of each call to &lt;code&gt;btDiscreteDynamicsWorld::stepSimulation&lt;/code&gt;. This happens in the &lt;code&gt;saveKinematicState&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;That function computes linear and angular velocities for objects that have moved. These velocities are then used by CCD, but weren&amp;rsquo;t really having any effect as far as I could tell. This is probably because the actual &lt;code&gt;worldTransform&lt;/code&gt; of the object isn&amp;rsquo;t interpolated for the internal sub-ticks simulated by bullet (&lt;code&gt;internalSingleStepSimulation&lt;/code&gt;). As a result, my character controller was seeing the objects at their final point at the end of the simulated frame rather than seeing its movement interpolated along sub-ticks.&lt;/p&gt;
&lt;h2 id=&#34;the-solution&#34;&gt;The Solution&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;To resolve this, I manually interpolated the world transform of kinematic collision objects each subtick.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Then, I added a new &lt;code&gt;applyManualMotionStateInterpolation&lt;/code&gt; method to &lt;code&gt;btDiscreteDynamicsWorld&lt;/code&gt; which I call before each time &lt;code&gt;internalSingleStepSimulation&lt;/code&gt; is run. &lt;code&gt;applyManualMotionStateInterpolation&lt;/code&gt; uses that &lt;code&gt;m_lastWorldTransform&lt;/code&gt; variable along with the velocities computed in &lt;code&gt;saveKinematicState&lt;/code&gt; to simulate the transform of the object &lt;code&gt;dt&lt;/code&gt; seconds after the end of the last frame.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the full function:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-cpp&#34; data-lang=&#34;cpp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * This is some custom stuff I added to get interpolated positions of kinematic objects for internal
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * subticks.  The built-in interpolation doesn&amp;#39;t seem to work for kinematic objects, and this forces
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * interpolation between the past and most recently set transforms for kinematic objects.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; btDiscreteDynamicsWorld&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;applyManualMotionStateInterpolation(btScalar dt) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;; i &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; m_nonStaticRigidBodies.size(); i&lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt;){
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		btRigidBody&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; body &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; m_nonStaticRigidBodies[i];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (body&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;isActive() &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; body&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;isKinematicObject()) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#66d9ef&#34;&gt;auto&lt;/span&gt; startPos &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; body&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;getLastFrameWorldTransform();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			btTransform dstPos;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			btTransformUtil&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;integrateTransform(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				startPos,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				body&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;getInterpolationLinearVelocity(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				body&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;getInterpolationAngularVelocity(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				dt,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				dstPos
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			body&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;setWorldTransform(dstPos);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			body&lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;setLastFrameWorldTransform(dstPos);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It uses the pre-existing &lt;code&gt;integrateTransform&lt;/code&gt; to apply the velocities which is where the math and heavy lifting happens. Then, it sets the simulated destination position into &lt;code&gt;m_worldTransform&lt;/code&gt; via &lt;code&gt;setWorldTransform&lt;/code&gt; - which is the actual live position of the object used in physics simulations.&lt;/p&gt;
&lt;p&gt;I needed to make one additional change to the &lt;code&gt;saveKinematicState&lt;/code&gt; function in order for this to be valid when using fixed tick-rate mode for bullet:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;void btRigidBody::saveKinematicState(btScalar timeStep)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	//todo: clamp to some (user definable) safe minimum timestep, to limit maximum angular/linear velocities
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	if (timeStep != btScalar(0.))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+		m_interpolationWorldTransform = m_worldTransform;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		//if we use motionstate to synchronize world transforms, get the new kinematic/animated world transform
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		if (getMotionState())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			getMotionState()-&amp;gt;getWorldTransform(m_worldTransform);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		btTransformUtil::calculateVelocity(m_interpolationWorldTransform,m_worldTransform,timeStep,m_linearVelocity,m_angularVelocity);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		m_interpolationLinearVelocity = m_linearVelocity;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		m_interpolationAngularVelocity = m_angularVelocity;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		m_interpolationWorldTransform = m_worldTransform;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	} else {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		printf(&amp;#34;Zero timestep???\n&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Without that change, the transforms of objects affected by this manual interpolation wouldn&amp;rsquo;t reflect the portion of the movement from the remainder of the &lt;code&gt;dt&lt;/code&gt;. &lt;code&gt;m_interpolationWorldTransform&lt;/code&gt; is the transform that the object should have by the end of the current tick, but it doesn&amp;rsquo;t quite make it there due to that remainder.&lt;/p&gt;
&lt;p&gt;Setting &lt;code&gt;m_interpolationWorldTransform&lt;/code&gt; equal to &lt;code&gt;m_worldTransform&lt;/code&gt; works around this by computing the motion as starting from the last simulated point of the object rather than the last set point.&lt;/p&gt;
&lt;h2 id=&#34;results&#34;&gt;Results&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;As a result of these fixes, I was able to actually benefit from turning up the simulation tick rate and my collision issues with fast-moving kinematic objects vastly improved.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Note that this fix might not be valid for all bullet physics configurations. If you do stuff like setting your &lt;code&gt;btDispatcher&lt;/code&gt;&amp;rsquo;s &lt;code&gt;m_dispatchFunc&lt;/code&gt; to &lt;code&gt;DISPATCH_CONTINUOUS&lt;/code&gt; (it defaults to &lt;code&gt;DISPATCH_DISCRETE&lt;/code&gt;) or maybe even just use certain types of collision objects, this might result in incorrect physics if the CCD (which seems to be inactive and do nothing in my case) actually gets used somewhere.&lt;/p&gt;
&lt;h2 id=&#34;other-misc-notes&#34;&gt;Other Misc. Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Use specialized collision objects like &lt;code&gt;btBoxShape&lt;/code&gt; when possible rather than &lt;code&gt;btTriangleMesh&lt;/code&gt;. These seem to behave much better (less clipping through fast-moving objects, jitter, inaccurate displacements, etc.) and are much more performant.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The built-in &lt;a href=&#34;https://github.com/kripken/ammo.js/blob/main/bullet/src/BulletDynamics/Character/btKinematicCharacterController.cpp&#34;&gt;&lt;code&gt;btKinematicCharacterController&lt;/code&gt;&lt;/a&gt; doesn&amp;rsquo;t work well out of the box. They note this in their official docs, and its code hasn&amp;rsquo;t been touched in a decade or more. It also requires careful tuning of constants like jump height, step height, max penetration depth, physics engine tick length, and more in order to get decent results.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I had to make numerous tweaks and customizations to get the character controller working well for my use case. I also added a variety of extra features like clamping the character to moving platforms, adding external velocity for things like directional dashes and jump pads, etc. These changes can be found on &lt;a href=&#34;https://github.com/Ameobea/ammo.js/tree/updates&#34;&gt;my fork&lt;/a&gt;.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Creating Constrained Bezier Curves for an Envelope Generator</title>
      <link>https://cprimozic.net/notes/posts/creating-constrained-bezier-curves-for-an-envelope-generator/</link>
      <pubDate>Wed, 25 Dec 2024 19:27:03 -0600</pubDate>
      <guid>https://cprimozic.net/notes/posts/creating-constrained-bezier-curves-for-an-envelope-generator/</guid>
      <description>&lt;p&gt;I recently finished some work involving constrained bezier curves for use in my &lt;a href=&#34;https://synth.ameo.dev/&#34;&gt;browser-based digital audio workstation&lt;/a&gt;. Specifically, I used them in its &lt;em&gt;envelope generator&lt;/em&gt;, which looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/cp2.png&#34; alt=&#34;A screenshot of the envelope generator for web synth showing the bezier curves used to define the curves that are joined together to produce the transfer function.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The circles are draggable handles that allow the user to create whatever shape they desire for the envelope. The green handles define the start and end point of each curve segment and the blue circles control its shape/steepness.&lt;/p&gt;</description>
      <content>&lt;p&gt;I recently finished some work involving constrained bezier curves for use in my &lt;a href=&#34;https://synth.ameo.dev/&#34;&gt;browser-based digital audio workstation&lt;/a&gt;. Specifically, I used them in its &lt;em&gt;envelope generator&lt;/em&gt;, which looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/cp2.png&#34; alt=&#34;A screenshot of the envelope generator for web synth showing the bezier curves used to define the curves that are joined together to produce the transfer function.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The circles are draggable handles that allow the user to create whatever shape they desire for the envelope. The green handles define the start and end point of each curve segment and the blue circles control its shape/steepness.&lt;/p&gt;
&lt;h2 id=&#34;motivation&#34;&gt;Motivation&lt;/h2&gt;
&lt;p&gt;Originally, I used the function &lt;code&gt;x^n&lt;/code&gt; to build the curves that made up the envelope. This curve type worked decently well and allowed for curves of varying steepness to be represented, but it had some problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;x^n&lt;/code&gt; function can produce numerically unstable results with very large or very small exponents. This can result in very rapid changes in underlying value or even produce &lt;code&gt;NaN&lt;/code&gt; or &lt;code&gt;Infinity&lt;/code&gt; values. As you might imagine, isn&amp;rsquo;t very good for audio use cases.&lt;/li&gt;
&lt;li&gt;Curves aren&amp;rsquo;t symmetrical going in vs. going out which makes it impossible to create symmetrical peaks/dips, which is something often desired.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the main goal was to add support for a different curve type that would solve these problems and ideally add options for even more expressive shapes than before.&lt;/p&gt;
&lt;p&gt;Bezier curves were an obvious choice due to how simple they are to implement and their wide adoption. However, they also pose some challenges of their own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;x^n&lt;/code&gt; didn&amp;rsquo;t offer enough control over the shape of the curve, Bezier curves offer &lt;em&gt;too&lt;/em&gt; much control. For example, it&amp;rsquo;s possible to create bezier curves that loop back in the X dimension like this:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/cp3.png&#34; alt=&#34;A screenshot of a bezier curve with a X coordinate that increases, decreases, and then increases again along its span - making a sort of “s” shape.&#34;&gt;&lt;/p&gt;
&lt;p&gt;This clearly makes no sense for an envelope generator use case. I needed a way to constrain the generated curves in a way that ensures they&amp;rsquo;re valid.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The control points - which are used to adjust the shape of the curve - don&amp;rsquo;t lie on the curve itself. To work nicely in my existing UI, I needed to be able to add a handle somewhere on the curve that could be dragged to adjust its shape directly.&lt;/li&gt;
&lt;li&gt;Additionally, for cubic bezier splines, there are two different control points which need to be selected to shape the curve. I needed a way to pick values for these behind the scenes from a single handle position.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;constrained-bezier-curves&#34;&gt;Constrained Bezier Curves&lt;/h2&gt;
&lt;p&gt;A cubic bezier curve is defined by four points: A start point (&lt;code&gt;P0&lt;/code&gt;), end point (&lt;code&gt;P3&lt;/code&gt;), and two control points (&lt;code&gt;P1&lt;/code&gt; and &lt;code&gt;P2&lt;/code&gt;). The start and end point of the curves are fixed, so all we have to worry about is picking values for the two control points.&lt;/p&gt;
&lt;p&gt;I experimented with some bezier curve editors and manually tested out some different control point patterns. I eventually realized that &lt;strong&gt;setting both control points to the same position&lt;/strong&gt; worked quite well and get me very close to what I was looking for.&lt;/p&gt;
&lt;p&gt;It turns out that this produces curves with a lot of desirable characteristics. Crucially, the curves&amp;rsquo; X values all monotonically increase/decrease along the span of the curve, so no looping back or other degenerate curve shapes can be created.&lt;/p&gt;
&lt;p&gt;One important property of bezier curves which I learned is that &lt;strong&gt;all points on the curve will fit within the convex hull of the points that define it&lt;/strong&gt;. So, if the control points are set to a position within the rectangle bounded by the start and end points, all resulting curves should be valid.&lt;/p&gt;
&lt;p&gt;Although a lot of possible curve shapes aren&amp;rsquo;t creatable with coincident control points, it still provides more than enough control over the shape of the curve. So, I decided to stick with this method to constrain the curves.&lt;/p&gt;
&lt;h2 id=&#34;computing-control-points&#34;&gt;Computing Control Points&lt;/h2&gt;
&lt;p&gt;The final piece to figure out was a way to control the shape of the curve by dragging a handle on the curve itself rather than moving a floating control point directly.&lt;/p&gt;
&lt;p&gt;I needed to pick a point on the curve on which to attach the handle. I experimented with a few options and quickly discovered that &lt;em&gt;the midpoint of the curve by length was the obvious best choice&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Bezier curves can be defined as a linear combination of lower-degree bezier curves. At the midpoint of the curve (&lt;code&gt;t=0.5&lt;/code&gt;), the direction of the curve is evenly balanced between heading towards the shared control point and heading towards the endpoint. This works out to producing an instantaneous direction that is parallel to the line from the start point to the endpoint.&lt;/p&gt;
&lt;p&gt;Anyway, now remainder of problem is quite well defined. &lt;code&gt;P0&lt;/code&gt; and &lt;code&gt;P3&lt;/code&gt; are already defined and fixed. There&amp;rsquo;s a constraint that the resulting curve must pass through the handle&amp;rsquo;s position (&lt;code&gt;H&lt;/code&gt;) at &lt;code&gt;t=0.5&lt;/code&gt;. Given that, we have to solve for the shared control point (&lt;code&gt;C&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Here is the formula for a constrained cubic Bezier curve using coincident control points:&lt;/p&gt;
$$ConstrainedBezier(t) = (1 - t)^3 P_0 + 3 (1 - t)^2 t C + 3 (1 - t)t^2 C + t^3 P_3$$&lt;p&gt;This can be simplified using some algebra into the following:&lt;/p&gt;
$$ConstrainedBezier(t) = (1 - t)^3 P_0 + 3t(1 - t)C + t^3P_3$$&lt;p&gt;Then, to solve for the position of the handle &lt;code&gt;H&lt;/code&gt;, we can plug its &lt;code&gt;t=0.5&lt;/code&gt; value into the equation:&lt;/p&gt;
$$H = ConstrainedBezier(t) = (1 - 0.5)^3 P_0 + 3 \times 0.5 (1 - 0.5)C + 0.5^3P_3$$&lt;p&gt;Those constants collapse down, and after re-arranging &lt;code&gt;H&lt;/code&gt; and &lt;code&gt;C&lt;/code&gt; and applying a bit more algebra, we get the following equation yielding the position of the shared control point:&lt;/p&gt;
$$C = \frac{1}{6} P_0 + \frac{4}{3} H + \frac{1}{6} P_3$$&lt;p&gt;I was pretty surprised that the solution ended up being so simple - a linear combination of the start point, end point, and handle position. Sure enough, when I plugged them in, I reliably got out control points that generated curves which accurately intersected the handles at the curves&amp;rsquo; midpoints.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how it looks all put together:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/cp4.webp&#34; alt=&#34;A screen recording of the envelope generator using the constrained Bezier curve with some additional debug elements showing the position of the computed control point in pink, a line between the start and end point in dark red, and a line from the control point to the midpoint between the start and end point in pink.  The draggable handle also always sits on that line no matter where the handle is dragged.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Dragging the handle adjusts the curve in a way that feels intuitive and responsive. I was very happy with how it ended up feeling to use this.&lt;/p&gt;
&lt;p&gt;The pink circle is the computed shared control point for the curve. As a final step, I clamp that control point to be within the bounds of the envelope segment and then re-calculate the handle position using that clamped value. This ensures that the curve always remains valid and produces values that are in bounds.&lt;/p&gt;
&lt;p&gt;One other thing I noticed while setting this up is that the control point and handle always end up on the same line as the midpoint between the start and end point (this is drawn in pink in the animation above).&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s about it! I just wrote this up because I thought the properties of cubic bezier curves with coincident control points ended up being very neat and potentially useful in other domains.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m very happy with how this ended up working in the envelope generator itself as well. The curves look beautiful and seem to be working very well for generating musically interesting envelopes.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Rust&#43;WebAssembly Memory Access Out of Bounds Errors in Debug Mode</title>
      <link>https://cprimozic.net/notes/posts/fixing-rust-wasm-memory-access-out-of-bounds/</link>
      <pubDate>Mon, 23 Dec 2024 12:18:52 -0600</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-rust-wasm-memory-access-out-of-bounds/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;In my project built with Rust compiled to WebAssembly, I started seeing errors like this that caused a crash - but only when it was compiled in debug mode:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;wavetable.wasm-011dbbd6:0x17b89 Uncaught RuntimeError: memory access out of bounds
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at wavetable.wasm._ZN9wavetable2fm7effects14EffectInstance10from_parts17h321e4433e1216e56E (wavetable.wasm-011dbbd6:0x17b89)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at wavetable.wasm._ZN9wavetable2fm7effects11EffectChain10set_effect17h969349e414104e9eE (wavetable.wasm-011dbbd6:0x1ba60)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at wavetable.wasm.fm_synth_set_effect (wavetable.wasm-011dbbd6:0x314d3)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at FMSynthAWP.port.onmessage (FMSynthAWP.js:171:37)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When compiling in release mode, the code ran just fine.&lt;/p&gt;
&lt;p&gt;The decompiled debug-mode Wasm at the point of the crash looked like this:&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;In my project built with Rust compiled to WebAssembly, I started seeing errors like this that caused a crash - but only when it was compiled in debug mode:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;wavetable.wasm-011dbbd6:0x17b89 Uncaught RuntimeError: memory access out of bounds
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at wavetable.wasm._ZN9wavetable2fm7effects14EffectInstance10from_parts17h321e4433e1216e56E (wavetable.wasm-011dbbd6:0x17b89)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at wavetable.wasm._ZN9wavetable2fm7effects11EffectChain10set_effect17h969349e414104e9eE (wavetable.wasm-011dbbd6:0x1ba60)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at wavetable.wasm.fm_synth_set_effect (wavetable.wasm-011dbbd6:0x314d3)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at FMSynthAWP.port.onmessage (FMSynthAWP.js:171:37)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When compiling in release mode, the code ran just fine.&lt;/p&gt;
&lt;p&gt;The decompiled debug-mode Wasm at the point of the crash looked like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-wat&#34; data-lang=&#34;wat&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; $var833 &lt;span style=&#34;color:#66d9ef&#34;&gt;i32&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; $var834 &lt;span style=&#34;color:#66d9ef&#34;&gt;i32&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; $var835 &lt;span style=&#34;color:#66d9ef&#34;&gt;i32&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;global.get $__stack_pointer
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;local.set $var22
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;i32.const &lt;span style=&#34;color:#ae81ff&#34;&gt;1767008&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;local.set $var23
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;local.get $var22
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;local.get $var23
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;i32.sub
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;local.set $var24
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;local.get $var24
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;global.set $__stack_pointer
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;local.get $var24
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;local.get $var1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;i32.store &lt;span style=&#34;color:#66d9ef&#34;&gt;offset&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;//&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;the&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;instruction&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;which&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;produced&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;the&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;invalid&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;access&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;The only thing going on in this function up to that point is that it&amp;rsquo;s bumping the stack pointer down to make space for the local variables used by the function.&lt;/p&gt;
&lt;p&gt;When I hovered over the &lt;code&gt;$__stack_pointer&lt;/code&gt; variable in Chrome Devtools, I saw that it currently had this value stored:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;type: &amp;#34;i32&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;value: -792224
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The stack pointer is definitely not supposed to be negative.&lt;/p&gt;
&lt;p&gt;So the actual thing going on here was actually a stack overflow. That constant &lt;code&gt;1767008&lt;/code&gt; earlier on in the function was how much stack space the function was trying to request - around 1.75 MB. This was more than what was available, and that caused the stack pointer to go negative and produce an invalid memory access when trying to read it.&lt;/p&gt;
&lt;p&gt;This tracked with the code for the function itself. Within it, I was initializing some large buffers in memory using code like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rs&#34; data-lang=&#34;rs&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; buffer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Box::new(CircularBuffer::new());
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;CircularBuffer&lt;/code&gt; type was very large and stored the entire buffer inside itself directly without any indirection:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rs&#34; data-lang=&#34;rs&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;CircularBuffer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;LENGTH&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  buffer: [&lt;span style=&#34;color:#66d9ef&#34;&gt;f32&lt;/span&gt;; &lt;span style=&#34;color:#66d9ef&#34;&gt;LENGTH&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;/// Points to the index that the most recently added value was written to
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  head: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When Rust initializes &lt;code&gt;Box&lt;/code&gt;es in debug mode, it first creates the value on the stack and then copies it to the heap. In release mode, this copy is optimized out in most cases, so the large value are written directly into the allocated memory. That&amp;rsquo;s why this stack overflow only happens in debug mode.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A bit of a side note&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;It used to be possible to work around this issue by using the unstable &lt;code&gt;box&lt;/code&gt; syntax in Rust to force the stack-to-heap copy to not occur, like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rs&#34; data-lang=&#34;rs&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; buffer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;box&lt;/span&gt; CircularBuffer::new();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, &lt;code&gt;box&lt;/code&gt; syntax has since been &lt;a href=&#34;https://github.com/rust-lang/rust/issues/49733#issuecomment-1399427237&#34;&gt;removed&lt;/a&gt; with no plans on bringing it back.&lt;/p&gt;
&lt;p&gt;The comment linked above mentions some alternatives that would bring the same functionality back (they call it &amp;ldquo;placement new&amp;rdquo;). But as far as I can tell, as of writing this (Dec. 2024) none of these alternatives are available yet - stable or otherwise.&lt;/p&gt;
&lt;h2 id=&#34;a-workaround&#34;&gt;A Workaround&lt;/h2&gt;
&lt;p&gt;I found two different fixes for this issue.&lt;/p&gt;
&lt;p&gt;The first one is to manually bump the stack size to work around it. This can be done by editing the &lt;code&gt;.cargo/config.toml&lt;/code&gt; file in your project&amp;rsquo;s workspace:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;target&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;wasm32-unknown-unknown&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;rustflags&lt;/span&gt; = [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-C&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;link-args=-z stack-size=15000000&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, this has the effect of increasing the stack size for release mode as well which isn&amp;rsquo;t necessary.&lt;/p&gt;
&lt;p&gt;I tried to configure it to only increase it in dev mode, which required me to enable the &lt;code&gt;profile-rustflags&lt;/code&gt; feature for my workspace. After doing that, I got compilation errors for some of my dependencies which complained about the &lt;code&gt;-z stack-size&lt;/code&gt; argument not being supported by my linker, which didn&amp;rsquo;t happen before.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure what was going on with that, but I decided to stop trying this approach and go for what turned out to be a better solution.&lt;/p&gt;
&lt;h2 id=&#34;a-better-fix&#34;&gt;A Better Fix&lt;/h2&gt;
&lt;p&gt;Rather than work around the problem by just increasing the stack size, I decided to solve the underlying problem that caused it by just reducing the amount of data being written to the stack.&lt;/p&gt;
&lt;p&gt;To do this, I explicitly implement the optimization that &lt;code&gt;box&lt;/code&gt; syntax/placement new would provide by allocating uninitialized memory and then writing my data into it manually. This requires a bit of unsafe code, but looks something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rs&#34; data-lang=&#34;rs&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;mut&lt;/span&gt; buffer: Box&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;MaybeUninit&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;CircularBuffer&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;_&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Box::new_uninit();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; buf_ptr &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; buffer.as_mut_ptr();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;unsafe&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; inner &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;mut&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;buf_ptr).buffer;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  inner.fill(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;buf_ptr).head &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; buffer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;unsafe&lt;/span&gt; { buffer.assume_init() };
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For my situation, there was an even better solution.&lt;/p&gt;
&lt;p&gt;Since every single field of the struct I was initializing gets filled with data which has a bit representation of all zeroes, I can use a shortcut to initialize my buffer:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rs&#34; data-lang=&#34;rs&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; feedback_buffer: Box&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;CircularBuffer&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;_&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;unsafe&lt;/span&gt; { Box::new_zeroed().assume_init() };
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I can just ask for the memory to be pre-filled with all zeroes from the allocator and leave it at that!&lt;/p&gt;
&lt;p&gt;Once I made this change and fixed a couple of other places where I was initializing some large boxed values on the stack, the invalid memory access errors went away and my program started working in debug mode again.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Cypress Tests Failing with 404 Errors in Github Actions CI but Working Locally</title>
      <link>https://cprimozic.net/notes/posts/fixing-cypress-404-in-github-actions-ci-but-working-locally/</link>
      <pubDate>Sun, 24 Nov 2024 18:58:31 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-cypress-404-in-github-actions-ci-but-working-locally/</guid>
      <description>&lt;p&gt;I have some very basic Cypress tests set up for one of my personal projects - a single-page application created with React.  I have some simple Github Actions configured to automatically run those tests every time I push to the repository.&lt;/p&gt;
&lt;p&gt;At some point somewhat randomly, those tests started failing in CI.  It was probably due to some dependency getting upgraded or a change in the environment in which Github Actions runs.  The tests still ran fine locally when using the same commands and configuration, though, and I struggled to figure out why.&lt;/p&gt;</description>
      <content>&lt;p&gt;I have some very basic Cypress tests set up for one of my personal projects - a single-page application created with React.  I have some simple Github Actions configured to automatically run those tests every time I push to the repository.&lt;/p&gt;
&lt;p&gt;At some point somewhat randomly, those tests started failing in CI.  It was probably due to some dependency getting upgraded or a change in the environment in which Github Actions runs.  The tests still ran fine locally when using the same commands and configuration, though, and I struggled to figure out why.&lt;/p&gt;
&lt;p&gt;I was using the official &lt;a href=&#34;https://github.com/cypress-io/github-action&#34;&gt;Cypress Github Action&lt;/a&gt; to run the tests.  The config I was using is very basic with little to nothing special going on&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;cypress-test&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;runs-on&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ubuntu-latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;needs&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;uses&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;actions/checkout@v4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Download built site artifacts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;uses&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;actions/download-artifact@v4.1.7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;dist&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;path&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;./dist&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#75715e&#34;&gt;# Run Cypress tests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Cypress Run&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;uses&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;cypress-io/github-action@v6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;browser&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;chrome&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;start&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;yarn cypress:serve&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;wait-on&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;http://localhost:9000&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s the error I was getting in CI:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  2) Entrypoint
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       Should render composition 46 without errors:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     CypressError: `cy.visit()` failed trying to load:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;http://localhost:9000/composition/46
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;The response we received from your web server was:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;gt; 404: Not Found
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;This was considered a failure because the status code was not `2xx`.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When running Cypress locally, all the tests pass without issue.&lt;/p&gt;
&lt;p&gt;The application I was testing is a single-page app.  The &lt;code&gt;/composition/46&lt;/code&gt; path doesn&amp;rsquo;t map to anything on the filesystem and instead is handled by the application itself.  I had configured my &lt;code&gt;cypress:serve&lt;/code&gt; script using &lt;a href=&#34;https://www.npmjs.com/package/http-server&#34;&gt;&lt;code&gt;http-server&lt;/code&gt;&lt;/a&gt; to proxy missing URLs back to the root &lt;code&gt;index.html&lt;/code&gt; file, just like other SPA dev servers do.&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s what I had in my package.json for that &lt;code&gt;cypress:serve&lt;/code&gt; script:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;cypress:serve&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;http-server dist -p 9000 -P http://localhost:9000? -c-1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To re-iterate, this command works totally fine when running locally.  I can run &lt;code&gt;yarn cypress:serve&lt;/code&gt;, navigate to &lt;a href=&#34;http://localhost:9000/composition/43&#34;&gt;http://localhost:9000/composition/43&lt;/a&gt;, and view the expected page which is returned with a 200 response code.&lt;/p&gt;
&lt;h2 id=&#34;the-problem--the-fix&#34;&gt;The Problem + The Fix&lt;/h2&gt;
&lt;p&gt;After multiple hours of banging my head against this problem, I finally figured out what was causing the issue.&lt;/p&gt;
&lt;p&gt;For whatever reason, using &lt;code&gt;localhost&lt;/code&gt; in the proxy path for the &lt;code&gt;http-server&lt;/code&gt; command doesn&amp;rsquo;t work in Github Actions.  I&amp;rsquo;m not sure if it has something to do with DNS, ipv6, the networking that GH Actions uses to communicate between commands, or something else.&lt;/p&gt;
&lt;p&gt;In any case the fix turned out to be as easy as replacing &lt;code&gt;localhost&lt;/code&gt; with &lt;code&gt;127.0.0.1&lt;/code&gt; in my &lt;code&gt;http-server&lt;/code&gt; command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;diff --git a/package.json b/package.json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;index 1331d39..c6ba50e 100644
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;--- a/package.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+++ b/package.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;@@ -51,7 +51,7 @@
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;#34;prettier&amp;#34;: &amp;#34;prettier --write \&amp;#34;src/**/*.{ts,js,tsx}\&amp;#34;&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;#34;cypress:open&amp;#34;: &amp;#34;cypress open&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;#34;cypress:run&amp;#34;: &amp;#34;cypress run --browser chrome&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    &amp;#34;cypress:serve&amp;#34;: &amp;#34;http-server dist -p 9000 -P http://localhost:9000? -c-1&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    &amp;#34;cypress:serve&amp;#34;: &amp;#34;http-server dist -p 9000 -P http://127.0.0.1:9000? -c-1&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;#34;dependencies&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;#34;@pixi/app&amp;#34;: &amp;#34;^7.4.2&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So if you&amp;rsquo;re having similar issues and use a similar setup, give that a try.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Notes on Self Hosting a Bluesky PDS Alongside Other Services</title>
      <link>https://cprimozic.net/notes/posts/notes-on-self-hosting-bluesky-pds-alongside-other-services/</link>
      <pubDate>Wed, 13 Nov 2024 19:03:37 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/notes-on-self-hosting-bluesky-pds-alongside-other-services/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve recently set up a Bluesky Personal Data Server (PDS) to store the data for my Bluesky account. I wanted to host it on my server alongside the many other web apps, databases, and many other services. I additionally wanted to use my top-level domain as my handle.&lt;/p&gt;
&lt;p&gt;I started out following the install guide on the official &lt;a href=&#34;https://github.com/bluesky-social/pds&#34;&gt;PDS repo&lt;/a&gt; and it initially started out pretty well. However, I pretty quickly ran into some issues where the default config didn&amp;rsquo;t work for me.&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;ve recently set up a Bluesky Personal Data Server (PDS) to store the data for my Bluesky account. I wanted to host it on my server alongside the many other web apps, databases, and many other services. I additionally wanted to use my top-level domain as my handle.&lt;/p&gt;
&lt;p&gt;I started out following the install guide on the official &lt;a href=&#34;https://github.com/bluesky-social/pds&#34;&gt;PDS repo&lt;/a&gt; and it initially started out pretty well. However, I pretty quickly ran into some issues where the default config didn&amp;rsquo;t work for me.&lt;/p&gt;
&lt;p&gt;I eventually managed to solve all of the issues and get it running smoothly, and I figured I&amp;rsquo;d record my experience here for anyone else struggling with similar problems.&lt;/p&gt;
&lt;h2 id=&#34;manual-container-management&#34;&gt;Manual Container Management&lt;/h2&gt;
&lt;p&gt;As I mentioned previously, I started out by running the default &lt;code&gt;installer.sh&lt;/code&gt; script which worked most of the way, but some of the Docker containers it spawned failed to start. Additionally, when I tried to set up an account through that script after the main services were initialized, I got 400/500 server errors and it failed.&lt;/p&gt;
&lt;p&gt;The default install script for the PDS (as of November 2024 anyway) sets up the following services launched as Docker containers managed by docker-compose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The PDS itself&lt;/li&gt;
&lt;li&gt;A Caddy reverse proxy&lt;/li&gt;
&lt;li&gt;&lt;code&gt;containrrr/watchtower&lt;/code&gt; which seems to facilitate zero-downtime upgrades&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It has code to auto-install Docker and some other packages, pull images, and launch containers. The &lt;code&gt;compose.yml&lt;/code&gt; file that it uses can be found here: &lt;a href=&#34;https://github.com/bluesky-social/pds/blob/main/compose.yaml&#34;&gt;https://github.com/bluesky-social/pds/blob/main/compose.yaml&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For my server, I already had a NGINX reverse proxy set up which I use as the front door for all my services. The default setup that &lt;code&gt;installer.sh&lt;/code&gt; uses launches the Caddy container on the host network and has it try to listen on port 80/443, so that conflicted with NGINX and caused Caddy to fail to start.&lt;/p&gt;
&lt;p&gt;To work around these issues, I tore down and deleted all of the auto-launched containers and created a custom &lt;code&gt;compose.yml&lt;/code&gt; file that skips the Caddy reverse proxy and watchtower containers entirely, only launching the PDS. It also sets up the PDS to expose itself on a different port than 3000 since I was already using that for a different service.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what my &lt;code&gt;compose.yml&lt;/code&gt; file looks like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;version&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;3.9&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;pds&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;container_name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;pds&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ghcr.io/bluesky-social/pds:0.4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;restart&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;unless-stopped&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#f92672&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bind&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;source&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/opt/pds&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;target&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/pds&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;6010:3000&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;env_file&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ae81ff&#34;&gt;/opt/pds/pds.env&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One other thing to note is that by default, the install script creates the data directory on the host at &lt;code&gt;/pds&lt;/code&gt;. This is where all of the state for your PDS lives (and is probably a good thing to set up automated backups for).&lt;/p&gt;
&lt;p&gt;I moved it to &lt;code&gt;/opt/pds&lt;/code&gt; to fit better with the rest of my server setup and updated the volume mount to match, but you can skip that if you prefer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: If you do change the location of this directory, &lt;em&gt;do not&lt;/em&gt; update the paths in your &lt;code&gt;pds.env&lt;/code&gt; file accordingly. These paths are read from inside the container where it is mapped to &lt;code&gt;/pds&lt;/code&gt; no matter where the directory is located on the host, so leave it as is.&lt;/p&gt;
&lt;p&gt;I put the &lt;code&gt;compose.yml&lt;/code&gt; file in a &lt;code&gt;bsky&lt;/code&gt; directory and can launch it by running &lt;code&gt;docker compose up -d&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;nginx-reverse-proxy&#34;&gt;NGINX Reverse Proxy&lt;/h2&gt;
&lt;p&gt;Once I had the manual &lt;code&gt;compose.yml&lt;/code&gt; file set up, I had the PDS container up and healthy and exposed on port 6010. Now, I needed to configure my NGINX reverse proxy to expose that to the internet.&lt;/p&gt;
&lt;p&gt;One caveat was that I had other things available on the domain I wanted to use (ameo.dev) and couldn&amp;rsquo;t just let the PDS take over the whole domain - which is the behavior that the install script expects.&lt;/p&gt;
&lt;p&gt;I did some research and discovered that luckily, the PDS only needs to control a few paths - so I could leave all my other sites + services on the domain intact and just map a few paths through.&lt;/p&gt;
&lt;p&gt;The paths that it needs are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/xrpc/&lt;/code&gt;: This is the main path under which all of the APIs/RPCs that the PDS exposes are available&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/.well-known/atproto-did&lt;/code&gt;: This is used to verify domain ownership in addition to DNS methods
&lt;ul&gt;
&lt;li&gt;Note: You may not need this if you use DNS to verify your domain ownership. However, I was having issues with verifying my domain/handle and set up both methods.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s the NGINX config I ended up with to expose these two paths and route them through to the PDS:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;server {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    server_name ameo.dev www.ameo.dev;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    # ... rest of your server config ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    location /xrpc {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        proxy_pass http://localhost:6010;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        proxy_set_header X-Forwarded-Proto https;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        proxy_set_header Host            $host;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        proxy_set_header Upgrade $http_upgrade;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        proxy_set_header Connection $connection_upgrade;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    location /.well-known/atproto-did {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        proxy_pass http://localhost:6010/.well-known/atproto-did;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        proxy_set_header Host            $host;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    # ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s pretty straightforward, but there are a few pieces that are important.&lt;/p&gt;
&lt;p&gt;For the &lt;code&gt;well-known/atproto-did&lt;/code&gt; path, the &lt;code&gt;Host&lt;/code&gt; header &lt;em&gt;needs&lt;/em&gt; to be forwarded through in order for it to work. The handler for that path in the PDS looks at the &lt;code&gt;Host&lt;/code&gt; header to figure out which DID to return, so if it&amp;rsquo;s not set the path will return an error/not found response.&lt;/p&gt;
&lt;p&gt;I also added this line to my &lt;code&gt;pds.env&lt;/code&gt; file to try to make the &lt;code&gt;/.well-known/atproto-did&lt;/code&gt; route work:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PDS_SERVICE_HANDLE_DOMAINS&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;.ameo.dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(replace with your domain of course)&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if this was actually necessary, but if you are still having issues with that route even after forwarding the Host header correctly then maybe give it a try.&lt;/p&gt;
&lt;p&gt;Additionally, the &lt;code&gt;Upgrade&lt;/code&gt; and &lt;code&gt;Connection&lt;/code&gt; headers &lt;em&gt;need&lt;/em&gt; to be set in order for the PDS to function. These support websocket proxying - and you might need to set some other config in your reverse proxy in order to get websocket proxying to work.&lt;/p&gt;
&lt;p&gt;I initially figured that it was a similar situation to Mastodon and the websocket functionality was optional and only used for things like realtime notifications and live feeds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;However, I learned that having a working websocket feed to your PDS is 100% necessary in order for it to work correctly and interface with the rest of the Bluesky network&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;To make sure this was working, I used an &lt;a href=&#34;https://piehost.com/websocket-tester&#34;&gt;online websocket tester&lt;/a&gt; and put this URL in (modify to your domain if you use it): &lt;code&gt;wss://ameo.dev/xrpc/com.atproto.sync.subscribeRepos?cursor=0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If your proxy is set up correctly, it should report that the connection is successfully established when you test it.&lt;/p&gt;
&lt;h2 id=&#34;issues-with-top-level-domain-handles&#34;&gt;Issues with Top-Level Domain Handles&lt;/h2&gt;
&lt;p&gt;Once I had everything up and running and proxied out to the public internet, I was able to set up an account using the &lt;code&gt;pdsadmin&lt;/code&gt; command on the server - mostly following the official docs in the PDS repo&amp;rsquo;s guide.&lt;/p&gt;
&lt;p&gt;I made it as far as logging in through the main bsky.app Web UI and saw that requests were successfully going to and getting answered by my PDS on my custom domain. However, there were issues with my account that quickly became apparent.&lt;/p&gt;
&lt;p&gt;On my profile page, my handle was showing up as &amp;ldquo;Invalid Handle ⚠`. I had already performed the steps to set up my domain&amp;rsquo;s DNS to verify the domain, but it didn&amp;rsquo;t seem to be working. I tried changing my handle between my main domain and subdomain to no avail.&lt;/p&gt;
&lt;p&gt;For me, the cause turned out to be that I didn&amp;rsquo;t have the websocket proxying working properly. As I mentioned in the previous section, a working websocket connections is necessary for a PDS to work. Once I set that up, I ran this command on my server to trigger the network to re-scrape my PDS:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pdsadmin request-crawl bsky.network&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once I did all that, my &amp;ldquo;Invalid Handle&amp;rdquo; issue went away after less than a minute and my handle was successfully set to &lt;code&gt;ameo.dev&lt;/code&gt; - just what I wanted.&lt;/p&gt;
&lt;h3 id=&#34;other-debugging-resources&#34;&gt;Other Debugging Resources&lt;/h3&gt;
&lt;p&gt;While trying to figure out this issue, I came across some resources that might be useful to you if you&amp;rsquo;re having similar issues verifying your handle or domain.&lt;/p&gt;
&lt;p&gt;The first is an official debugging app to help check your domain verification and handle status:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://bsky-debug.app/&#34;&gt;https://bsky-debug.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The second is a semi-official Discord server with a community of people to help debug issues with self-hosted Bluesky PDSes:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://discord.gg/NDATMNx2&#34;&gt;https://discord.gg/NDATMNx2&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t end up chatting there myself, but there seems to be a pretty active group who are eager to help out fellow PDS admins with their issues.&lt;/p&gt;
&lt;h2 id=&#34;top-level-domain-handle-issues&#34;&gt;Top-Level Domain Handle Issues&lt;/h2&gt;
&lt;p&gt;One other thing to note is that &lt;strong&gt;it seems to currently be impossible to start off with a top-level domain as your handle for the initial account created on a self-hosted PDS&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As I understand it, it&amp;rsquo;s just a limitation of the current setup and not something inherent with the AT Protocol or Bluesky.&lt;/p&gt;
&lt;p&gt;To work around this, I started off by using a subdomain as a handle (I used casey.ameo.dev) and then later switching it to my top-level domain (ameo.dev) via the settings in the main bsky.app web UI. Once I got my websocket proxying and DNS set up, it worked just fine.&lt;/p&gt;
&lt;h2 id=&#34;smtp-server&#34;&gt;SMTP Server&lt;/h2&gt;
&lt;p&gt;One last thing to note is that I needed to set up a custom SMTP server to make email verification work. To be honest, I&amp;rsquo;m not 100% sure if email verification is even required or not since this is a self-hosted PDS and I&amp;rsquo;m the only user. However, I did end up setting it up to make the message in the UI go away if nothing else.&lt;/p&gt;
&lt;p&gt;I initially tried setting up a free account through &lt;a href=&#34;https://resend.com/&#34;&gt;Resend&lt;/a&gt; as suggested in the official PDS repo docs. However, I already have MX records set for my domain since I used it for my own email, so I didn&amp;rsquo;t want to mess with them in order to verify my domain through Resend.&lt;/p&gt;
&lt;p&gt;For my specific case, I manage email for my domain through Google Workspace. Because of this, I was able to use Google&amp;rsquo;s SMTP relay to send emails and it worked fine.&lt;/p&gt;
&lt;p&gt;I added these lines to my &lt;code&gt;pds.env&lt;/code&gt; file to make that happen:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PDS_EMAIL_SMTP_URL&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;smtp://smtp-relay.gmail.com/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PDS_EMAIL_FROM_ADDRESS&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;casey@ameo.dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once that was set, my verification email was delivered successfully and I was able to verify it.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s all I have to note. It was a bit tricky but now that I&amp;rsquo;ve figured it all out, my PDS has been running smoothly for over a day and I&amp;rsquo;m interacting with Bluesky completely normally.&lt;/p&gt;
&lt;p&gt;I hope this writeup helps save you some effort if you&amp;rsquo;re having these or similar issues yourself!&lt;/p&gt;
&lt;p&gt;Feel free to tag or message me on Bluesky (@ameo.dev) if you have any questions as well :)&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Artifacts Caused by Negative Color Sampling in WebGL</title>
      <link>https://cprimozic.net/notes/posts/fixing-webgl-negative-color-sampling/</link>
      <pubDate>Sun, 10 Nov 2024 16:22:35 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-webgl-negative-color-sampling/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was working on a screen-space reflection shader.  This involves reading pixels out of the main scene&amp;rsquo;s framebuffer/texture to be reflected.&lt;/p&gt;
&lt;p&gt;I was seeing strange grainy-looking artifacts showing up in my reflections that I couldn&amp;rsquo;t trace to anything in my shader code.  Here&amp;rsquo;s what they looked like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/cmg.png&#34; alt=&#34;A screenshot of a scene rendered with Three.JS using a screen-space reflections shader.  It shows a turquoise rectangular prism floating above a green rectangular platform with a reflection of the prism visible on the platform below.  The reflection has many grainy spots across it that looks like static.&#34;&gt;&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was working on a screen-space reflection shader.  This involves reading pixels out of the main scene&amp;rsquo;s framebuffer/texture to be reflected.&lt;/p&gt;
&lt;p&gt;I was seeing strange grainy-looking artifacts showing up in my reflections that I couldn&amp;rsquo;t trace to anything in my shader code.  Here&amp;rsquo;s what they looked like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/cmg.png&#34; alt=&#34;A screenshot of a scene rendered with Three.JS using a screen-space reflections shader.  It shows a turquoise rectangular prism floating above a green rectangular platform with a reflection of the prism visible on the platform below.  The reflection has many grainy spots across it that looks like static.&#34;&gt;&lt;/p&gt;
&lt;p&gt;One thing I noticed was that the artifacts seemed tied to specific positions in the texture being read, meaning that the artifacts were tied to individual texels in the texture being read from.&lt;/p&gt;
&lt;p&gt;Another interesting thing I noticed was that not all of the reflections had these artifacts; it depended on their color.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;In my shader, I was returning sentinel values of &lt;code&gt;-1&lt;/code&gt; in my reflection-finding function to indicate that there was nothing to reflect.  I then had a check in the main function like this to just use the existing fragment color in that case:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; raymarchReflections(...) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;hasReflection) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1.&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; main() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; reflectedColor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; raymarchReflections(...);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// if no reflections found, pass through existing color&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (reflectedColor.r &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    gl_FragColor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; diffuse;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After a good deal of debugging, I discovered that changing the check to this made the artifacts go away completely:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (reflectedColor.r &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1.&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;As far as I can tell, there is just inherent imprecision when reading values from certain textures in WebGL.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When reading some parts of the image with small values for some channel (red in my case), the sampled value is sometimes slightly negative.  This also explains why the artifacts only showed up in some of the reflections.  The blue one in the screenshot above had a color of &lt;code&gt;#09f0f9&lt;/code&gt; which has a small but non-zero red channel.  Other reflections with larger red channel values didn&amp;rsquo;t suffer from the imprecision issue.&lt;/p&gt;
&lt;p&gt;For my case, I was using WebGL textures with a type of &lt;code&gt;HALF_FLOAT&lt;/code&gt; and an internal format of &lt;code&gt;RGBA16F&lt;/code&gt;.  It&amp;rsquo;s possible that this is the reason for the imprecision and that this issue might not happen if storing textures in some other format like unsigned integer.  It might also be a quirk of my particular hardware, drivers, WebGL params, or one of the other hundreds of variables in play.&lt;/p&gt;
&lt;p&gt;Anyway, the main thing I learned is that sampling values from textures isn&amp;rsquo;t guaranteed to be exact and to take more care when using special-case values like these in shaders.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>A Small Change to Significantly Improve Triplanar Mapping</title>
      <link>https://cprimozic.net/notes/posts/a-small-change-to-improve-triplanar-mapping/</link>
      <pubDate>Sun, 01 Sep 2024 15:25:14 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/a-small-change-to-improve-triplanar-mapping/</guid>
      <description>&lt;p&gt;Triplanar Mapping is a method I make use of all the time in my 3D projects.  Over time, I&amp;rsquo;ve experimented with tweaks and alterations to it with the goal of making it look better, run more performantly, and be useful in more situations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The main change I present here is switching from using a linear mix of texture weights from each axis to a non-linear mix.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To explain what I mean by this, I&amp;rsquo;ll just show some code.&lt;/p&gt;</description>
      <content>&lt;p&gt;Triplanar Mapping is a method I make use of all the time in my 3D projects.  Over time, I&amp;rsquo;ve experimented with tweaks and alterations to it with the goal of making it look better, run more performantly, and be useful in more situations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The main change I present here is switching from using a linear mix of texture weights from each axis to a non-linear mix.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To explain what I mean by this, I&amp;rsquo;ll just show some code.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a basic triplanar mapping implementation:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; triplanarTexture(&lt;span style=&#34;color:#66d9ef&#34;&gt;sampler2D&lt;/span&gt; map, &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; pos, &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; normal) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; weights &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; abs(normal);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  weights &lt;span style=&#34;color:#f92672&#34;&gt;/=&lt;/span&gt; (weights.x &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; weights.y &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; weights.z);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; outColor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.yz) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.x;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.zx) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.y;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.xy) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.z;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; outColor;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And here&amp;rsquo;s a modified version that uses a non-linear mix of the same texture samples:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; triplanarTexture(&lt;span style=&#34;color:#66d9ef&#34;&gt;sampler2D&lt;/span&gt; map, &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; pos, &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; normal) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; weights &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; abs(normal);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// non-linear scaling of weights&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  weights &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pow(weights, &lt;span style=&#34;color:#ae81ff&#34;&gt;8&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  weights &lt;span style=&#34;color:#f92672&#34;&gt;/=&lt;/span&gt; (weights.x &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; weights.y &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; weights.z);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; outColor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.yz) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.x;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.zx) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.y;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.xy) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.z;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; outColor;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It turns out that this one-line change has a lot of impact - especially for certain types of models/scenes.&lt;/p&gt;
&lt;p&gt;First of all, it makes the transition regions smaller and reduces the amount of visible overlap between different planes.  This is most obvious on smooth meshes like this sphere:&lt;/p&gt;
&lt;iframe src=&#34;https://homepage-external-mixins.ameo.design/triplanar_mapping_enhancement.html&#34; loading=&#34;lazy&#34; style=&#34;width: 100%;aspect-ratio: 847/812;overflow:hidden;display: block;outline:none;border:none;box-sizing:border-box; margin-left: auto; margin-right: auto&#34;&gt;&lt;/iframe&gt;
&lt;p&gt;The left side shows the result of triplanar mapping with default linear weights, and the right shows the result when using the &lt;code&gt;pow(weights, 8)&lt;/code&gt; change above.&lt;/p&gt;
&lt;p&gt;As you can see, there is no more visible layering of the texture.  Instead, there are some small areas where the texture faces smoothly between two different planes which looks much better.&lt;/p&gt;
&lt;h2 id=&#34;performance-improvements&#34;&gt;Performance Improvements&lt;/h2&gt;
&lt;p&gt;In addition to the visual improvements, this change also opens up the possibility for some significant performance improvements.&lt;/p&gt;
&lt;p&gt;Applying this non-linear transformation to weights serves to drive small weights to zero and large weights to one.  This makes it possible to ignore small weights entirely in some cases since they go from making a small impact to a negligible/unnoticeable impact on the output.&lt;/p&gt;
&lt;p&gt;So to optimize the shader, it&amp;rsquo;s possible to skip some texture lookups entirely if the weight is small enough:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-glsl&#34; data-lang=&#34;glsl&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; triplanarTexture(&lt;span style=&#34;color:#66d9ef&#34;&gt;sampler2D&lt;/span&gt; map, &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; pos, &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; normal) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec3&lt;/span&gt; weights &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; abs(normal);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// non-linear scaling of weights&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  weights &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pow(weights, &lt;span style=&#34;color:#ae81ff&#34;&gt;8&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  weights &lt;span style=&#34;color:#f92672&#34;&gt;/=&lt;/span&gt; (weights.x &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; weights.y &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; weights.z);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt; outColor &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;vec4&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (weights.x &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.01&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.yz) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.x;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (weights.y &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.01&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.zx) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.y;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (weights.z &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.01&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outColor &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; texture2D(map, pos.xy) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weights.z;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; outColor;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This optimization would be fine to include without the non-linear weights transformation, but it would rarely activate.  In the modified version, there is much more area where individual weights end up being very close to zero.&lt;/p&gt;
&lt;p&gt;Also, the higher the power chosen to raise the weights to, the more aggressive the sharpening will be and the more effective the optimization will become.  I usually choose as high of a power as I can that avoids any visible discontinuities in the texture.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;And that&amp;rsquo;s it!  I highly recommend giving this a try in your own projects when using triplanar mapping.  It makes an already extremely effective technique even better.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>A Basic Graphviz Dark Theme Config</title>
      <link>https://cprimozic.net/notes/posts/basic-graphviz-dark-theme-config/</link>
      <pubDate>Thu, 20 Jun 2024 16:14:03 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/basic-graphviz-dark-theme-config/</guid>
      <description>&lt;p&gt;I regularly use Graphviz to generate&amp;hellip; graph visualizations.  Since my website and blog are all dark-themed, I usually want to set it up so that the generated SVG is dark-themed as well.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the basic config I use to make graphviz produce outputs with dark backgrounds and light content:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dot&#34; data-lang=&#34;dot&#34;&gt;digraph G {
  bgcolor=&amp;#34;#181818&amp;#34;;

  node [
    fontcolor = &amp;#34;#e6e6e6&amp;#34;,
    style = filled,
    color = &amp;#34;#e6e6e6&amp;#34;,
    fillcolor = &amp;#34;#333333&amp;#34;
  ]

  edge [
    color = &amp;#34;#e6e6e6&amp;#34;,
    fontcolor = &amp;#34;#e6e6e6&amp;#34;
  ]

  # The rest of your code goes here...
  A -&amp;gt; B;
  B -&amp;gt; C;
  C -&amp;gt; A;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That results in an output that looks like this:&lt;/p&gt;</description>
      <content>&lt;p&gt;I regularly use Graphviz to generate&amp;hellip; graph visualizations.  Since my website and blog are all dark-themed, I usually want to set it up so that the generated SVG is dark-themed as well.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the basic config I use to make graphviz produce outputs with dark backgrounds and light content:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-dot&#34; data-lang=&#34;dot&#34;&gt;digraph G {
  bgcolor=&amp;#34;#181818&amp;#34;;

  node [
    fontcolor = &amp;#34;#e6e6e6&amp;#34;,
    style = filled,
    color = &amp;#34;#e6e6e6&amp;#34;,
    fillcolor = &amp;#34;#333333&amp;#34;
  ]

  edge [
    color = &amp;#34;#e6e6e6&amp;#34;,
    fontcolor = &amp;#34;#e6e6e6&amp;#34;
  ]

  # The rest of your code goes here...
  A -&amp;gt; B;
  B -&amp;gt; C;
  C -&amp;gt; A;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That results in an output that looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/c9k.svg&#34; alt=&#34;A simple graphviz-generated directed graph visualization with a dark theme.  There are three nodes pointing to each other in a triangle.  The nodes, text, and edges are white while the background is dark.&#34;&gt;&lt;/p&gt;
&lt;p&gt;You can of course tweak the hex color to your liking.  There may be some fancy elements you can add that this doesn&amp;rsquo;t cover, but it should be all you need for most cases.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing `regl` Error Invalid Dynamic Attribute</title>
      <link>https://cprimozic.net/notes/posts/fixing-regl-error-invalid-dynamic-attribute/</link>
      <pubDate>Wed, 01 May 2024 01:43:06 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-regl-error-invalid-dynamic-attribute/</guid>
      <description>&lt;p&gt;Recently when working with the &lt;a href=&#34;https://github.com/regl-project/regl&#34;&gt;&lt;code&gt;regl&lt;/code&gt;&lt;/a&gt; WebGL wrapper library, I encountered a weird error that I didn&amp;rsquo;t understand the cause of:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(regl) invalid dynamic attribute &amp;quot;position&amp;quot; in command&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I checked my code, and I was indeed passing a &lt;code&gt;position&lt;/code&gt; attribute and the data array I was using to construct the buffer looked correct.  The error also happened intermittently, happening after my application hot-reloaded.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;It turns out that I had accidentally created two &lt;code&gt;regl&lt;/code&gt; instances and was trying to use buffers allocated on one with commands created on a different one.&lt;/p&gt;</description>
      <content>&lt;p&gt;Recently when working with the &lt;a href=&#34;https://github.com/regl-project/regl&#34;&gt;&lt;code&gt;regl&lt;/code&gt;&lt;/a&gt; WebGL wrapper library, I encountered a weird error that I didn&amp;rsquo;t understand the cause of:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(regl) invalid dynamic attribute &amp;quot;position&amp;quot; in command&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I checked my code, and I was indeed passing a &lt;code&gt;position&lt;/code&gt; attribute and the data array I was using to construct the buffer looked correct.  The error also happened intermittently, happening after my application hot-reloaded.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;It turns out that I had accidentally created two &lt;code&gt;regl&lt;/code&gt; instances and was trying to use buffers allocated on one with commands created on a different one.&lt;/p&gt;
&lt;p&gt;When my app hot-reloaded, it was incorrectly creating a new instance for the command while re-using the old buffers.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I corrected the issue that was causing duplicate &lt;code&gt;regl&lt;/code&gt; instances from getting created, and the error went away and my app worked again!&lt;/p&gt;
&lt;p&gt;It was a bit tricky to figure out due to the rather vague error message, though.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Sveltekit Failed to Load URL __SERVER__/internal.js</title>
      <link>https://cprimozic.net/notes/posts/fixing-sveltekit-cannot-find-module-__server__-internal/</link>
      <pubDate>Tue, 30 Apr 2024 12:36:56 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-sveltekit-cannot-find-module-__server__-internal/</guid>
      <description>&lt;p&gt;I was working on a new SvelteKit project with Svelte 5 recently, and I kept getting an error that prevented anything from loading in the browser:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[vite] Pre-transform error: Failed to load url __SERVER__/internal.js (resolved id: /Users/casey/osu-embeddings/frontend/.svelte-kit/generated/server/internal.js) in /Users/casey/osu-embeddings/frontend/node_modules/@sveltejs/kit/src/runtime/server/index.js. Does the file exist?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[vite] Error when evaluating SSR module /node_modules/@sveltejs/kit/src/runtime/server/index.js: failed to import &amp;#34;__SERVER__/internal.js&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|- Error: Cannot find module &amp;#39;__SERVER__/internal.js&amp;#39; imported from &amp;#39;/Users/casey/osu-embeddings/frontend/node_modules/@sveltejs/kit/src/runtime/server/index.js&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at nodeImport (file:///Users/casey/osu-embeddings/frontend/node_modules/vite/dist/node/chunks/dep-DkOS1hkm.js:55067:25)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at ssrImport (file:///Users/casey/osu-embeddings/frontend/node_modules/vite/dist/node/chunks/dep-DkOS1hkm.js:54976:30)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at eval (/Users/casey/osu-embeddings/frontend/node_modules/@sveltejs/kit/src/runtime/server/index.js:5:37)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async instantiateModule (file:///Users/casey/osu-embeddings/frontend/node_modules/vite/dist/node/chunks/dep-DkOS1hkm.js:55036:9)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;It turns out that I had a broken symlink in my &lt;code&gt;static/&lt;/code&gt; directory. I had cloned the project down to a new computer, and the file only existed on the old one.&lt;/p&gt;</description>
      <content>&lt;p&gt;I was working on a new SvelteKit project with Svelte 5 recently, and I kept getting an error that prevented anything from loading in the browser:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[vite] Pre-transform error: Failed to load url __SERVER__/internal.js (resolved id: /Users/casey/osu-embeddings/frontend/.svelte-kit/generated/server/internal.js) in /Users/casey/osu-embeddings/frontend/node_modules/@sveltejs/kit/src/runtime/server/index.js. Does the file exist?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[vite] Error when evaluating SSR module /node_modules/@sveltejs/kit/src/runtime/server/index.js: failed to import &amp;#34;__SERVER__/internal.js&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|- Error: Cannot find module &amp;#39;__SERVER__/internal.js&amp;#39; imported from &amp;#39;/Users/casey/osu-embeddings/frontend/node_modules/@sveltejs/kit/src/runtime/server/index.js&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at nodeImport (file:///Users/casey/osu-embeddings/frontend/node_modules/vite/dist/node/chunks/dep-DkOS1hkm.js:55067:25)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at ssrImport (file:///Users/casey/osu-embeddings/frontend/node_modules/vite/dist/node/chunks/dep-DkOS1hkm.js:54976:30)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at eval (/Users/casey/osu-embeddings/frontend/node_modules/@sveltejs/kit/src/runtime/server/index.js:5:37)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async instantiateModule (file:///Users/casey/osu-embeddings/frontend/node_modules/vite/dist/node/chunks/dep-DkOS1hkm.js:55036:9)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;It turns out that I had a broken symlink in my &lt;code&gt;static/&lt;/code&gt; directory. I had cloned the project down to a new computer, and the file only existed on the old one.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I created a file at the location pointed to by the broken symlink which made it valid. Then, when I re-started the server, things worked.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible that this was a fluke and some other thing happened which caused this error, but if you&amp;rsquo;re seeing it and have symlinks in your project, it might be worth looking into that.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Svelte VS Code `e.children.findLastIndex` Is Not a Function</title>
      <link>https://cprimozic.net/notes/posts/fixing-svelte-vs-code-e.children.findlastindex-is-not-a-function/</link>
      <pubDate>Mon, 15 Apr 2024 16:21:08 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-svelte-vs-code-e.children.findlastindex-is-not-a-function/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I recently created a new SvelteKit project using the Svelte 5 preview and kept getting an error at the beginning of every Svelte component:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/c3g.jpg&#34; alt=&#34;A screenshot of VS Code showing an error at the first character of a Svelte file, marked with a red squiggly line.  The error message reads “e.children.findLastIndex is not a function svelte”&#34;&gt;&lt;/p&gt;
&lt;p&gt;The error was &lt;code&gt;e.children.findLastIndex is not a function (svelte)&lt;/code&gt;. It was showing up even on the basic SvelteKit skeleton starter project as soon as I added a &lt;code&gt;&amp;lt;style&amp;gt;&amp;lt;/style&amp;gt;&lt;/code&gt; block.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I recently created a new SvelteKit project using the Svelte 5 preview and kept getting an error at the beginning of every Svelte component:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/c3g.jpg&#34; alt=&#34;A screenshot of VS Code showing an error at the first character of a Svelte file, marked with a red squiggly line.  The error message reads “e.children.findLastIndex is not a function svelte”&#34;&gt;&lt;/p&gt;
&lt;p&gt;The error was &lt;code&gt;e.children.findLastIndex is not a function (svelte)&lt;/code&gt;. It was showing up even on the basic SvelteKit skeleton starter project as soon as I added a &lt;code&gt;&amp;lt;style&amp;gt;&amp;lt;/style&amp;gt;&lt;/code&gt; block.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I looked around online, and I didn&amp;rsquo;t see anyone having this problem. This led me to believe that it was likely an issue with my local environment or configuration.&lt;/p&gt;
&lt;p&gt;I looked in my VS Code configuration and found this line:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;svelte.language-server.runtime&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/Users/casey/.nvm/versions/node/v16.13.0/bin/node&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I didn&amp;rsquo;t remember setting that or why I did it. However, once I removed it and reloaded my VS Code, the error went away!&lt;/p&gt;
&lt;p&gt;So if you&amp;rsquo;re having this issue yourself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check for a config option similar to this in your VS Code settings.json and delete it if you find one&lt;/li&gt;
&lt;li&gt;Try updating your Node.JS to a new version. I&amp;rsquo;m using version 21.1.0 myself.&lt;/li&gt;
&lt;/ul&gt;
</content>
    </item>
    
    <item>
      <title>Investigating Bogus Plausible Analytics Traffic</title>
      <link>https://cprimozic.net/notes/posts/investigating-strange-artificial-plausible-analytics-events/</link>
      <pubDate>Wed, 20 Mar 2024 10:42:24 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/investigating-strange-artificial-plausible-analytics-events/</guid>
      <description>&lt;p&gt;I run self-hosted &lt;a href=&#34;https://plausible.io/&#34;&gt;Plausible analytics&lt;/a&gt; to keep track of how many people are visiting my various websites and web apps. I&amp;rsquo;m largely very happy with it - it gives me all of the info I need with only a miniscule JS payload, no cookies or other invasive tracking, and full control over the data.&lt;/p&gt;
&lt;p&gt;However, recently I&amp;rsquo;ve been running into an issue with fake/bogus traffic getting submitted for my personal homepage. Usually, I&amp;rsquo;m more than happy to see a bump in traffic to one of my sites, but something was off about it this time.&lt;/p&gt;</description>
      <content>&lt;p&gt;I run self-hosted &lt;a href=&#34;https://plausible.io/&#34;&gt;Plausible analytics&lt;/a&gt; to keep track of how many people are visiting my various websites and web apps. I&amp;rsquo;m largely very happy with it - it gives me all of the info I need with only a miniscule JS payload, no cookies or other invasive tracking, and full control over the data.&lt;/p&gt;
&lt;p&gt;However, recently I&amp;rsquo;ve been running into an issue with fake/bogus traffic getting submitted for my personal homepage. Usually, I&amp;rsquo;m more than happy to see a bump in traffic to one of my sites, but something was off about it this time.&lt;/p&gt;
&lt;p&gt;First, all of this traffic seemed to be limited to the homepage of my site. Traffic spikes usually correspond to some blog post I&amp;rsquo;ve written getting posted to Reddit or similar, so having everything limited to just the homepage was quite strange.&lt;/p&gt;
&lt;p&gt;Next, all of the traffic had no set referrer/source. This is strange for the same reason: usually people are coming to my sites from somewhere else rather than just typing in the URL manually.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve seen some spam analytics events in the past with their referrer header set to spam domains. The intention seemed to be to advertise to me in my analytics dashboard or something along those lines. That&amp;rsquo;s not the case here, though.&lt;/p&gt;
&lt;p&gt;When filtering my site&amp;rsquo;s traffic to only include events that match that criteria, it&amp;rsquo;s clear to see when this started happening in early March:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/byk.png&#34; alt=&#34;A screenshot of my Plausible analytics web UI showing traffic for my homepage website.  It’s filtered to show only traffic to the root of the website (path of “/”).  It shows a large increase in traffic to this page starting around March 6 with daily hits going from less than 20 to over 100 on multiple days.  It also shows that the top source for this traffic is “Direct / None”.&#34;&gt;&lt;/p&gt;
&lt;p&gt;This was a very strange pattern that I hadn&amp;rsquo;t seen before, so I dug in deeper.&lt;/p&gt;
&lt;p&gt;Since I host my homepage myself, I have direct access to the raw server logs for the NGINX webserver that serves it. Plausible submits all of its events to a single URL (&lt;code&gt;/api/event&lt;/code&gt;), so it was pretty easy to filter down the logs to find the Plausible requests.&lt;/p&gt;
&lt;p&gt;Since there were so many of these events coming in to the point where it was a large portion of my site&amp;rsquo;s total traffic, it was pretty easy to match the requests on the server to the traffic displayed in the Plausible UI. I identified several of these no-referrer homepage requests and quickly identified some other interesting patterns with those requests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They were all coming from different IP addresses.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This does make sense since Plausible suggested a wide spread of different countries that these requests were originating from. However, this is pretty surprising if this is automated traffic; it would require the perpetrator to have control over dozens-hundreds of unique IPs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They all had similar user agents.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s a random sampling of some of the user agents that I saw from these requests:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.1392.1086 Mobile Safari/537.36
Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.1010.1725 Mobile Safari/537.36
Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.5871.1821 Mobile Safari/537.36
Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.4338.1594 Mobile Safari/537.36
Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2734.1416 Mobile Safari/537.36
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There are some clear similarities between them, but also some randomized parts as well. This makes it clear that whatever is sending these events is at the minimum more sophisticated than some simple script.&lt;/p&gt;
&lt;p&gt;I searched my server logs for the string &lt;code&gt;Mobile Safari/537.36&lt;/code&gt;. There were a lot of results there other than the plausible requests. Although there were some that looked like legitimate user-originating traffic, it looked like the majority of these requests were coming from bots like &lt;code&gt;Googlebot&lt;/code&gt; or &lt;code&gt;PetalBot&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Although &lt;code&gt;Mobile Safari/537.36&lt;/code&gt; seems to be something that can come up in real user traffic, it also seems to be a very common thing to include in bot/scraper user agents. This lends even more credence to the idea that these events are fake and don&amp;rsquo;t represent real user traffic.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All of the IPs I checked originate from residential/non-datacenter sources&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I ran a collection of the IP addresses sending these requests through the &lt;a href=&#34;https://www.maxmind.com/en/geoip-demo&#34;&gt;Maxmind GeoIP service&lt;/a&gt;, and here are the results:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/byj.png&#34; alt=&#34;A screenshot of geo-IP search results for a subset of IPs submitting bogus events to my Plausible analytics.  It shows that the IPs trace back to a variety of locations across Europe and the rest of the world, a variety of different internet providers, and have connection types of “Cable/DSL” and “Corporate”&#34;&gt;&lt;/p&gt;
&lt;p&gt;As far as I can tell, these look like legitimate residential IPs that could belong to real users. I would have guessed that they would have belonged to public clouds or datacenters given the apparent automated source of the traffic, so this was pretty surprising to me.&lt;/p&gt;
&lt;p&gt;I know that there are available residential IP proxies that people use for scraping and similar use cases. It&amp;rsquo;s also possible that this traffic is coming from devices compromised by a virus or malicious browser extension. Both of these seem pretty extreme and unlikely to me, though, for something such at this.&lt;/p&gt;
&lt;p&gt;I did some basic googling of some of these IPs looking for entries in abuse blocklists or similar, but nothing came up. They looked entirely like real user IPs in every way I could see.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All of the IPs I checked only make requests to the Plausible event submission URL (&lt;code&gt;/api/event&lt;/code&gt;) without making &lt;em&gt;any&lt;/em&gt; other requests to my webserver&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was the smoking gun for me that indicated that this traffic was almost certainly not real. When I searched through the server logs, there would always be a single request from that IP. They didn&amp;rsquo;t even download the &lt;code&gt;plausible.js&lt;/code&gt; script that is used to kick off these requests!&lt;/p&gt;
&lt;p&gt;I checked multiple IP addresses and this was the case for all of them. I double-checked the logs of the CDN I use for some of the static assets on my site (it&amp;rsquo;s not used for the &lt;code&gt;index.html&lt;/code&gt; nor the &lt;code&gt;plausible.js&lt;/code&gt; script though) and there was no trace of those IPs there either.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s probably technically possible that these requests came from users that had my entire homepage cached from like a week ago, but that&amp;rsquo;s extremely unlikely and doesn&amp;rsquo;t seem remotely realistic.&lt;/p&gt;
&lt;p&gt;The most likely explanation for these requests I can think of is intentionally submitted artificial traffic from a relatively sophisticated source.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;So to summarize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I&amp;rsquo;m seeing a lot of fake traffic getting submitted to my Plausible analytics for one of my websites&lt;/li&gt;
&lt;li&gt;All of the requests are to the homepage, bounce immediately, and have no referrer/source listed&lt;/li&gt;
&lt;li&gt;The requests all come from unique residential-looking IPs&lt;/li&gt;
&lt;li&gt;The requests all have similar but randomized user agents&lt;/li&gt;
&lt;li&gt;There&amp;rsquo;s no other requests or traffic to other pages or content from any of these IPs whatsoever; they all just hit the Plausible event submission endpoint a single time each&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Very strange indeed! This leaves the one big question remaining: Why?&lt;/p&gt;
&lt;p&gt;I have no idea! This is my personal website that I mainly use as a portfolio and blog. I have no ads on the site at all, don&amp;rsquo;t sell anything on it, and have no paywalls or login system of any kind. It&amp;rsquo;s all static content, and the site itself is &lt;a href=&#34;https://github.com/ameobea/homepage&#34;&gt;open source&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If someone out there is trying to inflate my ego by making it look like hundreds of people are coming every day to stare at my homepage before leaving, I appreciate the thought! But now that I&amp;rsquo;m almost positive it&amp;rsquo;s fake, it&amp;rsquo;s much more annoying than anything else; I&amp;rsquo;d prefer to have an accurate as possible view of real traffic on my sites.&lt;/p&gt;
&lt;p&gt;Given how little of a reason for this traffic I can see, I still feel like there&amp;rsquo;s probably some explanation for this other than someone setting it up intentionally to give my website fake hits. Someone hotlinking or duping my site maybe, or perhaps some web cache or internet archive-like site acting in a weird way? Some kind of ad fraud bot that got lost or mis-targeted?&lt;/p&gt;
&lt;p&gt;In any case, I&amp;rsquo;m thoroughly intrigued and would love to know the truth. In my experience, these kinds of mysterious web traffic events tend to clear up on their own after a while, so I wouldn&amp;rsquo;t be surprised if this fake traffic just vanishes in a few days or weeks.&lt;/p&gt;
&lt;p&gt;If anyone has seen something similar or has any ideas, I&amp;rsquo;d be very curious to hear!&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Trying Out `sea-orm`</title>
      <link>https://cprimozic.net/notes/posts/trying-out-sea-orm/</link>
      <pubDate>Mon, 22 Jan 2024 13:56:15 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/trying-out-sea-orm/</guid>
      <description>&lt;p&gt;For a new project at my dayjob, I&amp;rsquo;ve had the opportunity to try out &lt;a href=&#34;https://www.sea-ql.org/SeaORM/&#34;&gt;&lt;code&gt;sea-orm&lt;/code&gt;&lt;/a&gt; for the database layer. In the past, I&amp;rsquo;ve tried out other Rust SQL solutions including &lt;a href=&#34;https://diesel.rs/&#34;&gt;&lt;code&gt;diesel&lt;/code&gt;&lt;/a&gt; and &lt;a href=&#34;https://github.com/jmoiron/sqlx&#34;&gt;&lt;code&gt;sqlx&lt;/code&gt;&lt;/a&gt;, so I have some context to compare this one to.&lt;/p&gt;
&lt;p&gt;At a high level, &lt;code&gt;sea-orm&lt;/code&gt; provides a fully-featured solution for managing your database setup in Rust. It provides a framework and CLI for setting up and maintaining migrations, code-gen&amp;rsquo;ing entities and relations, and writing + running queries. Like most other Rust DB options, it is fully typed and integration into Rust&amp;rsquo;s type system.&lt;/p&gt;</description>
      <content>&lt;p&gt;For a new project at my dayjob, I&amp;rsquo;ve had the opportunity to try out &lt;a href=&#34;https://www.sea-ql.org/SeaORM/&#34;&gt;&lt;code&gt;sea-orm&lt;/code&gt;&lt;/a&gt; for the database layer. In the past, I&amp;rsquo;ve tried out other Rust SQL solutions including &lt;a href=&#34;https://diesel.rs/&#34;&gt;&lt;code&gt;diesel&lt;/code&gt;&lt;/a&gt; and &lt;a href=&#34;https://github.com/jmoiron/sqlx&#34;&gt;&lt;code&gt;sqlx&lt;/code&gt;&lt;/a&gt;, so I have some context to compare this one to.&lt;/p&gt;
&lt;p&gt;At a high level, &lt;code&gt;sea-orm&lt;/code&gt; provides a fully-featured solution for managing your database setup in Rust. It provides a framework and CLI for setting up and maintaining migrations, code-gen&amp;rsquo;ing entities and relations, and writing + running queries. Like most other Rust DB options, it is fully typed and integration into Rust&amp;rsquo;s type system.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sea-orm&lt;/code&gt; is built on top of several other &lt;code&gt;sea-*&lt;/code&gt; crates that make up the ecosystem. They include crates like &lt;code&gt;sea-query&lt;/code&gt;, &lt;code&gt;sea-schema&lt;/code&gt;, &lt;code&gt;sea-orm-cli&lt;/code&gt;, and some internal crates like &lt;code&gt;sea-orm-migration&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: At the time of writing this (Jan. 2024), I don&amp;rsquo;t have extensive experience with or knowledge about &lt;code&gt;sea-orm&lt;/code&gt;; this is more of a &amp;ldquo;first impressions&amp;rdquo; writeup than a detailed review. Also, &lt;code&gt;sea-orm&lt;/code&gt; and the rest of the &lt;code&gt;sea-query&lt;/code&gt; ecosystem is under active development, so things may change significantly over time.&lt;/p&gt;
&lt;h2 id=&#34;setup&#34;&gt;Setup&lt;/h2&gt;
&lt;p&gt;For this project, we started with a fresh Postgres database with nothing in it. This is the best case for frameworks like these since they like to have full ownership + control over everything.&lt;/p&gt;
&lt;p&gt;I opted to go for the full setup with migrations, code-generated entities, and managed DB connections.&lt;/p&gt;
&lt;p&gt;One thing I noticed right away is that &lt;code&gt;sea-orm&lt;/code&gt; is pretty prescriptive/opinionated about the crate layout and setup of the different components. It expects you to create separate crates for both the migrations and entities and join them together into a workspace.&lt;/p&gt;
&lt;p&gt;For one thing, it&amp;rsquo;s set up to pull in &lt;code&gt;async-std&lt;/code&gt; by default for the migration crate. We (and pretty much everything else in the modern Rust ecosystem) use &lt;code&gt;tokio&lt;/code&gt; which isn&amp;rsquo;t compatible with &lt;code&gt;async-std&lt;/code&gt; for some things. I was able to manually change it to use &lt;code&gt;tokio&lt;/code&gt; without seeming to breaking anything, though.&lt;/p&gt;
&lt;p&gt;Having to split up the database functionality into multiple crates is a bit awkward for our setup. We have a very large workspace with several dozen different crates at the top level. The project we&amp;rsquo;re adding this database to has a crate of its own, so that means adding two additional crates to manage the database stuff and then importing them.&lt;/p&gt;
&lt;p&gt;Compare this to something like &lt;code&gt;diesel&lt;/code&gt;. It has a single &lt;code&gt;schema.rs&lt;/code&gt; file which is generated based on the schema detected from the database. It contains a bunch of macros which define the various tables available and DSL for constructing queries. Since this all uses procedural macros, that definitely contributes to much of Diesel&amp;rsquo;s reputation for slow compile times, though.&lt;/p&gt;
&lt;p&gt;It might be possible to set &lt;code&gt;sea-orm&lt;/code&gt; up to avoid these extra crates, but the docs I read definitely don&amp;rsquo;t have any info on that. &lt;code&gt;sea-orm-cli&lt;/code&gt; seems to require that at least the migrations are in their own crate, since it compiles it as a binary and runs it to run migrations.&lt;/p&gt;
&lt;h2 id=&#34;migrations&#34;&gt;Migrations&lt;/h2&gt;
&lt;p&gt;The reason for this is that migrations themselves are defined using &lt;code&gt;sea-orm&lt;/code&gt;&amp;rsquo;s builder syntax rather than as raw SQL files like other libraries. So the migrations you write look something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; manager
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .create_table(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Table::create()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .table(Session::Table)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .if_not_exists()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .col(ColumnDef::new(Session::Id).uuid().not_null().primary_key())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .col(ColumnDef::new(Session::UserId).string())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .col(ColumnDef::new(Session::UserEmail).string())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .col(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ColumnDef::new(Session::CreationTimestamp)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          .timestamp()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          .not_null(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .to_owned(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .&lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The main benefit of this, according to the &lt;code&gt;sea-orm&lt;/code&gt; docs, is that it allows migrations to be independent of the backend database used - so you can theoretically re-use your migrations across Postgres, MySQL, Sqlite, and any other backends supported by &lt;code&gt;sea-query&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Personally, I&amp;rsquo;m not a big fan of this. I find the builder syntax very verbose and hard to work with. &lt;code&gt;sea-orm&lt;/code&gt; does have support for running migration queries as raw SQL, but you have to write them as Rust strings in the code anyway, so you don&amp;rsquo;t get syntax highlighting in your editor, have to deal with annoying escaping issues, etc.&lt;/p&gt;
&lt;p&gt;In addition, I&amp;rsquo;m not sure just how &amp;ldquo;backend-independent&amp;rdquo; these migrations are. I tried to set up an index by calling the &lt;a href=&#34;https://docs.rs/sea-query/latest/sea_query/table/struct.TableCreateStatement.html#method.index&#34;&gt;&lt;code&gt;.index()&lt;/code&gt;&lt;/a&gt; method on a &lt;code&gt;TableCreateStatement&lt;/code&gt;. This caused my migrations to start failing with a very unhelpful error:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;syntax error at or near &amp;#34;(&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I eventually figured out that there&amp;rsquo;s a &lt;code&gt;--verbose&lt;/code&gt; flag on the migration binary which prints out the raw queries being run, which helped.&lt;/p&gt;
&lt;p&gt;It turns out that &lt;code&gt;.index()&lt;/code&gt; function is actually only supported for the MySQL backend. For other databases, you have to create indexes using &lt;code&gt;SchemaManager::create_index()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It was annoying having to spend the time to figure this all out when I could have done it in 15 seconds if I was using raw SQL.&lt;/p&gt;
&lt;p&gt;To &lt;code&gt;sea-orm&lt;/code&gt;&amp;rsquo;s credit, though, pretty much every feature you could want &lt;em&gt;is&lt;/em&gt; available through the Rust interface itself. Even exotic types, postgres-specific built-in functions, typed JSON/array columns and operators, and more are all there.&lt;/p&gt;
&lt;h2 id=&#34;entities&#34;&gt;Entities&lt;/h2&gt;
&lt;p&gt;As I mentioned before, &lt;code&gt;sea-orm&lt;/code&gt; handles auto-generating models/entities based on the schema detected from the database.&lt;/p&gt;
&lt;p&gt;After you run your migrations, you run &lt;code&gt;sea-orm-cli generate entity&lt;/code&gt; to codegen a bunch of rust files that are dumped into your entities crate.&lt;/p&gt;
&lt;p&gt;That codegens a bunch of Rust that looks something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;use&lt;/span&gt; sea_orm::entity::prelude::&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#[sea_orm(table_name = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;session&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Model&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;#[sea_orm(primary_key, auto_increment = false)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; id: &lt;span style=&#34;color:#a6e22e&#34;&gt;Uuid&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; user_id: Option&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;String&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; user_email: Option&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;String&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; org_id: &lt;span style=&#34;color:#a6e22e&#34;&gt;Uuid&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; creation_timestamp: &lt;span style=&#34;color:#a6e22e&#34;&gt;DateTime&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; project_id: &lt;span style=&#34;color:#a6e22e&#34;&gt;Uuid&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;enum&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Relation&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;#[sea_orm(has_many = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;super::active_session::Entity&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ActiveSession,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;#[sea_orm(has_many = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;super::session_step::Entity&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    SessionStep,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;impl&lt;/span&gt; Related&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;super&lt;/span&gt;::active_session::Entity&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; Entity {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;to&lt;/span&gt;() -&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RelationDef&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Relation::ActiveSession.def()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;impl&lt;/span&gt; Related&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;super&lt;/span&gt;::session_step::Entity&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; Entity {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;to&lt;/span&gt;() -&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RelationDef&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Relation::SessionStep.def()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;impl&lt;/span&gt; ActiveModelBehavior &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; ActiveModel {}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This includes derives for a bunch of traits for getting/updating/inserting/deleting/joining these entities via &lt;code&gt;sea-orm&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Overall, the entity system seems&amp;hellip; OK. You can do all the usual ORM stuff like finding related entities, filtering with &lt;code&gt;.eq()&lt;/code&gt;, and inserting new stuff. The APIs mostly seem pretty clean and make sense.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s some of the familiar confusion due to the inclusion of &lt;code&gt;Active&lt;/code&gt; variants of the models and entities. This system is designed to let you leave some portion of the model&amp;rsquo;s fields unchanged/default when updating or inserting entities by wrapping them all in an &lt;a href=&#34;https://docs.rs/sea-orm/latest/sea_orm/entity/enum.ActiveValue.html&#34;&gt;&lt;code&gt;ActiveValue&lt;/code&gt;&lt;/a&gt; enum. The API makes sense conceptually and does indeed work, but I find that it makes for a verbose and clunky developer experience.&lt;/p&gt;
&lt;p&gt;My main grips with this are mostly rooted in the fact that I&amp;rsquo;m not really a big fan of heavy entity/model-based ORMs in general. I usually find it easier to write my queries by hand or using the more minimal query builder patterns from &lt;code&gt;diesel&lt;/code&gt; or similar.&lt;/p&gt;
&lt;p&gt;For most of the database stuff I end up building, it feels like the abstractions leak very quickly out of what the ORM is expecting. For some apps with simpler or more constrained database hierarchies, I feel like this kind of system would be a better fit. It&amp;rsquo;s also very possible that I&amp;rsquo;m just doing something wrong or overlooking something.&lt;/p&gt;
&lt;p&gt;In any case, there&amp;rsquo;s not a ton here I have to complain about that could be blamed on &lt;code&gt;sea-orm&lt;/code&gt; specifically.&lt;/p&gt;
&lt;h2 id=&#34;sea--crate-organization&#34;&gt;&lt;code&gt;sea-*&lt;/code&gt; Crate Organization&lt;/h2&gt;
&lt;p&gt;One other pain point I kept running into came from the fact that there are so many different &lt;code&gt;sea-*&lt;/code&gt; crates across which the functionality I need is split across.&lt;/p&gt;
&lt;p&gt;It was quite difficult to figure out where to import things from. There are even some cases where there are structs of the same name exist in different crates making it even harder to figure things out.&lt;/p&gt;
&lt;p&gt;I also ran into problems like this when trying to set up an enum type + field for one of my tables. My goal was to create a single Postgres enum type and use it for a field on one of my tables.&lt;/p&gt;
&lt;p&gt;In raw SQL, it would look something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TYPE&lt;/span&gt; customer_status &lt;span style=&#34;color:#66d9ef&#34;&gt;AS&lt;/span&gt; ENUM (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;inactive&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;pending&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;active&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; customers (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  id uuid &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  cur_status customer_status
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&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;sea-orm&lt;/code&gt; certainly has support for generating custom types including enum types, and they have two docs pages about it: &lt;a href=&#34;https://www.sea-ql.org/SeaORM/docs/generate-entity/enumeration/&#34;&gt;Generating Entities -&amp;gt; Enumerations&lt;/a&gt; and &lt;a href=&#34;https://www.sea-ql.org/SeaORM/docs/schema-statement/create-enum/&#34;&gt;Schema Statement -&amp;gt; Create Enum&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, there&amp;rsquo;s an issue. Both of those docs pages start from entities. They expect you to be manually defining these entities and then reference the entity in the code that performs the &lt;code&gt;CREATE TABLE&lt;/code&gt;. This poses a chicken and egg problem for actually creating and referencing custom enum types within migrations.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s extremely unclear to me how to actually set up an enum type and reference it in a table I&amp;rsquo;m creating from within a migration. I really feel like it&amp;rsquo;s possible, but there&amp;rsquo;s so much complexity involved.&lt;/p&gt;
&lt;p&gt;There are several different derivable traits on things. The migration examples show you creating a different enum for each table and deriving &lt;code&gt;DeriveIden&lt;/code&gt; on it so you can refer to the table and its fields in queries. Then, for the codegen&amp;rsquo;d entities, there&amp;rsquo;s &lt;code&gt;DeriveEntityModel&lt;/code&gt; which &amp;lsquo;is the ‘almighty’ macro which automatically generates Entity, Column, and PrimaryKey&amp;rsquo;. Then there&amp;rsquo;s an &lt;code&gt;ActiveEnum&lt;/code&gt; which is the Rust type which corresponds to a custom enum type living in the DB. However, to define a table, you reference fields on that enum which are &lt;code&gt;Iden&lt;/code&gt;s.&lt;/p&gt;
&lt;p&gt;I tried a bunch of things, but yeah I couldn&amp;rsquo;t figure it out. Diesel has a &lt;a href=&#34;https://github.com/adwhit/diesel-derive-enum&#34;&gt;third-party solution&lt;/a&gt; for this which I&amp;rsquo;m not particularly a fan of, but at least it works and is comparatively easy to set up.&lt;/p&gt;
&lt;h2 id=&#34;programmatic-query-generation&#34;&gt;Programmatic Query Generation&lt;/h2&gt;
&lt;p&gt;Although not part of &lt;code&gt;sea-orm&lt;/code&gt; itself, one thing I have to mention is how good &lt;code&gt;sea-query&lt;/code&gt; is at doing programmatic query manipulation.&lt;/p&gt;
&lt;p&gt;One project I built in the past year for my job at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt; is an interactive query builder web interface. It lets users design complicated with joins, filters, and complex aggregates. It does all of this in a sandboxed way, allowing us to make sure they can only query resources we give them access to.&lt;/p&gt;
&lt;p&gt;Building this would have been &lt;em&gt;extremely&lt;/em&gt; difficult if it weren&amp;rsquo;t for the powerful tools provided by &lt;code&gt;sea-query&lt;/code&gt; and &lt;code&gt;sea-schema&lt;/code&gt;. I strongly believe these libraries are the best tools available in the Rust ecosystem to perform advanced manipulation or generation of SQL queries. The amount of situations where something &amp;ldquo;just worked&amp;rdquo; after I found the right function was very high.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sea-schema&lt;/code&gt; is also a lovely gem of a crate. It&amp;rsquo;s what &lt;code&gt;sea-orm&lt;/code&gt; uses under the hood to do its codegen. &lt;code&gt;sea-schema&lt;/code&gt; can connect to a live database and automatically discover the full schema for a given database. This includes all the details you would every need like data types, foreign keys, constraints, and all that kind of stuff. There are a few bugs and edge cases we&amp;rsquo;ve run into which honestly isn&amp;rsquo;t surprising given how big that problem space is, but for the majority of cases it again &amp;ldquo;just works&amp;rdquo; and is very easy to work with code-wise.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Overall, my impression of &lt;code&gt;sea-orm&lt;/code&gt; is pretty lukewarm. The developer experience of setting up the crates, writing migrations, and making queries isn&amp;rsquo;t my favorite compared to other alternatives. It&amp;rsquo;s quite difficult for me to find the right way to do things due to a large number of magic traits required to do everything.&lt;/p&gt;
&lt;p&gt;Again, a large part of this sentiment is personal preference. I think that a developer coming from the Rails/ActiveRecord world might enjoy an entity-based ORM like &lt;code&gt;sea-orm&lt;/code&gt; much more than me. One of my co-workers advocated for us to try &lt;code&gt;sea-orm&lt;/code&gt; in the first place which is why I ended up using it, so you might enjoy it as well.&lt;/p&gt;
&lt;p&gt;I just want to say one more time that the underlying libraries like &lt;code&gt;sea-query&lt;/code&gt;/&lt;code&gt;sea-schema&lt;/code&gt; and the broader &lt;code&gt;sea-*&lt;/code&gt; ecosystem really are excellent, though. Even if you don&amp;rsquo;t go for the whole &lt;code&gt;sea-orm&lt;/code&gt; experience, they&amp;rsquo;re extremely useful on their own.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Handling Gamma Correction for Three.JS pmndrs `postprocessing` Effects</title>
      <link>https://cprimozic.net/notes/posts/threejs-pmndrs-postprocessing-gamma-correction/</link>
      <pubDate>Mon, 22 Jan 2024 11:31:08 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/threejs-pmndrs-postprocessing-gamma-correction/</guid>
      <description>&lt;p&gt;I recently received a &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays/issues/8&#34;&gt;bug report&lt;/a&gt; on a library I built - &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays&#34;&gt;&lt;code&gt;three-good-godrays&lt;/code&gt;&lt;/a&gt; - which implements screen-space raymarched godrays for Three.JS as a pass for the &lt;a href=&#34;https://github.com/pmndrs/postprocessing&#34;&gt;pmndrs &lt;code&gt;postprocessing&lt;/code&gt; library&lt;/a&gt;. One of the problems pointed out was that colors seemed washed out/desaturated when my pass was used, even when the pass wasn&amp;rsquo;t rendering any godrays.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how things look by default without the effect (and are supposed to look with it on):&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/btp.png&#34; alt=&#34;Screenshot of a scene rendered with Three.JS.  There’s a red plane with a red cube floating above it, casting a shadow on the plane.  The red color is quite bright and cherry/tomato colored.&#34;&gt;&lt;/p&gt;</description>
      <content>&lt;p&gt;I recently received a &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays/issues/8&#34;&gt;bug report&lt;/a&gt; on a library I built - &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays&#34;&gt;&lt;code&gt;three-good-godrays&lt;/code&gt;&lt;/a&gt; - which implements screen-space raymarched godrays for Three.JS as a pass for the &lt;a href=&#34;https://github.com/pmndrs/postprocessing&#34;&gt;pmndrs &lt;code&gt;postprocessing&lt;/code&gt; library&lt;/a&gt;. One of the problems pointed out was that colors seemed washed out/desaturated when my pass was used, even when the pass wasn&amp;rsquo;t rendering any godrays.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how things look by default without the effect (and are supposed to look with it on):&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/btp.png&#34; alt=&#34;Screenshot of a scene rendered with Three.JS.  There’s a red plane with a red cube floating above it, casting a shadow on the plane.  The red color is quite bright and cherry/tomato colored.&#34;&gt;&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s how they looked with the godrays pass enabled:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/btl.png&#34; alt=&#34;Screenshot of a scene rendered with Three.JS with the buggy godrays pass enabled.  The red plane and red cube look significantly darker and duller than they should.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The bug report is right - there&amp;rsquo;s definitely something wrong.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;The reason this is happening is due to differences in color space handling. According to the docs on the pmndrs &lt;code&gt;postprocessing&lt;/code&gt; library, the internal buffers used by postprocessing passes are expected to store data in sRGB format (&lt;a href=&#34;https://github.com/pmndrs/postprocessing?tab=readme-ov-file#output-color-space&#34;&gt;relevant docs&lt;/a&gt;). To handle this, popular effects such as &lt;code&gt;n8ao&lt;/code&gt; perform linear -&amp;gt; sRGB conversion by default: &lt;a href=&#34;https://github.com/N8python/n8ao?tab=readme-ov-file#usage&#34;&gt;https://github.com/N8python/n8ao?tab=readme-ov-file#usage&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To implement this, &lt;code&gt;n8ao&lt;/code&gt; calls &lt;code&gt;LinearTosRGB()&lt;/code&gt; on the pixel data output if &lt;code&gt;gammaCorrect&lt;/code&gt; is enabled (which is the default).&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;To fix &lt;code&gt;three-good-godrays&lt;/code&gt;, I simply added the exact same solution that &lt;code&gt;n8ao&lt;/code&gt; used. I added the same &lt;code&gt;gammaCorrection&lt;/code&gt; flag to the pass params, set it enabled by default, and added the same call to &lt;code&gt;LinearTosRGB()&lt;/code&gt; &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays/blob/main/src/compositor.frag#L61-L63&#34;&gt;to the shader&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, after adding this change, I noticed that my demos were experiencing some severe artifacts:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bto.png&#34; alt=&#34;Screenshot of artifacts caused by double encoding in a Three.Js pmndrs postprocessing pipeline.  There is a grainy pattern of colorful pixels appearing over an otherwise blank black background.&#34;&gt;&lt;/p&gt;
&lt;p&gt;After digging into this, I determined that the artifacts went away if I disabled the &lt;code&gt;SMAAEffect&lt;/code&gt; and its &lt;code&gt;EffectPass&lt;/code&gt; that I had added to the postprocessing pipeline after the &lt;code&gt;GodraysPass&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It turns out that output encoding is performed by default by &lt;code&gt;EffectPass&lt;/code&gt; in some circumstances. This was causing my colors to get double-converted to sRGB which produced those artifacts.&lt;/p&gt;
&lt;p&gt;To work around that, I &amp;hellip; just set &lt;code&gt;gammaCorrection: false&lt;/code&gt; in the godrays pass params. This had the result of making things look the same way they did before for my demos. Another method of achieving this is to set &lt;code&gt;outputEncoding = false&lt;/code&gt; on the &lt;code&gt;SMAAEffect&lt;/code&gt;&amp;rsquo;s &lt;code&gt;EffectPass&lt;/code&gt; which disables the double-encoding.&lt;/p&gt;
&lt;p&gt;One thing to note is that I have also set the &lt;code&gt;frameBufferType&lt;/code&gt; to &lt;code&gt;HalfFloatType&lt;/code&gt; on the framebuffers used by the &lt;code&gt;postprocessing&lt;/code&gt; &lt;code&gt;EffectComposer&lt;/code&gt;. The reason I do this is to avoid color banding that results from the loss of precision by the default 8-bit color format.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;EffectComposer&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;renderer&lt;/span&gt;, { &lt;span style=&#34;color:#a6e22e&#34;&gt;frameBufferType&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;THREE.HalfFloatType&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;other-notes&#34;&gt;Other Notes&lt;/h2&gt;
&lt;p&gt;The color space/encoding handling in the Three.JS ecosystem is very complex and confusing to me at the moment. Three.JS recently made some internal changes to &lt;a href=&#34;https://threejs.org/docs/#manual/en/introduction/Color-management&#34;&gt;Color Management&lt;/a&gt; which might be interfering/conflicting with pmndrs &lt;code&gt;postprocessing&lt;/code&gt; in some ways.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re still running into color issues in your Three.JS projects, some other things you can try are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Toggling &lt;code&gt;THREE.ColorManagement.enabled = true;&lt;/code&gt;. I set this to &lt;code&gt;true&lt;/code&gt; for my demos, and I believe this is the &amp;ldquo;right way&amp;rdquo; to do things in modern Three.JS.&lt;/li&gt;
&lt;li&gt;Changing the &lt;code&gt;outputColorSpace&lt;/code&gt; of your &lt;code&gt;WebGLRenderer&lt;/code&gt;. This is set to &lt;code&gt;THREE.SRGBColorSpace&lt;/code&gt; by default, and I think it&amp;rsquo;s suggested to not change that.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m honestly not really sure about the details of these things myself. For my projects, I generally end up trying various combinations of these settings until things look right :p&lt;/p&gt;
&lt;p&gt;In any case, I hope this helps give you some info on possible solutions if you&amp;rsquo;re running into these problems yourself.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Changing Linux Select to Paste Menu `fcitx` Keyboard Shortcut</title>
      <link>https://cprimozic.net/notes/posts/changing-linux-select-to-paste-menu-fcitx-keyboard-shortcut/</link>
      <pubDate>Tue, 16 Jan 2024 21:09:46 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/changing-linux-select-to-paste-menu-fcitx-keyboard-shortcut/</guid>
      <description>&lt;p&gt;For a long time - at least a couple of years - I&amp;rsquo;ve been cursed with an issue on my KDE Plasma Linux desktop where my PgUp key doesn&amp;rsquo;t work.  Instead of scrolling up in my terminal or editor, it pops open a menu with the title &amp;ldquo;Select to paste&amp;rdquo; and a listing of my most recent clipboard entries:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bt4.png&#34; alt=&#34;A screenshot of the fcitx menu with the title “select to paste” and a listing of my six most recent clipboard entries&#34;&gt;&lt;/p&gt;</description>
      <content>&lt;p&gt;For a long time - at least a couple of years - I&amp;rsquo;ve been cursed with an issue on my KDE Plasma Linux desktop where my PgUp key doesn&amp;rsquo;t work.  Instead of scrolling up in my terminal or editor, it pops open a menu with the title &amp;ldquo;Select to paste&amp;rdquo; and a listing of my most recent clipboard entries:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bt4.png&#34; alt=&#34;A screenshot of the fcitx menu with the title “select to paste” and a listing of my six most recent clipboard entries&#34;&gt;&lt;/p&gt;
&lt;p&gt;I tried briefly a couple of times to fix this and get my page up to work again but to no avail.  Today, I finally figured it out.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;I figured out that the menu getting opened is from a project called &lt;code&gt;fcitx&lt;/code&gt; which is some utility for input handling on Linux.  I found it by doing a code search on Github for the &lt;a href=&#34;https://github.com/fcitx/fcitx/blob/master/src/module/clipboard/clipboard.c#L372&#34;&gt;title of the menu&lt;/a&gt; - the only identifying information I could see.&lt;/p&gt;
&lt;p&gt;The problem remained that I couldn&amp;rsquo;t find the shortcut anywhere in my system settings, though, so I had no idea how to disable it.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;After much digging, I finally found a part of the system settings which controlled Fcitx.  Then, hidden two levels deep in sub-menus, I found the &amp;ldquo;Trigger Key for Clipboard History List&amp;rdquo; setting which was set to PgUp:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bt5.png&#34; alt=&#34;Screenshot of Linux KDE Plasma system settings for Input Method with the submenu for Addon config -&amp;gt; Clipboard opened showing the setting “Trigger Key for Clipboard History List” which was causing my problems&#34;&gt;&lt;/p&gt;
&lt;p&gt;When I hit the &amp;ldquo;Defaults&amp;rdquo; button, it restored the shortcut to Ctrl + ; and suddenly I remembered how it got broken in the first place.&lt;/p&gt;
&lt;p&gt;I wanted to use the control + semicolon shortcut for VS Code, so I switched the existing shortcut in my settings to something random (PgUp, apparently) and promptly forgot about it.  Then, KDE Plasma must have moved that setting in some system update (or maybe I just missed it all these times).&lt;/p&gt;
&lt;p&gt;Anyway, I changed the trigger key shortcut to ctrl + alt + shift + { and the problem is solved for good this time :)&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>PIXI.JS Optimizations</title>
      <link>https://cprimozic.net/notes/posts/pixi-js-optimizations/</link>
      <pubDate>Tue, 02 Jan 2024 10:19:53 -0600</pubDate>
      <guid>https://cprimozic.net/notes/posts/pixi-js-optimizations/</guid>
      <description>&lt;p&gt;I was recently working on speeding up a MIDI editor UI written in PIXI.JS which is part of my &lt;a href=&#34;https://synth.ameo.dev&#34;&gt;web synth&lt;/a&gt; project.  The UI is fairly simple itself, but it needs to be efficient in order to render potentially thousands+ notes on the screen at once.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what the MIDI editor UI looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bsr.png&#34; alt=&#34;Screenshot of a MIDI editor web UI.  There are a few dozen rows of notes with a labeled piano keyboard on the left.  There are green notes arrayed along the composition.  There is a toolbar with a variety of buttons with icons for controlling the MIDI editor on the top.&#34;&gt;&lt;/p&gt;</description>
      <content>&lt;p&gt;I was recently working on speeding up a MIDI editor UI written in PIXI.JS which is part of my &lt;a href=&#34;https://synth.ameo.dev&#34;&gt;web synth&lt;/a&gt; project.  The UI is fairly simple itself, but it needs to be efficient in order to render potentially thousands+ notes on the screen at once.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what the MIDI editor UI looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bsr.png&#34; alt=&#34;Screenshot of a MIDI editor web UI.  There are a few dozen rows of notes with a labeled piano keyboard on the left.  There are green notes arrayed along the composition.  There is a toolbar with a variety of buttons with icons for controlling the MIDI editor on the top.&#34;&gt;&lt;/p&gt;
&lt;p&gt;As I mentioned, I built this whole UI with &lt;a href=&#34;https://github.com/pixijs/pixijs&#34;&gt;Pixi.JS&lt;/a&gt; (except for the toolbar which is vanilla HTML/CSS/React).  For the most part, it was working well.  It supports zooming, scrolling/panning, and adding/removing/moving/resizing notes.  All of the interactivity was handled by PIXI.JS through its events/interaction system.&lt;/p&gt;
&lt;p&gt;I was looking to add a new feature to the MIDI editor for adding custom labels and annotations to some notes to support use cases where the MIDI editor controls a sampler or something similar where only some notes were active.  However, when I went to start implementing it, I noticed that the MIDI editor&amp;rsquo;s performance was quite bad.  It was using &amp;gt;80% of my GPU on my M1 Macbook Max to just render the base MIDI editor UI and a single note.&lt;/p&gt;
&lt;p&gt;For an almost completely static UI, this was absurd.  I felt strongly that there would be some low-hanging fruit to speed it up, so I went looking.&lt;/p&gt;
&lt;p&gt;I tried several things to get this UI to render faster.  Here&amp;rsquo;s a list of some of the things I tried to speed it up.&lt;/p&gt;
&lt;h2 id=&#34;avoiding-mask-usage&#34;&gt;Avoiding Mask Usage&lt;/h2&gt;
&lt;p&gt;For the piano keyboard on the left, I rendered the whole thing inside of a &lt;code&gt;PIXI.Container&lt;/code&gt; and then used a simple rectangular mask to cut off the top part so that it wouldn&amp;rsquo;t render underneath the cursor gutter bar.&lt;/p&gt;
&lt;p&gt;I used code like this to set that up:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;container&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;mask&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PIXI&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Graphics&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .&lt;span style=&#34;color:#a6e22e&#34;&gt;beginFill&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0xffffff&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .&lt;span style=&#34;color:#a6e22e&#34;&gt;drawRect&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PIANO_KEYBOARD_WIDTH&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;width&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PIANO_KEYBOARD_WIDTH&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;height&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .&lt;span style=&#34;color:#a6e22e&#34;&gt;endFill&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;According to the Pixi.JS docs, this kind of mask is the cheapest and should be applied using WebGL&amp;rsquo;s scissor testing.  However, I found that in practice it slowed down my rendering &lt;em&gt;significantly&lt;/em&gt;.  It seemed to be going into some kind of rendering slow path that was slowing down the whole application.&lt;/p&gt;
&lt;p&gt;I opted to just get rid of the masking entirely and instead extend the cursor gutter to the left and force it to render on top of the piano keyboard using &lt;code&gt;zIndex&lt;/code&gt; and &lt;code&gt;app.stage.sortableChildren = true&lt;/code&gt;.  This achieved the same effect but made it easier to PIXI to render.&lt;/p&gt;
&lt;h2 id=&#34;using-spectorjs-for-profiling--debugging&#34;&gt;Using Spector.JS for Profiling + Debugging&lt;/h2&gt;
&lt;p&gt;To try to figure out where the application was spending its time rendering, I used a browser extension called &lt;a href=&#34;https://spector.babylonjs.com/&#34;&gt;Spector.JS&lt;/a&gt;.  It&amp;rsquo;s a WebGL profiler/debugger that I&amp;rsquo;ve used in the past to debug and profile Three.JS applications.&lt;/p&gt;
&lt;p&gt;Since PIXI.JS uses WebGL under the hood to render, Spector.JS can be used to analyze all the render passes.  I loaded it up, selected the main canvas, and collected a profile.  Here&amp;rsquo;s what I saw:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bss.png&#34; alt=&#34;Screenshot of the Spector.JS UI showing the render passes and WebGL commands used to render the MIDI editor UI.  There are screenshots showing the result of each pass as it builds up the UI; each one adds a new line and the notes contained within that pass.  In the middle it shows commands like drawElements, bufferSubData, etc.  On the right it shows details for the currently selected pass such as a stack trace, WebGL flags, and arguments.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Overall, it was taking 238 commands to render the whole UI.  This isn&amp;rsquo;t an enormous number (some complex 3D scenes can use thousands or more commands to render) but it&amp;rsquo;s a lot more than should be necessary for such a simple UI.&lt;/p&gt;
&lt;p&gt;One thing that was clear was that each of the lines was being rendered one by one.  This surprised me; I was using a &lt;code&gt;PIXI.Graphics&lt;/code&gt; with &lt;code&gt;cacheAsBitmap = true&lt;/code&gt; to render those lines and ticks, and I was using the same instance for each line. I expected that this would activate PIXI&amp;rsquo;s optimizations and cause those to all get rendered together as sprites, but that evidently wasn&amp;rsquo;t the case.&lt;/p&gt;
&lt;p&gt;If there were multiple notes in one line, they were all getting rendered together in one pass, though, even when the little handles on either side for resizing them were included.&lt;/p&gt;
&lt;h2 id=&#34;upgrading-pixijs&#34;&gt;Upgrading PIXI.JS&lt;/h2&gt;
&lt;p&gt;I also took the opportunity to bump PIXI.JS to the latest version (7.3 at the time of writing this).  Sometimes you get lucky and get performance for free when doing that.&lt;/p&gt;
&lt;p&gt;Unfortunately, this was not one of those cases.  Performance seemed pretty much the same after upgrading.  Still a good thing to do, though, and I&amp;rsquo;ve been meaning to get to it for a while now.&lt;/p&gt;
&lt;h2 id=&#34;pre-rendering-to-sprites&#34;&gt;Pre-Rendering to Sprites&lt;/h2&gt;
&lt;p&gt;My main goal after seeing that info from Spector.JS was to try to get the note lines rendering more efficiently.  I knew that PIXI.JS had put extensive optimization into sprites, so I figured using real sprites rather than the cached graphics was a good idea to try.&lt;/p&gt;
&lt;p&gt;I switched my note lines + tick markings to render into a &lt;code&gt;PIXI.RenderTexture&lt;/code&gt; explicitly ahead of time, and then created sprites out of that shared texture:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PIXI&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Graphics&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ... rendering line and ticks based on current view + pan ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;renderTexture&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PIXI&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;RenderTexture&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;width&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;this.app.width&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;height&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;conf.LINE_HEIGHT&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;app&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;app&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;renderer&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;render&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;, { &lt;span style=&#34;color:#a6e22e&#34;&gt;renderTexture&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;MarkersCache&lt;/span&gt;.&lt;span style=&#34;color:#66d9ef&#34;&gt;set&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;markersCacheKey&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;renderTexture&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PIXI&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sprite&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;renderTexture&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Whenever the zoom level of horizontal scroll of the UI changed, I&amp;rsquo;d re-construct the texture for the markers and re-create the sprites.  After a bit of tweaking, I got it working as it was before, and performance was greatly improved!&lt;/p&gt;
&lt;h2 id=&#34;pre-rendering-containers-vs-children&#34;&gt;Pre-Rendering Containers vs. Children&lt;/h2&gt;
&lt;p&gt;The main bottleneck remaining that I could see was the piano keyboard.  Each of the keys consisted of a colored &lt;code&gt;PIXI.Graphics&lt;/code&gt; to render the key background + border as well as a &lt;code&gt;PIXI.Text&lt;/code&gt; for the label.&lt;/p&gt;
&lt;p&gt;For each of the keys, I rendered the background and the label separately and set &lt;code&gt;.cacheAsBitmap = true&lt;/code&gt; for each of them.  Then, I set them into containers, added those containers to the base piano keys container, and set them up to render in the correct order.&lt;/p&gt;
&lt;p&gt;As it turns out, this was a sub-optimal pattern for Pixi to work with.  Pixi had to create separate textures for all of the dozens of text labels and render each of the keys separately.&lt;/p&gt;
&lt;p&gt;The piano keyboard is mostly static; the only thing that changes is the Y offset with the current vertical scroll, and some pink transparent indicators are rendered on top of it when notes are actively playing.&lt;/p&gt;
&lt;p&gt;Taking advantage of this, I pre-rendered the entire piano keyboard into a bitmap by setting the parent container &lt;code&gt;.cacheAsBitmap = true&lt;/code&gt;.  This means that PIXI was able to save the whole thing as a single big texture and then render it all at once.&lt;/p&gt;
&lt;p&gt;This unsurprisingly sped things up significantly. I had to add a little bit of extra handling to the active note indicators, including the label in them again since they were rendering between the labels layer and keys layer previously, but it wasn&amp;rsquo;t that big of a task.&lt;/p&gt;
&lt;h2 id=&#34;results&#34;&gt;Results&lt;/h2&gt;
&lt;p&gt;After all that optimization, I ran some more profiling to see how effect it had:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bst.png&#34; alt=&#34;Screenshot of chrome browser dev tools showing the results of a profiling run done on the MIDI editor.  It shows that the GPU utilization is very low at around 5% or less and the CPU is mostly idle.&#34;&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what I&amp;rsquo;m talking about!   The GPU is basically idle - as it should be for this UI.&lt;/p&gt;
&lt;p&gt;I also ran another Spector.JS profiling run to see how the render pass count had changed:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bsu.png&#34; alt=&#34;Screenshot of the Spector.JS results from profiling the MIDI editor.  It shows that the entire UI is now rendered in a single render pass!&#34;&gt;&lt;/p&gt;
&lt;p&gt;The whole thing renders in a single render pass now!!&lt;/p&gt;
&lt;p&gt;I was quite shocked to see that honestly. PIXI.JS was finally able to show off its impressive capabilities here.  I&amp;rsquo;d be interested check out their code to see how it manages to do that at some point.&lt;/p&gt;
&lt;p&gt;So yeah - a very successful optimization journey indeed.  PIXI.JS needs some things to get set up in the right way in order for it to really shine, but it excels when they are.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Svelte Seo `Cannot find module &#39;./transformers/&#34;application/ld&#43;json&#34;&gt;${&#39;`</title>
      <link>https://cprimozic.net/notes/posts/fixing-svelte-seo-cannot-find-module-transformers-application-ld-json/</link>
      <pubDate>Tue, 28 Nov 2023 23:25:56 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-svelte-seo-cannot-find-module-transformers-application-ld-json/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;After upgrading &lt;code&gt;svelte-seo&lt;/code&gt; to the latest version (1.5.4 at the time of writing this), I encountered this error when trying to run my SvelteKit dev server:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Error while preprocessing /home/casey/dream/node_modules/svelte-seo/index.svelte - Cannot find module &amp;#39;./transformers/&amp;#34;application/ld+json&amp;#34;&amp;gt;${&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Require stack:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- /home/casey/dream/node_modules/svelte-preprocess/dist/autoProcess.js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- /home/casey/dream/node_modules/svelte-preprocess/dist/index.js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Error while preprocessing /home/casey/dream/node_modules/svelte-seo/index.svelte - Cannot find module &amp;#39;./transformers/&amp;#34;application/ld+json&amp;#34;&amp;gt;${&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Require stack:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- /home/casey/dream/node_modules/svelte-preprocess/dist/autoProcess.js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- /home/casey/dream/node_modules/svelte-preprocess/dist/index.js
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The issue turned out to be that my &lt;code&gt;svelte&lt;/code&gt; version was too old.&lt;/strong&gt;&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;After upgrading &lt;code&gt;svelte-seo&lt;/code&gt; to the latest version (1.5.4 at the time of writing this), I encountered this error when trying to run my SvelteKit dev server:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Error while preprocessing /home/casey/dream/node_modules/svelte-seo/index.svelte - Cannot find module &amp;#39;./transformers/&amp;#34;application/ld+json&amp;#34;&amp;gt;${&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Require stack:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- /home/casey/dream/node_modules/svelte-preprocess/dist/autoProcess.js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- /home/casey/dream/node_modules/svelte-preprocess/dist/index.js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Error while preprocessing /home/casey/dream/node_modules/svelte-seo/index.svelte - Cannot find module &amp;#39;./transformers/&amp;#34;application/ld+json&amp;#34;&amp;gt;${&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Require stack:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- /home/casey/dream/node_modules/svelte-preprocess/dist/autoProcess.js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- /home/casey/dream/node_modules/svelte-preprocess/dist/index.js
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The issue turned out to be that my &lt;code&gt;svelte&lt;/code&gt; version was too old.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I upgraded &lt;code&gt;svelte&lt;/code&gt; to the latest version (&lt;code&gt;svelte  ^3.50.1  →  ^4.2.7&lt;/code&gt;), re-started the dev server, and the issue went away.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Building a Realistic Raindrop-Covered Window Pane Material in Three.JS</title>
      <link>https://cprimozic.net/notes/posts/building-realistic-rainy-window-pane-in-threejs/</link>
      <pubDate>Sun, 12 Nov 2023 15:50:16 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/building-realistic-rainy-window-pane-in-threejs/</guid>
      <description>&lt;p&gt;Recently, I&amp;rsquo;ve been working on a &lt;a href=&#34;https://3d.ameo.design/rainy.html&#34;&gt;rainy scene&lt;/a&gt; in Three.JS.  One of the most important parts of this scene is a greenhouse with big glass windows.&lt;/p&gt;
&lt;p&gt;I decided to take a stab at making some realistic raindrop-covered window panes for them using Three.JS&amp;rsquo;s built-in transmission shader.  The result turned out pretty well if I do say so myself, so I thought I&amp;rsquo;d write up my process for building it from start to finish.&lt;/p&gt;</description>
      <content>&lt;p&gt;Recently, I&amp;rsquo;ve been working on a &lt;a href=&#34;https://3d.ameo.design/rainy.html&#34;&gt;rainy scene&lt;/a&gt; in Three.JS.  One of the most important parts of this scene is a greenhouse with big glass windows.&lt;/p&gt;
&lt;p&gt;I decided to take a stab at making some realistic raindrop-covered window panes for them using Three.JS&amp;rsquo;s built-in transmission shader.  The result turned out pretty well if I do say so myself, so I thought I&amp;rsquo;d write up my process for building it from start to finish.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how the finished material looks when included in my scene:&lt;/p&gt;
&lt;div style=&#34;display: flex; justify-content: center;&#34;&gt;
  &lt;video width=&#34;800&#34; height=&#34;600&#34; controls poster=&#34;https://i.ameo.link/boi.png&#34;&gt;
    &lt;source src=&#34;https://i.ameo.link/boj.mp4&#34; type=&#34;video/mp4&#34;&gt;
  &lt;/video&gt;
&lt;/div&gt;
&lt;p&gt;As you can see, the window looks like it has lots of raindrops on its outer surface.  Notice how buildings in the background get distorted and twisted in areas where the rain drops are heaviest.&lt;/p&gt;
&lt;p&gt;In addition, the whole background is quite blurred and out of focus - as if the window was quite thick or covered with condensation.&lt;/p&gt;
&lt;p&gt;Finally, there are lots of small black flecks on the window surface, making it appear dirty or old.  This helps make the window feel more tangible rather than having it be completely clear and clean.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll break down how I built each of these components and how they all work together to produce the final Three.JS material.&lt;/p&gt;
&lt;h2 id=&#34;transmission-via-meshphysicalmaterial&#34;&gt;Transmission via &lt;code&gt;MeshPhysicalMaterial&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Transmission&lt;/em&gt; is the key to making this material look good.  If you&amp;rsquo;re not familiar with it, transmission is a property available on &lt;a href=&#34;https://threejs.org/docs/#api/en/materials/MeshPhysicalMaterial&#34;&gt;&lt;code&gt;MeshPhysicalMaterial&lt;/code&gt;&lt;/a&gt; that functions kind of like a more advanced and powerful version of transparency/opacity.  It simulates different properties of light and exposes several controls for altering the way light passes through objects.&lt;/p&gt;
&lt;p&gt;For my needs with this window, there are a few properties which are key for getting the effect I wanted.&lt;/p&gt;
&lt;h3 id=&#34;transmission&#34;&gt;Transmission&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&#34;https://threejs.org/docs/#api/en/materials/MeshPhysicalMaterial.transmission&#34;&gt;&lt;code&gt;transmission&lt;/code&gt;&lt;/a&gt; parameter controls the amount of transmission.  It does pretty much the same thing as &lt;code&gt;opacity&lt;/code&gt;, but for transmissive materials rather than transparent ones.&lt;/p&gt;
&lt;p&gt;For my material, I just set &lt;code&gt;transmission&lt;/code&gt; to 1.  This means that the whole material is 100% transmissive/transparent.  That value makes sense for a window.&lt;/p&gt;
&lt;p&gt;Setting it to a value lower than 1 will cause the material&amp;rsquo;s base color to be mixed in with the colors transmitted through from the background.  Here&amp;rsquo;s how a value of 0.7 looks for my window material:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bok.png&#34; alt=&#34;Screenshot of transmissive window rendered with Three.JS.  It has had its transmission parameter set to 0.7, so the window appears milky white since its base color is white.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The window&amp;rsquo;s base color is white, so it looks milky-white as its base color is mixed in with the transmitted background.&lt;/p&gt;
&lt;h3 id=&#34;ior&#34;&gt;IOR&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://threejs.org/docs/#api/en/materials/MeshPhysicalMaterial.ior&#34;&gt;&lt;code&gt;ior&lt;/code&gt;&lt;/a&gt; stands for index of refraction.  This is a number which represents how much the material bends light.  The default value is 1.5, and setting it higher will add more bending.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ior&lt;/code&gt; was a bit harder to figure out.  For my window, I ended up with a value of 1.6 - slightly above the default of 1.5.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ior&lt;/code&gt; is very closely linked with &lt;code&gt;roughness&lt;/code&gt;; changing one will cause pretty drastic differences in the other&amp;rsquo;s functionality.  Because of this, I found it best to adjust them together.&lt;/p&gt;
&lt;p&gt;Overall, I found that higher IOR values tend to make the blur more pronounced and as the value approaches its minimum of 1, the raindrops almost completely disappear.&lt;/p&gt;
&lt;h3 id=&#34;roughness--roughness-map&#34;&gt;Roughness / Roughness Map&lt;/h3&gt;
&lt;p&gt;Now we get into the meat of the implementation.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;roughness&lt;/code&gt; is usually used to control how reflective a material is, but it has a special purpose on materials with transmission.  For transmissive materials, roughness controls how much blur the material applies to the background (higher = more blur).&lt;/p&gt;
&lt;p&gt;I chose a base roughness of 0.64 for my window.  This provides a good bit of blur without totally obscuring the background.&lt;/p&gt;
&lt;p&gt;However, the real important part is the &lt;code&gt;roughnessMap&lt;/code&gt;.  This allows the blur factor of the material to be varied across its surface.  Here&amp;rsquo;s the texture I&amp;rsquo;m using for the roughness map of the window material:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/be0.jpg&#34; alt=&#34;Looks like a close-up photograph of a textured metallic gold surface.  There are heavy specular highlights on the small smooth raised ridges on its surface.&#34;&gt;&lt;/p&gt;
&lt;p&gt;A bit of a surprising choice for use on a window!&lt;/p&gt;
&lt;p&gt;This is actually a texture I generated myself with stable diffusion for use in a metallic gold material I used in a different scene.  I converted it into a full-fledged PBR material using a process I detailed in &lt;a href=&#34;http://cprimozic.net/notes/posts/generating-textures-for-3d-using-stable-diffusion/&#34;&gt;a different post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One important property of this texture is that it&amp;rsquo;s seamless - meaning it repeats in both the X and Y axis without any gaps or discontinuities.  This is important for my material since the texture is pretty small (1024 x 1024) and so it needs to repeat multiple times across the large surface of all the windows.&lt;/p&gt;
&lt;p&gt;Anyway, although it might seem surprising at first, this material actually works very well for creating raindrops.&lt;/p&gt;
&lt;p&gt;When Three.JS interprets textures as roughness maps, it only looks at the green channel.  So, that texture will actually be the same as this grayscale image:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bom.jpg&#34; alt=&#34;Grayscale image showing the green channel extracted from the gold texture above.  There are prominent white strips across the surface which come from the specular highlights on the original texture.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Notice the white streaks across the texture.  Those come from the specular highlights (shiny parts where lots of light is reflecting) from the original texture.  At those parts of the texture, the roughness will be higher, causing there to be more blur when transmission is applied.  These spots correspond to the raindrops themselves.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the Three.JS material containing all the properties we set to far:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bon.png&#34; alt=&#34;Screenshot of the window shader rendered using the parameters we’ve gone through so far, including the newly introduced roughness map.  The raindrops are visible as blurry streaks across the window and do kind of look like raindrops at this point, but they’re missing depth and seem more like smudges.&#34;&gt;&lt;/p&gt;
&lt;p&gt;To be honest, I&amp;rsquo;d say that it they already sort of look like raindrops!  They look more like smudges than anything, though, and they lack a feeling of depth, but the effect is starting to take shape already.&lt;/p&gt;
&lt;h4 id=&#34;note-about-threejs-transmission-shader-blur&#34;&gt;Note About Three.JS Transmission Shader Blur&lt;/h4&gt;
&lt;p&gt;One important thing to note is that Three.JS made some &lt;a href=&#34;https://github.com/mrdoob/three.js/pull/25483&#34;&gt;major improvements&lt;/a&gt; to its transmission shader back in version 150 thanks to work by &lt;a href=&#34;https://github.com/N8python&#34;&gt;n8programs&lt;/a&gt;.  If you try to replicate this material or honestly if you make use of transmission with blur at all, make sure you&amp;rsquo;re using a version of Three.JS with those upgrades.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a before-and-after screenshot from the PR implementing the changes (left = before, right = after):&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bol.png&#34; alt=&#34;Screenshot showing a side-by-side before-and-after of the transmission shader improvements in Three.JS.  There is a spherical transmissive object in the middle of the image which is mostly transparent but blurry.  The left side has a very boxy and pixelated blur that looks artificial and low-quality.  The right side is much more rounded and natural looking.&#34;&gt;&lt;/p&gt;
&lt;p&gt;As you can see, it&amp;rsquo;s a massive improvement.  My material looked pretty bad without it!&lt;/p&gt;
&lt;h3 id=&#34;normal-map&#34;&gt;Normal Map&lt;/h3&gt;
&lt;p&gt;Now it&amp;rsquo;s time to give those raindrops the depth they&amp;rsquo;re lacking.&lt;/p&gt;
&lt;p&gt;Normal maps define the angle of the surface at every point on the material.  It can be used to add additional details to materials without having to add additional geometry.&lt;/p&gt;
&lt;p&gt;I used a web service called &lt;a href=&#34;https://withpoly.com/textures/edit&#34;&gt;Poly&lt;/a&gt; to generate PBR maps - including a normal map - from my gold texture using AI.  The result is a normal map which looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/be2.jpg&#34; alt=&#34;Tangent space normal map generated from the gold texture shown above.  The background is lavender/purple and the streaks/raindrops are more colorful with well-defined edges.&#34;&gt;&lt;/p&gt;
&lt;p&gt;This normal map has a very prominent impact on the way the transmission shader runs.&lt;/p&gt;
&lt;p&gt;A key part of the math behind transmission relies on the angle at which light enters and exits the material.  The normal map causes this to vary greatly around the raindrop streaks themselves and causes the path of light to vary greatly on them - just like in real life.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how the windows look after adding in the normal map:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/boo.png&#34; alt=&#34;Screenshot of transmissive window rendered with Three.JS.  Its outer surface appears to be covered by raindrops and streaks of water.  The background is dramatically refracted along the edges of the raindrops, causing ringing and lensing effects similar to those in real life.&#34;&gt;&lt;/p&gt;
&lt;p&gt;And there you go - that&amp;rsquo;s the core of the effect!&lt;/p&gt;
&lt;p&gt;Light is strongly refracted around the raindrops - especially around their edges - which causes light from different parts of the background scene to get pulled through and creating the cool-looking distortions.  It&amp;rsquo;s a real testament to the power of physically-based rendering that this works as well as it does.&lt;/p&gt;
&lt;h3 id=&#34;thickness--thickness-map&#34;&gt;Thickness / Thickness Map&lt;/h3&gt;
&lt;p&gt;Now we just need to put on some finishing touches.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://threejs.org/docs/#api/en/materials/MeshPhysicalMaterial.thickness&#34;&gt;&lt;code&gt;thickness&lt;/code&gt;&lt;/a&gt; parameter controls how thick the material is assumed to be when doing transmission computations.  The thicker it is, the more refraction and other effects will take place.&lt;/p&gt;
&lt;p&gt;I chose a base thickness value of 0.8 for my material.  I&amp;rsquo;m pretty sure that&amp;rsquo;s way higher than anything physically accurate, but I found that it seems to exaggerate the raindrops and make the end result look better to me.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;thicknessMap&lt;/code&gt; is multiplied to the base thickness across the material&amp;rsquo;s surface just like &lt;code&gt;roughnessMap&lt;/code&gt; and others.  I used the same texture as for the roughness map for the thickness map.&lt;/p&gt;
&lt;p&gt;It has a subtle but noticeable effect and helps make the raindrops even more prominent, treating the material as thicker at places where the raindrops are on its surface.&lt;/p&gt;
&lt;h3 id=&#34;texture-map&#34;&gt;Texture Map&lt;/h3&gt;
&lt;p&gt;The final piece for this material is the small black flecks on the window&amp;rsquo;s surface.&lt;/p&gt;
&lt;p&gt;To implement it, I used an almost-pure-white texture with some black flecks on its surface and set it as the material&amp;rsquo;s &lt;code&gt;map&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the actual image I use for it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bn8.jpg&#34; alt=&#34;Nearly pure white image with small organic-looking tiny black flecks throughout.&#34;&gt;&lt;/p&gt;
&lt;p&gt;To make the texture, I also used Stable Diffusion.  I generated a pretty generic texture like this to start out:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/boq.jpg&#34; alt=&#34;Screenshot of an organic-looking texture generated with Stable Diffusion XL.  It looks kind of like a dirty floor tile with an off-white background and lots of brown spots and marks covering its surface.&#34;&gt;&lt;/p&gt;
&lt;p&gt;This texture is also 1024x1024 and seamless like the gold texture, although that doesn&amp;rsquo;t matter as much in this case.&lt;/p&gt;
&lt;p&gt;I then processed it in GIMP, adjusting the color balance, exposure, and other things until it was almost perfectly white and only a few tiny flecks were visible.&lt;/p&gt;
&lt;p&gt;I found that it&amp;rsquo;s important to keep the map subtle for this material; making it too busy or opaque quickly makes the window look messy and greatly takes away from the cool transmission effects on the raindrops.&lt;/p&gt;
&lt;h2 id=&#34;final-result&#34;&gt;Final Result&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s the whole code I ended up with for the final rain-covered windows material:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;goldTextureNormal&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;loadTexture&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;https://i.ameo.link/be2.jpg&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;goldTextureAlbedo&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;loadTexture&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;https://i.ameo.link/be0.jpg&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;windowSurface&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;loadTexture&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;https://i.ameo.link/bn8.jpg&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;goldTextureNormal&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repeat&lt;/span&gt;.&lt;span style=&#34;color:#66d9ef&#34;&gt;set&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;34&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;34&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;goldTextureAlbedo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repeat&lt;/span&gt;.&lt;span style=&#34;color:#66d9ef&#34;&gt;set&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;34&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;34&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;windowSeamless&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;repeat&lt;/span&gt;.&lt;span style=&#34;color:#66d9ef&#34;&gt;set&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;40&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;40&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;greenhouseWindowsMaterial&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;MeshPhysicalMaterial&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;map&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;windowSurface&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;transmission&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;roughness&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;0.64&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;roughnessMap&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;goldTextureAlbedo&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;normalMap&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;goldTextureNormal&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;ior&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;1.6&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;thickness&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;0.8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;thicknessMap&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;goldTextureAlbedo&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s really not a lot to it code-wise; Three.JS does all the heavy lifting for us with its fancy transmission shader.&lt;/p&gt;
&lt;p&gt;Once you have good textures to use for your maps, it&amp;rsquo;s mostly just a matter of playing around with the IOR and roughness until you&amp;rsquo;re happy with the way it looks.&lt;/p&gt;
&lt;p&gt;Feel free to use these textures yourself directly; they&amp;rsquo;re public domain.  You can copy this whole material as well if you want.  If you do, I&amp;rsquo;d love to hear if you build something cool with it!&lt;/p&gt;
&lt;p&gt;My twitter is @ameobea10 and my mastodon is @ameo@mastodon.ameo.dev; feel free to let me know there.  I also post updates when I work on stuff like this, so you can give me a follow if you&amp;rsquo;d like to see those.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Anker USB Hub Not Connecting to M1 Mac</title>
      <link>https://cprimozic.net/notes/posts/anker-usb-hub-not-connecting-m1-mac/</link>
      <pubDate>Mon, 06 Nov 2023 12:02:42 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/anker-usb-hub-not-connecting-m1-mac/</guid>
      <description>&lt;p&gt;I have an Anker USB hub that I use with my work laptop - an M1 Mac Pro. I use it to plug in two USB-A peripherals (mouse and keyboard) as well as to plug in a HDMI monitor. The hub itself connects to my laptop via USB-C. In addition to the hub, I also have a second HDMI monitor, a USB-C internet adapter, wired headphones, and my charging cable connected to the laptop.&lt;/p&gt;</description>
      <content>&lt;p&gt;I have an Anker USB hub that I use with my work laptop - an M1 Mac Pro. I use it to plug in two USB-A peripherals (mouse and keyboard) as well as to plug in a HDMI monitor. The hub itself connects to my laptop via USB-C. In addition to the hub, I also have a second HDMI monitor, a USB-C internet adapter, wired headphones, and my charging cable connected to the laptop.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bn2.jpg&#34; alt=&#34;A photograph of an Anker USB hub with two USB-A cables plugged into it in the front as well as an HDMI cable plugged into it in the back.  It has a circular white light on the front right which is illuminated.&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been having an issue where the USB hub refuses to connect to the laptop after plugging it in after first setting it up for the day. When I plug the hub into the laptop, nothing will happen for a few seconds. Then, the white circular light on the hub will start blinking indefinitely and the hub will still not connect and none of the peripherals plugged into it nor the monitor are usable.&lt;/p&gt;
&lt;p&gt;This happens in spurts of a few days/weeks, seemingly randomly. In the past, I&amp;rsquo;ve fixed the problem to repeatedly un-plugging and re-plugging the hub - sometimes several times over the course of 15+ minutes - until it finally decides to connect.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve since figured out a more consistent fix for this issue.&lt;/p&gt;
&lt;p&gt;I plug both the hub and my second HDMI monitor into the right side of my laptop, and the plugs are right next to each other:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bn3.jpg&#34; alt=&#34;A close-up photograph of a black USB-C cable and a black HDMI cable plugged into the upper right side of an M1 Macbook Pro side by side.  There is a small sliver of monitor with the file tree of VS Code open visible on the right side.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I figured out that if I disconnect the HDMI cable plugged directly into the laptop and then unplug and re-plug the USB hub, it will almost always connect successfully after less than a minute. Then, I can re-connect the HDMI cable without issue and use both my monitors as well as my external mouse and keyboard with no problems.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t know why this happens; it might be some bug with my USB hub, some issue with the M1 Mac itself with my specific peripheral configuration, some static electricity or interference thing going on - who knows. I do know that I&amp;rsquo;ve ended up burning multiple hours waiting for this stuff to connect over the past couple of years and I&amp;rsquo;m happy to have finally figured out a solution.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Investigating Fidget Spinner Bot</title>
      <link>https://cprimozic.net/notes/posts/fidget-spinner-bot/</link>
      <pubDate>Mon, 06 Nov 2023 11:50:37 -0800</pubDate>
      <guid>https://cprimozic.net/notes/posts/fidget-spinner-bot/</guid>
      <description>&lt;p&gt;While watching some logs for my webserver recently, I&amp;rsquo;ve noticed a significant amount of requests coming from a bot I didn&amp;rsquo;t recognize with the user agent of &lt;code&gt;fidget-spinner-bot&lt;/code&gt;. It seems to be pretty aggressively crawling my personal network of sites that I maintain, following links and downloading page contents. Some requests are also coming from user agents including &lt;code&gt;my-tiny-bot&lt;/code&gt;, &lt;code&gt;thesis-research-bot&lt;/code&gt;, &lt;code&gt;test-bot&lt;/code&gt; which seems to be the same or related. I usually recognize most of the user agents of bots making significant amounts of requests to my server, so these stood out to me.&lt;/p&gt;</description>
      <content>&lt;p&gt;While watching some logs for my webserver recently, I&amp;rsquo;ve noticed a significant amount of requests coming from a bot I didn&amp;rsquo;t recognize with the user agent of &lt;code&gt;fidget-spinner-bot&lt;/code&gt;. It seems to be pretty aggressively crawling my personal network of sites that I maintain, following links and downloading page contents. Some requests are also coming from user agents including &lt;code&gt;my-tiny-bot&lt;/code&gt;, &lt;code&gt;thesis-research-bot&lt;/code&gt;, &lt;code&gt;test-bot&lt;/code&gt; which seems to be the same or related. I usually recognize most of the user agents of bots making significant amounts of requests to my server, so these stood out to me.&lt;/p&gt;
&lt;p&gt;I first noticed this bot around the beginning of November 2023, but it&amp;rsquo;s possible it started earlier than that. I looked around online to see if other people were noticing traffic from this bot, and there are a few discussions but not many. There&amp;rsquo;s a &lt;a href=&#34;https://www.webmasterworld.com/search_engine_spiders/5096715.htm&#34;&gt;forum thread&lt;/a&gt; on a webmaster forum where people were noticing traffic from these bots as well, but that&amp;rsquo;s pretty much all I could find.&lt;/p&gt;
&lt;p&gt;As mentioned on that thread, these bots are both hosted on AWS IP space. Requests are spread out over multiple IP addresses which would indicate that there are multiple coordinated instances of these bots doing this crawling rather than just some little project someone set up on a VPS.&lt;/p&gt;
&lt;p&gt;They don&amp;rsquo;t seem to be doing anything that seems malicious as far as I can tell. Their request counts are high but very much reasonable - at least for my server. They only seem to be requesting the HTML body of pages and not downloading any CSS, images, scripts, etc. There was some speculation on that webmasterworld thread that they might gathering training data for AI, but most people just go with commoncrawl for that rather than doing the collection themselves.&lt;/p&gt;
&lt;p&gt;These bots follow 301 redirects and seem to spider links pretty naively. I have some densely linked doc pages for a browser-based synthesizer I built with a lot of densely inter-linked pages, and it&amp;rsquo;s spending a lot of time crawling those pages in circles. I&amp;rsquo;m not 100% sure it&amp;rsquo;s re-crawling the same pages, but it does seem to be.&lt;/p&gt;
&lt;p&gt;All of its requests are made via HTTP 1.1. Individual requests are spaced 30 seconds to 2 minutes apart from what I can see, and that&amp;rsquo;s across all of my different domains. It&amp;rsquo;s made requests to at least 3 of my domains so far which makes sense since many of them are linked to from this website.&lt;/p&gt;
&lt;p&gt;Anyway, it&amp;rsquo;s an interesting bot and I&amp;rsquo;d be very curious to know what it&amp;rsquo;s doing, who runs it, etc. If someone knows, feel free to &lt;a href=&#34;https://cprimozic.net/contact/&#34;&gt;get in contact&lt;/a&gt; and I&amp;rsquo;ll update this page with anything people find out.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;UPDATE 2023-11-12&lt;/p&gt;
&lt;p&gt;I was contacted via email from someone who works as a network administrator at a hosting company.  He found this post and told me that he&amp;rsquo;s also been seeing high levels of traffic across many of his customers&amp;rsquo; websites hosted there - to the point that he felt it was malicious.&lt;/p&gt;
&lt;p&gt;He says that the bot seemed to be making specialized requests to various types of sites (woocommerce, drupal, wordpress, etc.) with the intention of using up as many resources as possible.  I can&amp;rsquo;t corroborate that myself as I don&amp;rsquo;t really host any of those kinds of sites and hadn&amp;rsquo;t noticed malicious-level traffic on my own sites/server.&lt;/p&gt;
&lt;p&gt;That being said, I did another search online and found some other people that were reporting that the bot was making huge numbers of requests and was causing service disruptions on their sites: &lt;a href=&#34;https://community.cloudflare.com/t/getting-tons-of-bad-bots-attack-recently/578502/14&#34;&gt;https://community.cloudflare.com/t/getting-tons-of-bad-bots-attack-recently/578502/14&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I just scanned my server logs and for today, I don&amp;rsquo;t see any requests from these bots.  Maybe they&amp;rsquo;ve gone, or maybe they&amp;rsquo;ve changed their user agents.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Exporting Minecraft Objects to Three.JS</title>
      <link>https://cprimozic.net/notes/posts/exporting-minecraft-objects-to-threejs/</link>
      <pubDate>Fri, 03 Nov 2023 03:47:51 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/exporting-minecraft-objects-to-threejs/</guid>
      <description>&lt;p&gt;Recently, I&amp;rsquo;ve been working on some &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;interactive sketches/games&lt;/a&gt; in Three.JS. For one of the levels I was building, I had the idea of importing something I built from one of my old MineCraft survival worlds in to use as part of it.&lt;/p&gt;
&lt;p&gt;I figured that there was a pretty good chance of some software existing to export MineCraft levels to some 3D model format for 3D rendering or other purposes, and that indeed is the case. There are multiple options out there, but the one I chose to go with was &lt;a href=&#34;https://github.com/jmc2obj/j-mc-2-obj/wiki/Getting-started&#34;&gt;&lt;code&gt;jmc2obj&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;</description>
      <content>&lt;p&gt;Recently, I&amp;rsquo;ve been working on some &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;interactive sketches/games&lt;/a&gt; in Three.JS. For one of the levels I was building, I had the idea of importing something I built from one of my old MineCraft survival worlds in to use as part of it.&lt;/p&gt;
&lt;p&gt;I figured that there was a pretty good chance of some software existing to export MineCraft levels to some 3D model format for 3D rendering or other purposes, and that indeed is the case. There are multiple options out there, but the one I chose to go with was &lt;a href=&#34;https://github.com/jmc2obj/j-mc-2-obj/wiki/Getting-started&#34;&gt;&lt;code&gt;jmc2obj&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;exporting-with-jmc2obj&#34;&gt;Exporting with &lt;code&gt;jmc2obj&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;jmc2obj&lt;/code&gt; is a fairly minimalistic application which lets you export parts of a MineCraft world into .obj format - a very common and simple 3D object format. I chose it because it&amp;rsquo;s very simple to install (just a standalone .jar file you can download), it&amp;rsquo;s fully open source, and it has some pretty good docs and usage guides.&lt;/p&gt;
&lt;p&gt;Its UI is pretty straightforward to use. You load your world, select the dimension to export, select the region of the world you want to export, set some export settings, and get a .obj file as output.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bmj.png&#34; alt=&#34;Screenshot of the jmc2obj UI showing a superflat MineCraft world loaded in.  The UI shows several buttons and other controls for things like making and editing selections, changing settings, and loading different worlds.&#34;&gt;&lt;/p&gt;
&lt;p&gt;There are a good deal of options, but most are fine at their defaults and there are some &lt;a href=&#34;https://github.com/jmc2obj/j-mc-2-obj/wiki/Options&#34;&gt;thorough docs&lt;/a&gt; on them as well. Here are a few you might want to take a look at though:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;Select blocks to export&amp;rdquo; if you want to filter out some blocks from the selection. I used this to get rid of torches, ladders, and some other stuff that I didn&amp;rsquo;t want included in the generated model.&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Export Textures&amp;rdquo; if you want the generated model to be textured with MineCraft textures. It will use the vanilla resource pack textures by default, but you can also load in your own resource pack.
&lt;ul&gt;
&lt;li&gt;Apparently there are also some special PBR resource packs that have things like normal maps built in. &lt;code&gt;jmc2obj&lt;/code&gt; has support for these, and if you use one the resulting model&amp;rsquo;s materials will be full-fledged PBR materials themselves.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After you run the export, you should have a .obj file and optionally some associated .mtl files. These contain the geometry and textures (if applicable) for your exported model respectively.&lt;/p&gt;
&lt;h2 id=&#34;loading-the-obj-model-in-threejs&#34;&gt;Loading the .obj Model in Three.JS&lt;/h2&gt;
&lt;p&gt;Three.JS has built-in support for loading .obj and .mtl files, so it&amp;rsquo;s actually possible to load the exported model into Three.JS directly.  After serving the generated files (.obj, .mtl, and the &lt;code&gt;tex&lt;/code&gt; directory containing texture images), they can be loaded with code something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;OBJLoader&lt;/span&gt; } &lt;span style=&#34;color:#66d9ef&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;three/addons/loaders/OBJLoader.js&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;MTLLoader&lt;/span&gt; } &lt;span style=&#34;color:#66d9ef&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;three/examples/jsm/loaders/MTLLoader.js&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;mtlLoader&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MTLLoader&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;mtlLoader&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;load&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/obj/mc-demo.mtl&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;materials&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;materials&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;preload&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;objLoader&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;OBJLoader&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;objLoader&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setMaterials&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;materials&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;objLoader&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;load&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;/obj/mc-demo.obj&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;obj&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;scene&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;obj&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After adding lights, orbit controls, and some light postprocessing, the result for my export looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bmk.png&#34; alt=&#34;Screenshot of a MineCraft build exported via jmc2obj loaded into Three.JS with ObjLoader.  The build itself is a stone wall surrounding a grassy field with textures similar to the MineCraft defaults, floating in a black void.&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;loading--modifying-the-model-in-blender&#34;&gt;Loading + Modifying the Model in Blender&lt;/h2&gt;
&lt;p&gt;This indeed works!  You can load these models into your world, add other stuff, and even integrate them with some physics engine like &lt;a href=&#34;https://rapier.rs/&#34;&gt;rapier&lt;/a&gt; or &lt;a href=&#34;https://github.com/kripken/ammo.js&#34;&gt;ammo.js&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For my purposes, though, I wanted to do a bit more and move away from the default MineCraft aesthetic.  Since the exported .obj file is a standard 3D model, it&amp;rsquo;s possible to work with it like any other 3D model.  I use Blender for all my 3D modelling, so I imported the .obj into Blender.  It&amp;rsquo;s extremely easy - just File -&amp;gt; Import -&amp;gt; Wavefront (.obj) and you should see it loaded with textures:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bml.png&#34; alt=&#34;Screenshot of the MineCraft build exported via jmc2obj loaded into Blender.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;re-exporting-to-gltf--building-a-scene&#34;&gt;Re-Exporting to glTF + Building a Scene&lt;/h3&gt;
&lt;p&gt;Now that we&amp;rsquo;re working in Blender, we can do a ton of stuff to the model.  I re-textured it completely to hide the fact that it&amp;rsquo;s made out of individual blocks.  In blender, I hand-modelled some other objects to flesh out the scene.  Then, I re-exported it as a gLTF file which is a more modern and feature-rich model format which is very commonly used in Three.JS.&lt;/p&gt;
&lt;p&gt;In Three.JS, I added in some more lighting, procedural terrain, and volumetric fog.  Here&amp;rsquo;s the result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bl4.png&#34; alt=&#34;Screenshot of a scene in Three.JS.  It consists of a stone wall I built in MineCraft and exported via jmc2obj, volumetric fog, rocky procedural terrain, and some spooky green lighting.&#34;&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the same wall as the one from MineCraft, but the aesthetics are completely different.  I&amp;rsquo;m a big fan of the creative possibilities of using this kind of technique.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Google Cloud Platform Gems</title>
      <link>https://cprimozic.net/notes/posts/google-cloud-platform-gems/</link>
      <pubDate>Wed, 25 Oct 2023 13:36:15 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/google-cloud-platform-gems/</guid>
      <description>&lt;p&gt;I was reading &lt;a href=&#34;https://news.ycombinator.com/item?id=38016849&#34;&gt;a Hacker News thread&lt;/a&gt; for an article comparing GCP to some alternatives like AWS. I&amp;rsquo;ve been a GCP user for a good while now, and it&amp;rsquo;s definitely my go-to public cloud. We also use it at my dayjob at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Reading the article and comments got me thinking about some of my favorite GCP features. GCP has a few excellent gems which are better than pretty much any competing cloud offering:&lt;/p&gt;</description>
      <content>&lt;p&gt;I was reading &lt;a href=&#34;https://news.ycombinator.com/item?id=38016849&#34;&gt;a Hacker News thread&lt;/a&gt; for an article comparing GCP to some alternatives like AWS. I&amp;rsquo;ve been a GCP user for a good while now, and it&amp;rsquo;s definitely my go-to public cloud. We also use it at my dayjob at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Reading the article and comments got me thinking about some of my favorite GCP features. GCP has a few excellent gems which are better than pretty much any competing cloud offering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://cloud.google.com/run&#34;&gt;Cloud Run&lt;/a&gt;. Best way to deploy containers hands down. All of the benefits of a serverless/containerized workload with all the ease of a traditional VPS deployment. Extremely cheap (pay $0 for side projects with little traffic).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://cloud.google.com/bigquery&#34;&gt;BigQuery&lt;/a&gt;. Very easy to use with immense power without having to deal with the details yourself. Billing can be either extremely cheap or rather expensive depending on what you&amp;rsquo;re doing, though.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://cloud.google.com/storage/docs/storage-classes#archive&#34;&gt;GCS Archive Storage&lt;/a&gt;. 1/3 the cost of glacier for storage ($0.0012/GB/Month lol), but more for retrieval. Has none of the annoyances of Glacier (you can download your files instantly, upload using normal APIs). Perfect for extremely cheap backups.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://cloud.google.com/spanner&#34;&gt;Cloud Spanner&lt;/a&gt;. I&amp;rsquo;ve not had a reason to use this yet myself, but there really aren&amp;rsquo;t any comparable offerings that I know of.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
    </item>
    
    <item>
      <title>Fixing Nginx Reverse Proxy Server Not Compressing gltf/glb Files</title>
      <link>https://cprimozic.net/notes/posts/fixing-nginx-proxy-server-not-compressing-gltf-glb/</link>
      <pubDate>Tue, 24 Oct 2023 03:39:17 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-nginx-proxy-server-not-compressing-gltf-glb/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been building some &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;interactive sketches/games&lt;/a&gt; in Three.JS, and I wanted to deploy it on my server. I export the models used by the level from Blender in glTF format, which is a modern, well-supported, and commonly used format for this. Specifically, I exported the models as a .glb file.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;When I loaded my levels in the browser, I noticed that the .glb file wasn&amp;rsquo;t getting compressed with gzip. This was a problem because the file is quite large - over 4MB - and glb is a format that should benefit greatly from compression.&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;ve been building some &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;interactive sketches/games&lt;/a&gt; in Three.JS, and I wanted to deploy it on my server. I export the models used by the level from Blender in glTF format, which is a modern, well-supported, and commonly used format for this. Specifically, I exported the models as a .glb file.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;When I loaded my levels in the browser, I noticed that the .glb file wasn&amp;rsquo;t getting compressed with gzip. This was a problem because the file is quite large - over 4MB - and glb is a format that should benefit greatly from compression.&lt;/p&gt;
&lt;p&gt;For my particular server setup, I use NGINX as a reverse proxy to a different Apache2 server hosting the raw files. Somewhere along the line, the compression was not taking place for some reason.&lt;/p&gt;
&lt;h2 id=&#34;nginx-config-changes&#34;&gt;NGINX Config Changes&lt;/h2&gt;
&lt;p&gt;There were a few things causing this issue for me.&lt;/p&gt;
&lt;p&gt;First, I had to add some MIME type definitions to my NGINX server manually to tell it that gltf/glb files are compressible. I added this to my &lt;code&gt;nginx.conf&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  gzip_proxied any;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  gzip_types text/plain text/css ... ... model/gltf model/gltf-binary;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  brotli_types text/plain text/css ... ... model/gltf model/gltf-binary;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This gives a list of MIME types that NGINX will treat as compressible. I also added some entries to the &lt;code&gt;mime.types&lt;/code&gt; file to instruct it to map .gltf and .glb files to those mime types:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  model/gltf                            gltf;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  model/gltf-binary                     glb;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, even after doing that, it still wasn&amp;rsquo;t working. The &lt;code&gt;gzip_proxied&lt;/code&gt; directive should be causing it to compress any compressible responses from the upstream Apache2 server even if they weren&amp;rsquo;t compressed natively, but it wasn&amp;rsquo;t happening.&lt;/p&gt;
&lt;h2 id=&#34;apache2-config-changes&#34;&gt;Apache2 Config Changes&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;It turns out that I also needed to make a config change on the upstream Apache2 server to get it to work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;NGINX bases its checks on whether or not to compress a proxied response based on the &lt;code&gt;Content-Type&lt;/code&gt; header of that response, which is set by the upstream. It turns out that the upstream Apache2 server serving the .glb file didn&amp;rsquo;t know about its MIME type, so I had to add config entries there as well to map the extension to the MIME type:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  AddType model/gltf .gltf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  AddType model/gltf-binary .glb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After doing that, the Apache2 upstream server properly set the &lt;code&gt;Content-Type: model/gltf-binary&lt;/code&gt; header on the response, even though it didn&amp;rsquo;t compress it. Then, the NGINX reverse proxy server saw that content type and matched it to the compressible MIME types I configured earlier and properly compressed it before forwarding it to the user.&lt;/p&gt;
&lt;p&gt;After compressing with gzip, my glb response size went down from 4.2MB to under 1MB; very much worth the effort!&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Speeding Up Geodesic Tracing in `geometry-central`</title>
      <link>https://cprimozic.net/notes/posts/speeding-up-geodesic-tracing-in-geometry-central/</link>
      <pubDate>Thu, 12 Oct 2023 01:21:13 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/speeding-up-geodesic-tracing-in-geometry-central/</guid>
      <description>&lt;p&gt;As part of some recent work in procedural mesh generation, I&amp;rsquo;ve been working with a computational geometry library called &lt;a href=&#34;https://github.com/nmwsharp/geometry-central&#34;&gt;&lt;code&gt;geometry-central&lt;/code&gt;&lt;/a&gt; to trace &lt;a href=&#34;https://geometry-central.net/surface/algorithms/geodesic_paths/&#34;&gt;geodesic paths&lt;/a&gt; on the surface of 3D meshes. &lt;code&gt;geometry-central&lt;/code&gt; is written in C++, and I compiled the library to WebAssembly with Emscripten in order to use it in the browser.&lt;/p&gt;
&lt;p&gt;As I recently learned, a geodesic path is the straightest path along a surface. It&amp;rsquo;s the path you would take if you were to walk in a straight line across the surface of some manifold for some distance in some direction.&lt;/p&gt;</description>
      <content>&lt;p&gt;As part of some recent work in procedural mesh generation, I&amp;rsquo;ve been working with a computational geometry library called &lt;a href=&#34;https://github.com/nmwsharp/geometry-central&#34;&gt;&lt;code&gt;geometry-central&lt;/code&gt;&lt;/a&gt; to trace &lt;a href=&#34;https://geometry-central.net/surface/algorithms/geodesic_paths/&#34;&gt;geodesic paths&lt;/a&gt; on the surface of 3D meshes. &lt;code&gt;geometry-central&lt;/code&gt; is written in C++, and I compiled the library to WebAssembly with Emscripten in order to use it in the browser.&lt;/p&gt;
&lt;p&gt;As I recently learned, a geodesic path is the straightest path along a surface. It&amp;rsquo;s the path you would take if you were to walk in a straight line across the surface of some manifold for some distance in some direction.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using them to wrap a 2D mesh onto the surface of a 3D mesh, kind of like inverse UV mapping or a sort of variation of the &amp;ldquo;shrink wrap&amp;rdquo; modifier from Blender. After extruding the output from that, the result looks pretty cool:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bk0.png&#34; alt=&#34;Screenshot of a stringy white mesh rendered with Blender.  It consists of a series of thin white curlicues that are processing outward onto different planes, meeting at a common corner.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I need trace up to millions of geodesic paths to generate this, and I want to do so as quickly as possible in order to make it possible to generate it dynamically. However, the initial performance numbers I was seeing weren&amp;rsquo;t very optimism-inspiring:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bk1.png&#34; alt=&#34;Screenshot of Chrome dev tools profiler flame graph showing a function for computing geodesics taking 3.22 seconds to run&#34;&gt;&lt;/p&gt;
&lt;p&gt;Drilling in further, it became clear that some functions from the Eigen linear algebra library named &lt;code&gt;ColPivHouseholder&lt;/code&gt; were the bottleneck of the process:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bk3.png&#34; alt=&#34;Screenshot of the Chrome dev tools profiler flame graph showing that functions from Eigen called ColPivHouseholder were where most of the CPU time was being spent when computing geodesics&#34;&gt;&lt;/p&gt;
&lt;p&gt;I found the function in the &lt;code&gt;geometry-central&lt;/code&gt; code where this was getting called. It was part of the logic for converting cartesian coordinate to barycentric coordinates. Barycentric coordinates are a way of representing some point within a triangle as a normalized mixture of each of the triangle&amp;rsquo;s points. It is used by the geodesic path tracing process.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the code itself:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-c++&#34; data-lang=&#34;c++&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;inline&lt;/span&gt; Vector3 &lt;span style=&#34;color:#a6e22e&#34;&gt;normalizeBarycentricDisplacement&lt;/span&gt;(Vector3 baryVec) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; s &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; sum(baryVec);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; baryVec &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; Vector3&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;constant(s &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3.&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;inline&lt;/span&gt; Vector3 &lt;span style=&#34;color:#a6e22e&#34;&gt;cartesianVectorToBarycentric&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; std&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;array&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Vector2, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;amp;&lt;/span&gt; vertCoords, Vector2 faceVec) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Build matrix for linear transform problem
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// (last constraint comes from chosing the displacement vector with sum = 0)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Eigen&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;Matrix3d A;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Eigen&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;Vector3d rhs;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; std&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;array&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Vector2, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;amp;&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; vertCoords; &lt;span style=&#34;color:#75715e&#34;&gt;// short name
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  A &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; c[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;].x, c[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;].x, c[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;].x, c[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;].y, c[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;].y, c[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;].y, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1.&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  rhs &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; faceVec.x, faceVec.y, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Solve
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Eigen&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;Vector3d result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; A.colPivHouseholderQr().solve(rhs);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Vector3 resultBary{result(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;), result(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;), result(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  resultBary &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; normalizeBarycentricDisplacement(resultBary);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; resultBary;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&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;geometry-central&lt;/code&gt; chose to implement the conversion by solving the system of equations directly via one of Eigen&amp;rsquo;s built-in solvers. This is a totally valid and correct way to handle it, but as you might imagine it leaves a lot of performance on the table.&lt;/p&gt;
&lt;p&gt;The data has to be copied into the Eigen matrices, the solver has to do whatever initialization and cleanup it needs to, and then there&amp;rsquo;s that additional &lt;code&gt;normalizeBarycentricDisplacement&lt;/code&gt; operation at the end that I don&amp;rsquo;t quite understand the purpose of.&lt;/p&gt;
&lt;p&gt;Since we know we&amp;rsquo;re doing Cartesian -&amp;gt; barycentric coordinate conversion, we can use a bit of a more specific algorithm than what Eigen might be doing here.&lt;/p&gt;
&lt;p&gt;I found some code &lt;a href=&#34;https://gamedev.stackexchange.com/a/23745&#34;&gt;on Gamedev Stack Exchange&lt;/a&gt; that itself was based on code from &lt;a href=&#34;http://realtimecollisiondetection.net/&#34;&gt;http://realtimecollisiondetection.net/&lt;/a&gt; by Christer Ericson that implements the conversion process using simple math operations inline. I made some small tweaks to get the coordinates looking exactly like they did using Eigen, and wound up with this equivalent code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-c++&#34; data-lang=&#34;c++&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;inline&lt;/span&gt; Vector3 &lt;span style=&#34;color:#a6e22e&#34;&gt;cartesianVectorToBarycentric&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; std&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;array&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Vector2, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&amp;amp;&lt;/span&gt; vertCoords, Vector2 faceVec) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Vector2 v0 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; vertCoords[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; vertCoords[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Vector2 v1 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; vertCoords[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; vertCoords[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Vector2 v2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; faceVec &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; vertCoords[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; d00 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; dot(v0, v0);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; d01 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; dot(v0, v1);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; d11 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; dot(v1, v1);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; d20 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; dot(v2, v0);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; d21 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; dot(v2, v1);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; denom &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; d00 &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; d11 &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; d01 &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; d01;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; v &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (d11 &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; d20 &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; d01 &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; d21) &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; denom;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; w &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (d00 &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; d21 &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; d01 &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; d20) &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; denom;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;double&lt;/span&gt; u &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; v &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; w;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; Vector3{u, v, w};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;results&#34;&gt;Results&lt;/h2&gt;
&lt;p&gt;Well is it faster? Yep!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bk2.png&#34; alt=&#34;Screenshot of the Chrome dev tools profiler flame graph showing that the total runtime for the geodesic path computation function has dropped from over 3 seconds to 842 milliseconds.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The entire function is over 3x faster and there&amp;rsquo;s other stuff going on besides this barycentric coordinate conversion, so the speedup for that is even greater. This should now be fast enough for my purposes.&lt;/p&gt;
&lt;p&gt;If I need more, the answer on stack exchange points out that a lot of the computations in that function can be pre-computed per triangle since they don&amp;rsquo;t depend on the input Cartesian coordinates. I&amp;rsquo;m not sure how much of an impact that would actually make for this case, though, but it&amp;rsquo;s an option to pursue if needed.&lt;/p&gt;
&lt;p&gt;It was very nice to find an easy to spot bottleneck and a pre-made optimization I was able to drop in with little effort.&lt;/p&gt;
&lt;p&gt;I pushed up these changes &lt;a href=&#34;https://github.com/Ameobea/geometry-central/tree/optimize-barycentric-coordinate-computation&#34;&gt;to a fork&lt;/a&gt; of &lt;code&gt;geometry-central&lt;/code&gt; if you&amp;rsquo;re interested in using them directly.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing rust-analyzer to work with `uuid_unstable` for v7 UUIDs</title>
      <link>https://cprimozic.net/notes/posts/fixing-rust-analyzer-with-uuid_unstable-uuid-v7/</link>
      <pubDate>Mon, 02 Oct 2023 20:28:57 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-rust-analyzer-with-uuid_unstable-uuid-v7/</guid>
      <description>&lt;p&gt;Recently, one of my coworkers added code that uses v7 UUIDs. v7 UUIDs are a new type of UUID that contains a timestamp along with randomness, making them useful for DB primary keys and similar things.&lt;/p&gt;
&lt;p&gt;However, that code broke my rust-analyzer install for local development. I already was running rust nightly locally, so there was some other issue. It said that &lt;code&gt;Uuid::new_v7()&lt;/code&gt; wasn&amp;rsquo;t a function even though the &lt;code&gt;v7&lt;/code&gt; feature was enabled for the &lt;code&gt;uuid&lt;/code&gt; crate and it was at the latest version.&lt;/p&gt;</description>
      <content>&lt;p&gt;Recently, one of my coworkers added code that uses v7 UUIDs. v7 UUIDs are a new type of UUID that contains a timestamp along with randomness, making them useful for DB primary keys and similar things.&lt;/p&gt;
&lt;p&gt;However, that code broke my rust-analyzer install for local development. I already was running rust nightly locally, so there was some other issue. It said that &lt;code&gt;Uuid::new_v7()&lt;/code&gt; wasn&amp;rsquo;t a function even though the &lt;code&gt;v7&lt;/code&gt; feature was enabled for the &lt;code&gt;uuid&lt;/code&gt; crate and it was at the latest version.&lt;/p&gt;
&lt;p&gt;Looking at the code, it seems that the UUID v7 functions are only available if the &lt;code&gt;uuid_unstable&lt;/code&gt; feature is set:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#[cfg(all(uuid_unstable, feature = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;v7&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;))]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;mod&lt;/span&gt; v7;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We already had a &lt;code&gt;.cargo/config.toml&lt;/code&gt; file in our workspace root that had this config in it to enable that cfg item:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# required to enable the v7 feature in the uuid crate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# https://docs.rs/uuid/latest/uuid/#unstable-features&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;rustflags&lt;/span&gt; = [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--cfg&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;uuid_unstable&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, rust-analyzer wasn&amp;rsquo;t picking that up and was showing errors in the code during development.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I added these two items to my VS code config:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;rust-analyzer.check.extraEnv&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;RUSTFLAGS&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--cfg uuid_unstable&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;rust-analyzer.cargo.extraEnv&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;RUSTFLAGS&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--cfg uuid_unstable&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After setting those and reloading VS code, rust-analyzer properly finds that function, the error goes away, and I have inline documentation for it as well:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bj4.png&#34; alt=&#34;A screenshot of inline documentation showing up on hover for the Uuid::new_v7() function in VS Code while editing some Rust code&#34;&gt;&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing `HSA_STATUS_ERROR_OUT_OF_RESOURCES` Error with Stable Diffusion</title>
      <link>https://cprimozic.net/notes/posts/fixing-stable-diffusion-hsa_status_error_out_of_resources/</link>
      <pubDate>Sun, 01 Oct 2023 22:45:34 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-stable-diffusion-hsa_status_error_out_of_resources/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been using Stable Diffusion XL via the Automatic1111 web UI to generate PBR textures for use in my Three.JS projects, as &lt;a href=&#34;https://cprimozic.net/notes/posts/generating-textures-for-3d-using-stable-diffusion/&#34;&gt;I&amp;rsquo;ve written about previously&lt;/a&gt;.  Everything was going great until at random, the generation started crashing at 100% and I got this error in my console:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:0:rocdevice.cpp            :2786: 58285575154 us: 154003: [tid:0x7f59ee1216c0] Callback: Queue 0x7f583ec00000 Aborting with error : HSA_STATUS_ERROR_OUT_OF_RESOURCES: The runtime failed to allocate the necessary resources. This error may also occur when the core runtime library needs to spawn threads or create internal OS-specific events. Code: 0x1008 Available Free mem : 13386 MB ./webui.sh: line 254: 154003 Aborted                 (core dumped) &amp;quot;${python_cmd}&amp;quot; &amp;quot;${LAUNCH_SCRIPT}&amp;quot; &amp;quot;$@&amp;quot;&lt;/code&gt;&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;ve been using Stable Diffusion XL via the Automatic1111 web UI to generate PBR textures for use in my Three.JS projects, as &lt;a href=&#34;https://cprimozic.net/notes/posts/generating-textures-for-3d-using-stable-diffusion/&#34;&gt;I&amp;rsquo;ve written about previously&lt;/a&gt;.  Everything was going great until at random, the generation started crashing at 100% and I got this error in my console:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;:0:rocdevice.cpp            :2786: 58285575154 us: 154003: [tid:0x7f59ee1216c0] Callback: Queue 0x7f583ec00000 Aborting with error : HSA_STATUS_ERROR_OUT_OF_RESOURCES: The runtime failed to allocate the necessary resources. This error may also occur when the core runtime library needs to spawn threads or create internal OS-specific events. Code: 0x1008 Available Free mem : 13386 MB ./webui.sh: line 254: 154003 Aborted                 (core dumped) &amp;quot;${python_cmd}&amp;quot; &amp;quot;${LAUNCH_SCRIPT}&amp;quot; &amp;quot;$@&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;My GPU didn&amp;rsquo;t seem to be close to running out of memory, I hadn&amp;rsquo;t changed anything notable with my system or with stable diffusion; I was stumped.&lt;/p&gt;
&lt;p&gt;I tried several things which didn&amp;rsquo;t work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Updating ROCm from 5.5 to 5.7&lt;/li&gt;
&lt;li&gt;Rebooting my computer multiple times&lt;/li&gt;
&lt;li&gt;Closing all other running applications&lt;/li&gt;
&lt;li&gt;Disabling all my stable diffusion extensions and resetting all settings to default&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;the-solution&#34;&gt;The Solution&lt;/h2&gt;
&lt;p&gt;What finally fixed it was pulling the latest automatic1111 stable diffusion web UI and using that.  I&amp;rsquo;m now able to generate images without issue again.&lt;/p&gt;
&lt;p&gt;So yeah - if you&amp;rsquo;re getting this issue or one like it, pulling the latest code for that and see if it fixes it.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Investigating Hill Noise for Terrain Generation</title>
      <link>https://cprimozic.net/notes/posts/investigating-hill-noise-for-terrain-generation/</link>
      <pubDate>Tue, 26 Sep 2023 22:21:51 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/investigating-hill-noise-for-terrain-generation/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been working on some procedural terrain generation for my 3D work in Three.JS lately.  I wanted to try out a fresh noise function for generating terrain; everyone has used Perlin noise or some variant of it for decades.&lt;/p&gt;
&lt;p&gt;I came across &lt;a href=&#34;https://blog.bruce-hill.com/hill-noise&#34;&gt;a blog post&lt;/a&gt; by Bruce Hill describing a noise function he designed himself - called Hill Noise.  It works by combining a bunch of sine waves together with different offsets and rotations.  If you add enough of them together, the results can look pretty organic and random.&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;ve been working on some procedural terrain generation for my 3D work in Three.JS lately.  I wanted to try out a fresh noise function for generating terrain; everyone has used Perlin noise or some variant of it for decades.&lt;/p&gt;
&lt;p&gt;I came across &lt;a href=&#34;https://blog.bruce-hill.com/hill-noise&#34;&gt;a blog post&lt;/a&gt; by Bruce Hill describing a noise function he designed himself - called Hill Noise.  It works by combining a bunch of sine waves together with different offsets and rotations.  If you add enough of them together, the results can look pretty organic and random.&lt;/p&gt;
&lt;p&gt;The code is very short to implement this, so I ported it to Rust with minimal effort.  I then tested it out generating some terrain.  Here&amp;rsquo;s one of the results:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bi7.png&#34; alt=&#34;Screenshot of some floating test terrain generated using Hill noise.  It’s textured with a reddish granite-looking material that has no apparent repetition or seams due to the use of a hex tiling shader&#34;&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s alright.  This example uses 11 octaves of hill noise with wavelengths of &lt;code&gt;[220, 160, 120, 100, 75, 40, 20, 10, 5, 2, 1]&lt;/code&gt;.  So that requires 11 sin operations per lookup; not the cheapest thing in the world, but it&amp;rsquo;s not horrible.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s closer-up view with a wireframe to see a bit more detail:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bia.png&#34; alt=&#34;Screenshot of a zoomed-in version of the terrain generated using Hill noise, rendered as a multicolored wireframe.  There is a regular lumpyness that shows through on the surface of the terrain, with the lumps all about the same size, shape, spacing.&#34;&gt;&lt;/p&gt;
&lt;p&gt;You can kinda see the higher-frequency sine waves showing through on the surface of the terrain.  It looks quite regular with all the lumps about the same size, shape, and spacing.  It&amp;rsquo;s probably possible to alleviate this by fine-tuning/adding more wavelengths or manually setting amplitudes.&lt;/p&gt;
&lt;p&gt;When I expand the terrain out further, here&amp;rsquo;s how it looks:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bi9.png&#34; alt=&#34;Screenshot of some floating test terrain generated using Hill noise, expanded out much further.  It’s pretty clear where the sine waves at the lower frequencies repeat.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The sine waves at the lower frequencies show through and the result looks pretty grid like and inorganic - at least it does to me.&lt;/p&gt;
&lt;p&gt;One of the demos from Hill&amp;rsquo;s blog (&lt;a href=&#34;https://blog.bruce-hill.com/media/hill-noise/hill_noise_2dshader.html&#34;&gt;https://blog.bruce-hill.com/media/hill-noise/hill_noise_2dshader.html&lt;/a&gt;) uses 31 octaves of noise, but if you zoom out far enough you get the same issue.&lt;/p&gt;
&lt;p&gt;You can probably find combinations of wavelengths to make it look at least decent for most settings, but it seems harder to work with than a full-fledged Perlin or Simplex noise.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is an incredibly simple noise function to implement, and it&amp;rsquo;s not that expensive to compute.  It suffers from some visible tilings due to lower wavelengths of sine waves showing through.&lt;/p&gt;
&lt;p&gt;It might be useful for some terrain generation uses, but probably not by itself.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Some Notes and Docs on the Rust Blackjack Procedural Geometry Tool</title>
      <link>https://cprimozic.net/notes/posts/rust-blackjack-notes-docs/</link>
      <pubDate>Sun, 24 Sep 2023 15:26:03 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/rust-blackjack-notes-docs/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been trying out a tool written in Rust called &lt;a href=&#34;https://github.com/setzer22/blackjack&#34;&gt;Blackjack&lt;/a&gt; for procedural, node-based 3D modelling.  It&amp;rsquo;s a lot like Blender&amp;rsquo;s geometry nodes.&lt;/p&gt;
&lt;p&gt;There aren&amp;rsquo;t a lot in terms of docs, and it looks like the project isn&amp;rsquo;t being actively developed right now.  Nonetheless, I think it&amp;rsquo;s an extremely cool project, and the code is very high quality.  So, I&amp;rsquo;ve been spending a bit of time getting familiar with it and trying it out.&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;ve been trying out a tool written in Rust called &lt;a href=&#34;https://github.com/setzer22/blackjack&#34;&gt;Blackjack&lt;/a&gt; for procedural, node-based 3D modelling.  It&amp;rsquo;s a lot like Blender&amp;rsquo;s geometry nodes.&lt;/p&gt;
&lt;p&gt;There aren&amp;rsquo;t a lot in terms of docs, and it looks like the project isn&amp;rsquo;t being actively developed right now.  Nonetheless, I think it&amp;rsquo;s an extremely cool project, and the code is very high quality.  So, I&amp;rsquo;ve been spending a bit of time getting familiar with it and trying it out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This was written in September 2023.  If development of this project ever picks back up, it&amp;rsquo;s likely that much of this will go out of date fast.&lt;/p&gt;
&lt;h2 id=&#34;selections&#34;&gt;Selections&lt;/h2&gt;
&lt;p&gt;Some nodes like bevel, extrude, and others operate on individual edges or faces.  Here&amp;rsquo;s &amp;ldquo;Edit Geometry&amp;rdquo; for example:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bhq.png&#34; alt=&#34;Screenshot of the “Edit Geometry” node from the blackjack UI.  Shows several inputs, outputs, and parameters such as a red “mesh” input, translation, rotation, and scale params, and a dropdown selection for edit type.&#34;&gt;&lt;/p&gt;
&lt;p&gt;The way to specifying which edge/face to operate on seems to be entering their indices manually.&lt;/p&gt;
&lt;p&gt;In the code, I found that it&amp;rsquo;s possible to use some special syntax to do things like select all and select ranges of indices:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;0, 1, 2 // Select elements 0, 1 and 2
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;* // Select all elements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;0..1 // Select a range of elements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;0..5, 7..10, 13, 17, 22 // Select multiple ranges, and some single faces
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; // (empty string), selects nothing
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Another issue I had at first was figuring out the indices of things.  I found a part of the UI under the &amp;ldquo;Mesh Visuals&amp;rdquo; button which has options to display text on the model itself to make that clear:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bhr.png&#34; alt=&#34;Screenshot of the “Mesh Visuals” menu from the blackjack UI along with a slightly deformed cube rendered in the 3D viewport.  The “V” option of the “Text Overlay” section is selected, which has caused white labels to be displayed on each vertex.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I looked a little bit to see if there was a way to programmatically define selections, like pick all edges that have an angle greater than some value, but I didn&amp;rsquo;t find anything for that.&lt;/p&gt;
&lt;h2 id=&#34;terrain&#34;&gt;Terrain&lt;/h2&gt;
&lt;p&gt;One thing I was eager to try out with this tool was terrain generation.  I saw in the code that there&amp;rsquo;s some stuff there for perlin noise-based terrain generation, and I wanted to try that out.&lt;/p&gt;
&lt;p&gt;However, as far as I can tell, that isn&amp;rsquo;t currently hooked up/accessible from the blackjack UI.  The only thing that&amp;rsquo;s there is a code box to enter some lua code that&amp;rsquo;s evaluated for each point on the heightmap to generate it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bhs.png&#34; alt=&#34;Screenshot of blackjack UI showing a terrain node with some code entered to generate a heightmap, along with the rendered result in the 3D viewport.  The entered code is function (x, y) return math.sin((x &amp;#43; y) / 10) end&#34;&gt;&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t very useful for much, unfortunately, and if I do end up using this I&amp;rsquo;ll probably end up writing some code to either pass through the perlin noise-based terrain.&lt;/p&gt;
&lt;p&gt;Also something to note: If you create a named function, it won&amp;rsquo;t work; you have to use an anonymous lua function.  This might be obvious to people familiar with Lua, but it was not for me.&lt;/p&gt;
&lt;h2 id=&#34;compiling-to-wasm&#34;&gt;Compiling to Wasm&lt;/h2&gt;
&lt;p&gt;Since Blackjack is written in Rust, it should be possible to compile it to Wasm.  There&amp;rsquo;s one draft PR up on the repo with some changes aimed at facilitating that, but it&amp;rsquo;s extremely stale at this point and will likely never be merged.&lt;/p&gt;
&lt;p&gt;I spent some time yesterday working to get &lt;code&gt;blackjack_engine&lt;/code&gt; - the crate which implements the actual graph and node operations - compiled to Wasm and running in the web browser.  After some significant effort, I was able to accomplish that.  It&amp;rsquo;s able to load a .bjk file (the format produced when saving Blackjack graphs) and evaluate the graph.  This includes getting the whole lua engine (which is written in C or C++) compiled to Wasm as well.&lt;/p&gt;
&lt;p&gt;Note that this is just the engine - not the UI and editor.  It should be possible to do that in the future, though, because as I understand it the UI library that Blackjack uses does have support for running in the browser.&lt;/p&gt;
&lt;p&gt;I plan to write a separate post about this in the future.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing WebGL/Three.JS `GL_INVALID_ENUM : glDrawElements: type was GL_FLOAT` error</title>
      <link>https://cprimozic.net/notes/posts/fixing-webgl-gldrawelements-gl-invalid-enum-gl-float/</link>
      <pubDate>Sat, 23 Sep 2023 11:25:33 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-webgl-gldrawelements-gl-invalid-enum-gl-float/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was working on some procedural dynamic LOD terrain in Three.JS.  As part of this, I was manually constructing indexed &lt;code&gt;BufferGeometry&lt;/code&gt; instances.  Things were working alright, but when I added in a depth pre-pass I started getting errors like this in the console and the terrain failed to render:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;GL ERROR :GL_INVALID_ENUM : glDrawElements: type was GL_FLOAT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The code I had looked something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;vertices&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Float32Array&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;indices&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Float32Array&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ... compute terrain and populate vertices and indices ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;geometry&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BufferGeometry&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;geometry&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setAttribute&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;position&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Float32BufferAttribute&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;vertices&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;geometry&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setIndex&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BufferAttribute&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;indices&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;mesh&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Mesh&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;geometry&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;material&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;This error is caused because I accidentally used a &lt;code&gt;Float32Array&lt;/code&gt; to store the indices, but I should have used an integer array type.  I&amp;rsquo;m not sure why it originally worked and then stopped working when I added the depth pre-pass though.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I was working on some procedural dynamic LOD terrain in Three.JS.  As part of this, I was manually constructing indexed &lt;code&gt;BufferGeometry&lt;/code&gt; instances.  Things were working alright, but when I added in a depth pre-pass I started getting errors like this in the console and the terrain failed to render:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;GL ERROR :GL_INVALID_ENUM : glDrawElements: type was GL_FLOAT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The code I had looked something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;vertices&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Float32Array&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;indices&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Float32Array&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ... compute terrain and populate vertices and indices ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;geometry&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BufferGeometry&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;geometry&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setAttribute&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;position&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Float32BufferAttribute&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;vertices&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;geometry&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;setIndex&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BufferAttribute&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;indices&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;mesh&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;THREE&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Mesh&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;geometry&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;material&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;This error is caused because I accidentally used a &lt;code&gt;Float32Array&lt;/code&gt; to store the indices, but I should have used an integer array type.  I&amp;rsquo;m not sure why it originally worked and then stopped working when I added the depth pre-pass though.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I switched the code creating the arrays to this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;vertices&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Float32Array&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;indexCount&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;segments&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;u16Max&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;65&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;_535&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;indices&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;indexCount&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;u16Max&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Uint32Array&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;indexCount&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Uint16Array&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;indexCount&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and left the rest of the code the same.&lt;/p&gt;
&lt;p&gt;After doing this, the error went away and the terrain rendered correctly again!&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Debugging WebGL GLSL Shader Behavior Differences Between M1 Mac and AMD GPU</title>
      <link>https://cprimozic.net/notes/posts/debugging-webgl-glsl-shader-behavior-differences-between-m1-mac-and-amd-gpu/</link>
      <pubDate>Fri, 22 Sep 2023 12:02:03 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/debugging-webgl-glsl-shader-behavior-differences-between-m1-mac-and-amd-gpu/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been working on a shader in GLSL for implementing volumetric fog via raytracing.  I did the majority of the work for it it on my M1 Macbook laptop while traveling, but I was eager to try it out on my powerful 7900 XTX when I got home to see how it performed.&lt;/p&gt;
&lt;p&gt;To my surprise, the results looked extremely different!  The lighting was very low-detail on my desktop with the AMD GPU compared to how it looked on my Macbook.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been working on a shader in GLSL for implementing volumetric fog via raytracing.  I did the majority of the work for it it on my M1 Macbook laptop while traveling, but I was eager to try it out on my powerful 7900 XTX when I got home to see how it performed.&lt;/p&gt;
&lt;p&gt;To my surprise, the results looked extremely different!  The lighting was very low-detail on my desktop with the AMD GPU compared to how it looked on my Macbook.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how it looked on the Mac:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bhe.png&#34; alt=&#34;A screenshot of some volumetric fog rendered with my shader on my M1 Macbook laptop.  There is a green light reflecting off the surface of the fog.  The lighting is detailed and shows off the detailed 3D texture of the fog.&#34;&gt;&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s how it looked on my desktop:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bhf.png&#34; alt=&#34;A screenshot of some volumetric fog rendered with my shader on my Desktop with an AMD GPU.  There is a green light reflecting off the surface of the fog.  The lighting looks inaccurate and low resolution, not matching the 3D texture of the fog well.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I tried changing a few things to make the cases as identical as possible.  Macbooks have a High DPI screen with a pixel ratio of 2, and I thought maybe that was maybe causing the change in behavior.  However, when I disabled High DPI rendering, the results on the mac looked pretty much the same - so that wasn&amp;rsquo;t it.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;To generate the fog, I use a series of composed noise functions.  Each subsequent layer, or octave, is rendered at a finer scale and adds more detail to the fog.  The noise function I used (&lt;a href=&#34;https://github.com/stegu/psrdnoise&#34;&gt;&lt;code&gt;psrdnoise&lt;/code&gt;&lt;/a&gt;) has built-in support for cheaply computing the gradient/derivative at each sampled point.  I make use of this functionality for my lighting computations.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an excerpt from the code where I accumulate that gradient:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-c++&#34; data-lang=&#34;c++&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sampleFogDensityLOD&lt;/span&gt;(vec3 worldPos, out vec3 gradient, &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; lod) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; weight &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; LODWeights[lod];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; scale &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; LODScales[lod];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  vec2 xzGradient;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; noise &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; psrdnoise(worldPos.xz &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; scale, vec2(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;, xzGradient);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  gradient &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; vec3(xzGradient.x, &lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;, xzGradient.y) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weight;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; noise &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; weight;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sampleFogDensity&lt;/span&gt;(vec3 worldPos, out vec3 gradient) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  gradient &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; vec3(&lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; density &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0.&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;float&lt;/span&gt; lod &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;; lod &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;; lod &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    density &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; sampleFogDensityLOD(worldPos, gradient, lod);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; density;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It turns out that there&amp;rsquo;s an issue in this code.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;gradient&lt;/code&gt; parameter is specified as &lt;code&gt;out vec3 gradient&lt;/code&gt;, but the &lt;code&gt;sampleFogDensityLOD&lt;/code&gt; function &lt;em&gt;reads and writes&lt;/em&gt; its value.  In GLSL, arguments marked &lt;code&gt;out&lt;/code&gt; have their value uninitialized by default.&lt;/p&gt;
&lt;p&gt;The GLSL compiler or something else in the graphics driver stack on Mac was being lenient and preserving the value.  The AMD compiler on the other hand was setting &lt;code&gt;gradient&lt;/code&gt; to all zeroes in &lt;code&gt;sampleFogDensityLOD&lt;/code&gt;, or something like that, and causing only the lowest level of detail&amp;rsquo;s gradient to be sampled.&lt;/p&gt;
&lt;p&gt;This is an instance of undefined behavior.  The initial value of an &lt;code&gt;out&lt;/code&gt; argument is undefined, so the compiler is free to do whatever it wants when optimizing the code.&lt;/p&gt;
&lt;h2 id=&#34;the-solution&#34;&gt;The Solution&lt;/h2&gt;
&lt;p&gt;The fix was simple: just change &lt;code&gt;out gradient&lt;/code&gt; to &lt;code&gt;inout gradient&lt;/code&gt;.  That way, the value passed in is preserved and the gradient is accumulated properly.&lt;/p&gt;
&lt;p&gt;This is something I&amp;rsquo;ll certainly watch out for when writing shaders in GLSL in the future.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing &#34;Could Not Load Openssl&#34; Mastodon Build Error</title>
      <link>https://cprimozic.net/notes/posts/fixing-mastodon-build-error-could-not-load-openssl/</link>
      <pubDate>Fri, 22 Sep 2023 11:15:03 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-mastodon-build-error-could-not-load-openssl/</guid>
      <description>&lt;p&gt;While trying to update &lt;a href=&#34;https://mastodon.ameo.dev/&#34;&gt;my Mastodon server&lt;/a&gt; to the latest v4.2.0 release, I kept running into build failures when running &lt;code&gt;docker-compose&lt;/code&gt; build.  I got errors like this in the logs:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#20 27.04 Bundler 2.4.10 is running, but your lockfile was generated with 2.4.13. Installing Bundler 2.4.13 and restarting using that version.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#20 27.21 There was an error installing the locked bundler version (2.4.13), rerun with the `--verbose` flag for more details. Going on using bundler 2.4.10.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#20 27.29 Could not load OpenSSL.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#20 27.29 You must recompile Ruby with OpenSSL support.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It was very confusing since the official CI builds seemed to be working fine.  Nevertheless, there were &lt;a href=&#34;https://github.com/mastodon/mastodon/issues/26888&#34;&gt;multiple&lt;/a&gt; &lt;a href=&#34;https://github.com/techulus/push-github-action/issues/8&#34;&gt;issues&lt;/a&gt; opened about this, so it&amp;rsquo;s something people were running into.&lt;/p&gt;</description>
      <content>&lt;p&gt;While trying to update &lt;a href=&#34;https://mastodon.ameo.dev/&#34;&gt;my Mastodon server&lt;/a&gt; to the latest v4.2.0 release, I kept running into build failures when running &lt;code&gt;docker-compose&lt;/code&gt; build.  I got errors like this in the logs:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#20 27.04 Bundler 2.4.10 is running, but your lockfile was generated with 2.4.13. Installing Bundler 2.4.13 and restarting using that version.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#20 27.21 There was an error installing the locked bundler version (2.4.13), rerun with the `--verbose` flag for more details. Going on using bundler 2.4.10.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#20 27.29 Could not load OpenSSL.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#20 27.29 You must recompile Ruby with OpenSSL support.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It was very confusing since the official CI builds seemed to be working fine.  Nevertheless, there were &lt;a href=&#34;https://github.com/mastodon/mastodon/issues/26888&#34;&gt;multiple&lt;/a&gt; &lt;a href=&#34;https://github.com/techulus/push-github-action/issues/8&#34;&gt;issues&lt;/a&gt; opened about this, so it&amp;rsquo;s something people were running into.&lt;/p&gt;
&lt;p&gt;I bisected this build issue to commit &lt;a href=&#34;https://github.com/mastodon/mastodon/commit/b749de766f5a6158fd0b5f3c3201943083fc7979&#34;&gt;https://github.com/mastodon/mastodon/commit/b749de766f5a6158fd0b5f3c3201943083fc7979&lt;/a&gt; which bumps the tag of the Docker &lt;code&gt;node&lt;/code&gt; image from Debian bullseye to bookworm.&lt;/p&gt;
&lt;p&gt;I dug into it further.  I tried to repro the build failure on my desktop, but it works there; it only fails when building on the server on which I run my Mastodon instance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After more investigation, I found that the hashes of the &lt;code&gt;ruby-jemalloc&lt;/code&gt; image used by the build process are different between the server and my local desktop&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Server:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[
    {
        &amp;#34;Id&amp;#34;: &amp;#34;sha256:c9d180a360e5d39e463cbb202eb10bc4e6d44af3f6ba53fbd26874027aea9ec9&amp;#34;,
        &amp;#34;RepoTags&amp;#34;: [
            &amp;#34;ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim&amp;#34;
        ],
        &amp;#34;RepoDigests&amp;#34;: [
            &amp;#34;ghcr.io/moritzheiber/ruby-jemalloc@sha256:9f452d67da4ffdc9abc103015ca447f442d324946a78cefcea9d35f558ce9a93&amp;#34;
        ],
        &amp;#34;Parent&amp;#34;: &amp;#34;&amp;#34;,
        &amp;#34;Comment&amp;#34;: &amp;#34;buildkit.dockerfile.v0&amp;#34;,
        &amp;#34;Created&amp;#34;: &amp;#34;2023-06-05T07:47:27.743594335Z&amp;#34;,
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Desktop:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[
    {
        &amp;#34;Id&amp;#34;: &amp;#34;sha256:6bebe65ca9cf60a9d55145104943af2493c46a593c36d9ada11f0940db7a280e&amp;#34;,
        &amp;#34;RepoTags&amp;#34;: [
            &amp;#34;ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim&amp;#34;
        ],
        &amp;#34;RepoDigests&amp;#34;: [
            &amp;#34;ghcr.io/moritzheiber/ruby-jemalloc@sha256:9a1487af836fada451d706b4b9b4f17a42e82cab5bcef1d77cb577134473f9a6&amp;#34;
        ],
        &amp;#34;Parent&amp;#34;: &amp;#34;&amp;#34;,
        &amp;#34;Comment&amp;#34;: &amp;#34;buildkit.dockerfile.v0&amp;#34;,
        &amp;#34;Created&amp;#34;: &amp;#34;2023-09-18T09:01:47.627305986Z&amp;#34;,
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;My guess is that whoever maintains that &lt;code&gt;ruby-jemalloc&lt;/code&gt; image force-pushed a new version of the package to the same tag.  My server has the old version cached, and my desktop picked up the new one.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I ran &lt;code&gt;docker image rm ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim&lt;/code&gt; on the server.  This cleared out that old stale &lt;code&gt;ruby-jemalloc&lt;/code&gt; image, re-fetched the new one, and fixed my build.&lt;/p&gt;
&lt;p&gt;So yeah - there&amp;rsquo;s a workaround for that @NahNick and @CSDUMMI.  The issue isn&amp;rsquo;t technically on mastodon&amp;rsquo;s side, and it should go away the next time they upgrade their ruby version (as long as the &lt;code&gt;ruby-jemalloc&lt;/code&gt; maintainer doesn&amp;rsquo;t do this again in the future).&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing pmndrs Postprocessing Recursive Depth Texture Binding</title>
      <link>https://cprimozic.net/notes/posts/fixing-pmndrs-postprocessing-recursive-depth-texture-binding/</link>
      <pubDate>Tue, 29 Aug 2023 22:55:42 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-pmndrs-postprocessing-recursive-depth-texture-binding/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been working on building &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;3D scenes and environments&lt;/a&gt; in the browser using Three.JS.  As part of those, I make pretty heavy use of the &lt;a href=&#34;https://github.com/pmndrs/postprocessing&#34;&gt;pmndrs &lt;code&gt;postprocessing&lt;/code&gt; library&lt;/a&gt; for post-processing and effects.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also implemented a custom godrays effect that works with &lt;code&gt;postprocesing&lt;/code&gt; called &lt;a href=&#34;https://github.com/ameobea/three-good-godrays&#34;&gt;&lt;code&gt;three-good-godrays&lt;/code&gt;&lt;/a&gt;.  It creates a custom pass that is added to the postprocessing &lt;code&gt;EffectComposer&lt;/code&gt; which renders volumetric screen-space godrays by reading the depth buffer and shadow map for a light.&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;ve been working on building &lt;a href=&#34;https://github.com/ameobea/sketches-3d&#34;&gt;3D scenes and environments&lt;/a&gt; in the browser using Three.JS.  As part of those, I make pretty heavy use of the &lt;a href=&#34;https://github.com/pmndrs/postprocessing&#34;&gt;pmndrs &lt;code&gt;postprocessing&lt;/code&gt; library&lt;/a&gt; for post-processing and effects.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also implemented a custom godrays effect that works with &lt;code&gt;postprocesing&lt;/code&gt; called &lt;a href=&#34;https://github.com/ameobea/three-good-godrays&#34;&gt;&lt;code&gt;three-good-godrays&lt;/code&gt;&lt;/a&gt;.  It creates a custom pass that is added to the postprocessing &lt;code&gt;EffectComposer&lt;/code&gt; which renders volumetric screen-space godrays by reading the depth buffer and shadow map for a light.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;For one of my scenes, the postprocessing chain had gotten pretty long with several different effects in use.  At some point, I started seeing errors like this in the JS console:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[.WebGL-0x16c00334d00]GL ERROR :GL_INVALID_OPERATION : glDrawArrays: Source and destination textures of the draw are the same.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In addition, my godrays effect stopped working.  I used the &lt;a href=&#34;https://spector.babylonjs.com/&#34;&gt;Spector.JS&lt;/a&gt; browser extension to debug the WebGL rendering sequence for one of my frames and I saw that the shaders were getting launched, but nothing seemed to be getting rendered into the destination buffers.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;As I mentioned previously, the shaders used internally by &lt;code&gt;three-good-godrays&lt;/code&gt; and some of the other effects in my pipeline needed access to scene depth information.  The &lt;code&gt;Pass&lt;/code&gt;es added to the &lt;code&gt;EffectComposer&lt;/code&gt; have a &lt;a href=&#34;https://github.com/pmndrs/postprocessing/blob/a65812998ebb76de962c7e2ef510ed3baf090bb2/src/passes/Pass.js#L150&#34;&gt;&lt;code&gt;needsDepthTexture&lt;/code&gt;&lt;/a&gt; attribute that they can set to indicate that they need access to the depth buffer.  If set, the &lt;code&gt;EffectComposer&lt;/code&gt; will call their &lt;code&gt;setDepthTexture()&lt;/code&gt; method and provide them a texture containing it.&lt;/p&gt;
&lt;p&gt;Internally, &lt;code&gt;EffectComposer&lt;/code&gt; has two buffers that it swaps back and forth between when rendering the passes.  I looked at the code and the logic looks roughly like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-py&#34; data-lang=&#34;py&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;input_buffer, output_buffer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; build_buffers()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; any_pass_needs_depth_texture:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  input_buffer&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;bind_depth_texture(build_depth_texture())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i, fx_pass &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; enumerate(passes):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  is_last &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; len(passes) &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  render_to_screen &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; is_last &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;render_to_screen
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# Passing None indicates that the pass should render to the canvas framebuffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# which puts its output directly onto the screen&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  fx_pass&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;render(input_buffer, &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; is_last &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; output_buffer)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# needs_swap defaults to true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;needs_swap &lt;span style=&#34;color:#f92672&#34;&gt;and&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; render_to_screen:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    output_buffer, input_buffer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; input_buffer, output_buffer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When the depth texture is first initialized, it gets set on the input buffer.  Since these buffers swap back and forth each frame, it&amp;rsquo;s possible that the depth buffer will be attached to the output buffer while processing the effect.  If the pass makes use of the depth texture as input for some shader by passing it as a uniform or similar, it will cause the WebGL error I pasted before and cause the pass to fail to render.&lt;/p&gt;
&lt;p&gt;This is a known bug/limitation of &lt;code&gt;postprocessing&lt;/code&gt;.  There are multiple issues about this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/pmndrs/postprocessing/issues/225&#34;&gt;https://github.com/pmndrs/postprocessing/issues/225&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/pmndrs/postprocessing/issues/416&#34;&gt;https://github.com/pmndrs/postprocessing/issues/416&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They plan to address it in a release of version 7, which is not yet out at the time of writing this.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;Luckily, it&amp;rsquo;s possible to work around this issue.  I had to update &lt;code&gt;three-good-godrays&lt;/code&gt; to detect and handle case where the provided depth texture is the same as the one bound to the provided output buffer.&lt;/p&gt;
&lt;p&gt;If it is the same, then I allocate an additional framebuffer the same size of the depth texture, run a &lt;code&gt;CopyPass&lt;/code&gt; to copy the contents of the depth texture into it, and then bind that copied buffer as the input for the &lt;code&gt;sceneDepth&lt;/code&gt; uniform of the shader instead.  This fixes the &amp;ldquo;Source and destination texture sof the draw are the same&amp;rdquo; error and allows the pass to render without issue.&lt;/p&gt;
&lt;p&gt;I made that change in &lt;a href=&#34;https://github.com/Ameobea/three-good-godrays/commit/294cc263697e0e135270dd7a620d95ce6d547dc2&#34;&gt;this commit&lt;/a&gt;.  I do hope that pmndrs &lt;code&gt;postprocessing&lt;/code&gt; gets that v7 rework; it was very hard to figure out what was causing this and debugging it took several hours.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing &#34;performance is not defined&#34; Issue with Svelte/SvelteKit</title>
      <link>https://cprimozic.net/notes/posts/fixing-svelte-performance-is-not-defined/</link>
      <pubDate>Fri, 25 Aug 2023 00:32:30 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-svelte-performance-is-not-defined/</guid>
      <description>&lt;h2 id=&#34;the-issue&#34;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;When building my SvelteKit project, I get lots of errors like this in my console:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ERROR in ./src/graphEditor/nodes/CustomAudio/MIDIToFrequency/MIDIToFrequencySmallView.svelte
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Module build failed (from ./node_modules/svelte-loader/index.js):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ReferenceError: performance is not defined
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at now (/home/casey/web-synth/node_modules/svelte/compiler.cjs:7:31)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at new Stats (/home/casey/web-synth/node_modules/svelte/compiler.cjs:47:21)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at compile (/home/casey/web-synth/node_modules/svelte/compiler.cjs:45535:16)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at injectVarsToCode (/home/casey/web-synth/node_modules/svelte-preprocess/dist/transformers/typescript.js:85:45)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at mixedImportsTranspiler (/home/casey/web-synth/node_modules/svelte-preprocess/dist/transformers/typescript.js:257:26)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at transformer (/home/casey/web-synth/node_modules/svelte-preprocess/dist/transformers/typescript.js:336:11)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at transform (/home/casey/web-synth/node_modules/svelte-preprocess/dist/autoProcess.js:46:12)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async /home/casey/web-synth/node_modules/svelte-preprocess/dist/autoProcess.js:117:29
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async script (/home/casey/web-synth/node_modules/svelte-preprocess/dist/autoProcess.js:147:33)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async process_single_tag (/home/casey/web-synth/node_modules/svelte/compiler.cjs:45984:21)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async Promise.all (index 0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async replace_in_code (/home/casey/web-synth/node_modules/svelte/compiler.cjs:45708:23)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async process_tag (/home/casey/web-synth/node_modules/svelte/compiler.cjs:46001:26)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async preprocess (/home/casey/web-synth/node_modules/svelte/compiler.cjs:46059:25)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;They&amp;rsquo;re originating from within the Svelte compiler.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-issue&#34;&gt;The Issue&lt;/h2&gt;
&lt;p&gt;When building my SvelteKit project, I get lots of errors like this in my console:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ERROR in ./src/graphEditor/nodes/CustomAudio/MIDIToFrequency/MIDIToFrequencySmallView.svelte
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Module build failed (from ./node_modules/svelte-loader/index.js):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ReferenceError: performance is not defined
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at now (/home/casey/web-synth/node_modules/svelte/compiler.cjs:7:31)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at new Stats (/home/casey/web-synth/node_modules/svelte/compiler.cjs:47:21)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at compile (/home/casey/web-synth/node_modules/svelte/compiler.cjs:45535:16)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at injectVarsToCode (/home/casey/web-synth/node_modules/svelte-preprocess/dist/transformers/typescript.js:85:45)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at mixedImportsTranspiler (/home/casey/web-synth/node_modules/svelte-preprocess/dist/transformers/typescript.js:257:26)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at transformer (/home/casey/web-synth/node_modules/svelte-preprocess/dist/transformers/typescript.js:336:11)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at transform (/home/casey/web-synth/node_modules/svelte-preprocess/dist/autoProcess.js:46:12)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async /home/casey/web-synth/node_modules/svelte-preprocess/dist/autoProcess.js:117:29
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async script (/home/casey/web-synth/node_modules/svelte-preprocess/dist/autoProcess.js:147:33)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async process_single_tag (/home/casey/web-synth/node_modules/svelte/compiler.cjs:45984:21)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async Promise.all (index 0)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async replace_in_code (/home/casey/web-synth/node_modules/svelte/compiler.cjs:45708:23)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async process_tag (/home/casey/web-synth/node_modules/svelte/compiler.cjs:46001:26)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    at async preprocess (/home/casey/web-synth/node_modules/svelte/compiler.cjs:46059:25)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;They&amp;rsquo;re originating from within the Svelte compiler.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;Svelte/SvelteKit &lt;a href=&#34;https://github.com/sveltejs/kit/pull/4922&#34;&gt;dropped support&lt;/a&gt; for NodeJS v14.  I still had this old version going locally which caused the build to fail because it was trying access stuff that wasn&amp;rsquo;t available in that version.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I upgraded to NodeJS v18 which is much newer and works fully with Svelte/SvelteKit:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nvm use 18.9.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You might have to use a different method to do this if you&amp;rsquo;ve got Node installed differently on your system, but yeah after doing that the build went through fine for me.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing &#34;can&#39;t resolve &#39;svelte/internal&#39;&#34; Error After Upgrading `svelte`</title>
      <link>https://cprimozic.net/notes/posts/fixing-cant-resolve-svelte-internal-disclose-version-error/</link>
      <pubDate>Thu, 24 Aug 2023 17:00:08 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/fixing-cant-resolve-svelte-internal-disclose-version-error/</guid>
      <description>&lt;p&gt;I bumped several of the dependencies for one of my projects.  It uses Svelte and Webpack, and makes use of &lt;code&gt;svelte-loader&lt;/code&gt; to facilitate importing &lt;code&gt;.svelte&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;I upgraded Svelte from v3.57.0 to v4.2.0, and bumped &lt;code&gt;svelte-loader&lt;/code&gt;, &lt;code&gt;svelte-preprocess&lt;/code&gt;, &lt;code&gt;prettier-plugin-svelte&lt;/code&gt;, and many other libraries to their latest versions as well.&lt;/p&gt;
&lt;p&gt;After the upgrade, my Webpack dev server started up but failed to load with many errors like these displayed in the console:&lt;/p&gt;</description>
      <content>&lt;p&gt;I bumped several of the dependencies for one of my projects.  It uses Svelte and Webpack, and makes use of &lt;code&gt;svelte-loader&lt;/code&gt; to facilitate importing &lt;code&gt;.svelte&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;I upgraded Svelte from v3.57.0 to v4.2.0, and bumped &lt;code&gt;svelte-loader&lt;/code&gt;, &lt;code&gt;svelte-preprocess&lt;/code&gt;, &lt;code&gt;prettier-plugin-svelte&lt;/code&gt;, and many other libraries to their latest versions as well.&lt;/p&gt;
&lt;p&gt;After the upgrade, my Webpack dev server started up but failed to load with many errors like these displayed in the console:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ERROR in ./src/welcomePage/DemoTile.svelte 2:0-28:25
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Module not found: Error: Can&amp;#39;t resolve &amp;#39;svelte/internal&amp;#39; in &amp;#39;/home/casey/web-synth/src/welcomePage&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/welcomePage/WelcomePage.svelte 27:0-55 118:17-25 132:17-25 146:17-25 160:17-25 495:34-42
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/welcomePage/WelcomePage.ts 3:0-49 14:10-23
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/engine_bg.js 17:0-124 639:2-19 643:2-22 647:2-19 651:2-21
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/engine.js 2:0-31 2:0-31
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/index.tsx 14:13-31
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ERROR in ./src/welcomePage/DemoTile.svelte 30:0-42
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Module not found: Error: Can&amp;#39;t resolve &amp;#39;svelte/internal/disclose-version&amp;#39; in &amp;#39;/home/casey/web-synth/src/welcomePage&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/welcomePage/WelcomePage.svelte 27:0-55 118:17-25 132:17-25 146:17-25 160:17-25 495:34-42
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/welcomePage/WelcomePage.ts 3:0-49 14:10-23
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/engine_bg.js 17:0-124 639:2-19 643:2-22 647:2-19 651:2-21
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/engine.js 2:0-31 2:0-31
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/index.tsx 14:13-31
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ERROR in ./src/welcomePage/WelcomePage.svelte 2:0-24:25
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Module not found: Error: Can&amp;#39;t resolve &amp;#39;svelte/internal&amp;#39; in &amp;#39;/home/casey/web-synth/src/welcomePage&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/welcomePage/WelcomePage.ts 3:0-49 14:10-23
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/engine_bg.js 17:0-124 639:2-19 643:2-22 647:2-19 651:2-21
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/engine.js 2:0-31 2:0-31
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/index.tsx 14:13-31
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ERROR in ./src/welcomePage/WelcomePage.svelte 26:0-42
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Module not found: Error: Can&amp;#39;t resolve &amp;#39;svelte/internal/disclose-version&amp;#39; in &amp;#39;/home/casey/web-synth/src/welcomePage&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/welcomePage/WelcomePage.ts 3:0-49 14:10-23
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/engine_bg.js 17:0-124 639:2-19 643:2-22 647:2-19 651:2-21
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/engine.js 2:0-31 2:0-31
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; @ ./src/index.tsx 14:13-31
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I found the fix in a &lt;a href=&#34;https://github.com/sveltejs/svelte-loader/issues/234#issuecomment-1607058996&#34;&gt;Github issue thread&lt;/a&gt; after extensive searching.&lt;/p&gt;
&lt;p&gt;In my Webpack config file &lt;code&gt;webpack.config.js&lt;/code&gt;, I had this code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;alias&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;svelte&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;path&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;dirname&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;require&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;svelte/package.json&amp;#39;&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I don&amp;rsquo;t remember why I put it there originally; probably copy-pasted from a README of template.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In any case, this code is broken after updating from Svelte 3-&amp;gt;4 when using Webpack and needs to be updated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here was the diff I applied:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;diff --git b/webpack.base.js a/webpack.base.js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;index e73c6d2..da52655 100644
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;--- b/webpack.base.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+++ a/webpack.base.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;@@ -74,7 +74,7 @@ const config = {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     extensions: [&amp;#39;.ts&amp;#39;, &amp;#39;.tsx&amp;#39;, &amp;#39;.js&amp;#39;, &amp;#39;.jsx&amp;#39;, &amp;#39;.wasm&amp;#39;, &amp;#39;.svelte&amp;#39;, &amp;#39;.mjs&amp;#39;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     modules: [path.resolve(&amp;#39;./node_modules&amp;#39;), path.resolve(&amp;#39;.&amp;#39;)],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     alias: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-      svelte: path.dirname(require.resolve(&amp;#39;svelte/package.json&amp;#39;)),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+      svelte: path.resolve(&amp;#39;node_modules&amp;#39;, &amp;#39;svelte/src/runtime&amp;#39;),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     conditionNames: [&amp;#39;require&amp;#39;, &amp;#39;node&amp;#39;, &amp;#39;svelte&amp;#39;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   },
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After re-starting the webpack dev server, everything works just fine!&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>`amdgpu_top`: A Modern `radeontop` Alternative</title>
      <link>https://cprimozic.net/notes/posts/amdgpu_top-a-modern-radeontop-alternative/</link>
      <pubDate>Wed, 23 Aug 2023 21:57:55 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/amdgpu_top-a-modern-radeontop-alternative/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been using a tool called &lt;a href=&#34;https://github.com/clbr/radeontop&#34;&gt;&lt;code&gt;radeontop&lt;/code&gt;&lt;/a&gt; for years to monitor the performance and utilization of my AMD GPUs on Linux.  It&amp;rsquo;s a TUI-based application that renders the value of different performance counters as bar charts:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/beq.png&#34; alt=&#34;A screenshot of the radeontop application.  Shows several different bars rendered in a terminal in various colors with labels like Graphics pipe, Event Engine, Scan Converter, Clip Rectangle, and others.&#34;&gt;&lt;/p&gt;
&lt;p&gt;For the most part, it does a good job and it provides a concise overview of GPU utilization.&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;ve been using a tool called &lt;a href=&#34;https://github.com/clbr/radeontop&#34;&gt;&lt;code&gt;radeontop&lt;/code&gt;&lt;/a&gt; for years to monitor the performance and utilization of my AMD GPUs on Linux.  It&amp;rsquo;s a TUI-based application that renders the value of different performance counters as bar charts:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/beq.png&#34; alt=&#34;A screenshot of the radeontop application.  Shows several different bars rendered in a terminal in various colors with labels like Graphics pipe, Event Engine, Scan Converter, Clip Rectangle, and others.&#34;&gt;&lt;/p&gt;
&lt;p&gt;For the most part, it does a good job and it provides a concise overview of GPU utilization.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;However, it seems that &lt;code&gt;radeontop&lt;/code&gt; is no longer actively developed/updated&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&amp;rsquo;s received ~7 commits in the past ~3 years and although it does still mostly work even with the latest GPUs like the 7900 XTX, but it&amp;rsquo;s not under active development.&lt;/p&gt;
&lt;h2 id=&#34;enter-amdgpu_top&#34;&gt;Enter &lt;code&gt;amdgpu_top&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Today, I discovered an alternative tool that does all of the same things as &lt;code&gt;radeontop&lt;/code&gt; and more: &lt;a href=&#34;https://github.com/Umio-Yasuno/amdgpu_top&#34;&gt;&lt;code&gt;amdgpu_top&lt;/code&gt;&lt;/a&gt;.  It&amp;rsquo;s actively developed with commits in the past couple of weeks at the time of writing this.&lt;/p&gt;
&lt;p&gt;It is written in Rust, and so was extremely easy to install:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;cargo install amdgpu_top
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&amp;rsquo;s what its UI looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/ber.png&#34; alt=&#34;A screenshot of the amdgpu_top TUI.  Shows several bars tracking performance counters with names like Graphics Pipe, Shader Processor Interpolator, etc.  It also has a list of processes and information about them including the amount of VRAM they’re using and GPU utilization percents for graphics, compute, DMA, and encoding/decoding.  At the bottom, there is a sensors section with information about the clock rate of various GPU clocks, GPU temperature, and power.&#34;&gt;&lt;/p&gt;
&lt;p&gt;As you can see, there&amp;rsquo;s a ton more data available!  One immediate benefit is the process list which shows which processes are consuming what GPU resources.  This is extremely useful and something I had no ability to see with &lt;code&gt;radeontop&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The sensors view is also extremely useful.  &lt;code&gt;radeontop&lt;/code&gt; had no way to track GPU temperature, but here &lt;code&gt;amdgpu_top&lt;/code&gt; provides that along with block rates, power metrics, and more.&lt;/p&gt;
&lt;p&gt;For the displayed performance counters, I think they&amp;rsquo;re using the same underlying data with some differences in naming and grouping.  I think that &lt;code&gt;radeontop&lt;/code&gt; is slightly more granular with the performance counters, but I honestly have no idea what half of those counters even track so having a bit of a coarser view is actually beneficial to me.&lt;/p&gt;
&lt;h3 id=&#34;gui-view&#34;&gt;GUI View&lt;/h3&gt;
&lt;p&gt;Another big upgrade that &lt;code&gt;amdgpu_top&lt;/code&gt; provides is a fully-fledged GUI application as an alternative to their default TUI view.  It can be accessed by launching &lt;code&gt;amdgpu_top --gui&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bep.png&#34; alt=&#34;A screenshot of the amdgpu_top GUI.  It shows a detailed set of metrics tracking GPU utilization and other statistics.  There are utilization percentage displays and line charts plotting the history of all the recorded metrics with names like Graphics Pipe, Shader Processor Interpolator, etc.  It also has a list of processes and information about them including the amount of VRAM they’re using and GPU utilization percents for graphics, compute, DMA, and encoding/decoding.&#34;&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s quite dense, but that&amp;rsquo;s because it&amp;rsquo;s packed with so much information.  Having the ability to track GPU utilization over time is a hugely useful feature, and as such I&amp;rsquo;ll probably end up using the GUI mode most often when I use &lt;code&gt;amdgpu_top&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;So yeah, personally I see no way at all in which &lt;code&gt;radeontop&lt;/code&gt; wins over &lt;code&gt;amdgpu_top&lt;/code&gt;.  &lt;code&gt;amdgpu_top&lt;/code&gt; seems like a clear upgrade in every way, and I&amp;rsquo;ll not be looking back personally.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll admit that as an avid Rust programmer, I&amp;rsquo;m slightly biased since &lt;code&gt;amdgpu_top&lt;/code&gt; is written in Rust and &lt;code&gt;radeontop&lt;/code&gt; is in C, but even besides that it&amp;rsquo;s just a better, more feature-rich, and modern tool.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in tracking performance for your AMD GPU, give &lt;code&gt;amdgpu_top&lt;/code&gt; a try for sure!&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Generating 4K PBR Textures Using Stable Diffusion XL</title>
      <link>https://cprimozic.net/notes/posts/generating-textures-for-3d-using-stable-diffusion/</link>
      <pubDate>Sat, 19 Aug 2023 17:05:24 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/generating-textures-for-3d-using-stable-diffusion/</guid>
      <description>&lt;p&gt;I&amp;rsquo;m picking back up the work that I started last year building &lt;a href=&#34;https://github.com/ameobea/sketches-3d/&#34;&gt;3D scenes and sketches&lt;/a&gt; with Three.JS.&lt;/p&gt;
&lt;p&gt;At that time, it was just after AI image generators like DALL-E and Stable Diffusion were really taking off.  I had success running Stable Diffusion locally and using it to generate textures for terrain, buildings, and other environments in the 3D worlds I was building.&lt;/p&gt;
&lt;p&gt;I was using Stable Diffusion v1 back then.  I had found some prompts that I liked which created images with styles kind of like this: &lt;img src=&#34;https://pub-80300747d44d418ca912329092f69f65.r2.dev/imgen/2593919833_ridged_niobium_wall__relic_of_an_ancient_alien_civilization__long_sulfur_trenches_with_deep_symmetrical_geometric_patterned_corroded__erosion_exposes_intricate_black_crystalline_neodymium_computer_circuitry.png&#34; alt=&#34;Texture generated with stable diffusion v1.  Shows a mostly black and white image with outset spiraly pieces.  Looks kind of organic but also like rock, and has vibes of an electron microscope image.  The prompt used to generate it was “ridged niobium wall  relic of an ancient alien civilization  long sulfur trenches with deep symmetrical geometric patterned corroded  erosion exposes intricate black crystalline neodymium computer circuitry”&#34;&gt;&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;m picking back up the work that I started last year building &lt;a href=&#34;https://github.com/ameobea/sketches-3d/&#34;&gt;3D scenes and sketches&lt;/a&gt; with Three.JS.&lt;/p&gt;
&lt;p&gt;At that time, it was just after AI image generators like DALL-E and Stable Diffusion were really taking off.  I had success running Stable Diffusion locally and using it to generate textures for terrain, buildings, and other environments in the 3D worlds I was building.&lt;/p&gt;
&lt;p&gt;I was using Stable Diffusion v1 back then.  I had found some prompts that I liked which created images with styles kind of like this: &lt;img src=&#34;https://pub-80300747d44d418ca912329092f69f65.r2.dev/imgen/2593919833_ridged_niobium_wall__relic_of_an_ancient_alien_civilization__long_sulfur_trenches_with_deep_symmetrical_geometric_patterned_corroded__erosion_exposes_intricate_black_crystalline_neodymium_computer_circuitry.png&#34; alt=&#34;Texture generated with stable diffusion v1.  Shows a mostly black and white image with outset spiraly pieces.  Looks kind of organic but also like rock, and has vibes of an electron microscope image.  The prompt used to generate it was “ridged niobium wall  relic of an ancient alien civilization  long sulfur trenches with deep symmetrical geometric patterned corroded  erosion exposes intricate black crystalline neodymium computer circuitry”&#34;&gt;&lt;/p&gt;
&lt;p&gt;SDv1 was very good at generating textures like this, and I generated hundreds/thousands of images while playing around with different prompts.&lt;/p&gt;
&lt;p&gt;It is now August 2023, and the AI image generation ecosystem has continued to improve by leaps and bounds.  I&amp;rsquo;ve been trying out Stable Diffusion XL for texture generation, starting out with mostly terrain, and I&amp;rsquo;ve been getting some great results.&lt;/p&gt;
&lt;p&gt;At first, I was getting some discouraging results since the prompts I was using before weren&amp;rsquo;t working very well.  However, after some experimentation, I&amp;rsquo;ve been able to get results that are way better than anything I&amp;rsquo;d accomplished in the past.&lt;/p&gt;
&lt;h2 id=&#34;stable-diffusion-xl-v10-pbr-texture-generation-workflow&#34;&gt;Stable Diffusion XL v1.0 PBR Texture Generation Workflow&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve come up with a pretty solid workflow for generating high-quality 4K PBR textures using Stable Diffusion XL and some other tools.  I&amp;rsquo;m extremely impressed with the results so far, and I&amp;rsquo;ve only been playing around with this stuff for a few days now.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the kind of results I&amp;rsquo;m able to get:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bdm.jpg&#34; alt=&#34;4K texture generated with stable diffusion XL.  Generated with the prompt “top-down image of rough solid flat dark, rich slate rock. interspersed with bright ((flecks)) of ((glinting)) metallic spots like mica. high quality photograph, detailed, realistic”&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;installing--running-stable-diffusion-xl-with-7900-xtx&#34;&gt;Installing + Running Stable Diffusion XL with 7900 XTX&lt;/h3&gt;
&lt;p&gt;I recently bought a 7900 XTX graphics card.  It has 24GB of VRAM which is enough to generate 1024x1024 images with Stable Diffusion with no upscaling or other tricks required.&lt;/p&gt;
&lt;p&gt;I chose the &lt;a href=&#34;https://github.com/AUTOMATIC1111/stable-diffusion-webui&#34;&gt;AUTOMATIC1111 WebUI&lt;/a&gt; for installing and running Stable Diffusion.  It seems to be the most feature-rich and popular, and it supports AMD GPUs out of the box.  I cloned the repository, ran the &lt;code&gt;./run_webui.sh&lt;/code&gt; script, and it handled automatically installing dependencies and downloading Stable Diffusion weights and other model files.&lt;/p&gt;
&lt;p&gt;To run it, I needed to do a few manual things.  I got a segfault on my first run but was able to work around this by setting two environment variables:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;export CL_EXCLUDE&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;gfx1036
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;export HSA_OVERRIDE_GFX_VERSION&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;11.0.0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m currently using ROCm v5.5 which I believe is why this is required.  That version adds support for AMD 7000-series GPUs, but the support isn&amp;rsquo;t quite complete.  These environment variables trick Pytorch and other libraries into thinking it&amp;rsquo;s some other model and making it work.&lt;/p&gt;
&lt;p&gt;I previously wrote &lt;a href=&#34;https://cprimozic.net/notes/posts/setting-up-tensorflow-with-rocm-on-7900-xtx/&#34;&gt;some notes&lt;/a&gt; about installing ROCm and TensorFlow with the 7900 XTX.  They might already be out of date, though.&lt;/p&gt;
&lt;p&gt;Anyway, after installing and exporting those environment variables, the web UI started up and images were generating!&lt;/p&gt;
&lt;h3 id=&#34;prompts&#34;&gt;Prompts&lt;/h3&gt;
&lt;p&gt;The first step of the process was finding some good prompts for texture generation.  This is of course the most creative part of the whole thing and has the biggest impact on your results.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have a ton of guidance here; it largely depends on what you&amp;rsquo;re trying to generate and the style you&amp;rsquo;re looking to get.  However, here&amp;rsquo;s an example of a prompt that I&amp;rsquo;ve had a lot of success with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;top-down image of rough solid flat dark, rich slate rock. interspersed with bright ((flecks)) of ((glinting)) metallic spots like mica. high quality photograph, detailed, realistic&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Negative Prompt: &lt;code&gt;blurry, cracks, crevice, round, smooth, distinct, pebble, shadows, coin, circle&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Note the parenthesis around some words.  That&amp;rsquo;s a feature of the AUTO1111 webui I&amp;rsquo;m using, and it tells the model to pay more attention to the parts of the prompt surrounded by them.  There are some other fancy prompt syntax tricks as well; tons of flexibility and options to explore.&lt;/p&gt;
&lt;h3 id=&#34;params&#34;&gt;Params&lt;/h3&gt;
&lt;p&gt;The next step is coming up with good parameters for the generation.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve found that Stable Diffusion XL is actually more sensitive to the chosen params than older versions.  On top of that, there are WAY more things to tweak than there was a year ago.  As a result, it did take a good while to come up with settings that resulted in images I was happy with.&lt;/p&gt;
&lt;p&gt;To help with this, I made extensive use of the &amp;ldquo;X/Y/Z Plot&amp;rdquo; feature of the AUTO1111 webui.  It&amp;rsquo;s located in the &amp;ldquo;Script&amp;rdquo; dropdown of the UI:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bdn.png&#34; alt=&#34;Screenshot of a section the Automatic1111 Stable Diffusion web UI.  There is a red arrow pointing to the “Script” dropdown at the bottom, and the “X/Y/Z Plot” option is selected in it.&#34;&gt;&lt;/p&gt;
&lt;p&gt;It generates grids of output images where each cell is generated with a different combination of parameters.  Here&amp;rsquo;s what the output looks like when using it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bdo.jpg&#34; alt=&#34;An X/Y grid generated using the Automatic111 stable diffusion web UI.  Shows a grid with cells comparing four different samplers (DMP&amp;#43;&amp;#43; 2M Karras, Euler, LMS, and Heun) and various CFG scale values.&#34;&gt;&lt;/p&gt;
&lt;p&gt;It was extremely useful for zeroing in on a good set of baseline params for my images.&lt;/p&gt;
&lt;p&gt;After a lot of experimentation, here&amp;rsquo;s the baseline params I now use to start with when generating new textures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sampling method: &lt;strong&gt;Euler&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Sampling steps: &lt;strong&gt;60&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Width/Height: &lt;strong&gt;1024x1024&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;CFG Scale: &lt;strong&gt;6.5&lt;/strong&gt; (I found that getting this right is especially important)&lt;/li&gt;
&lt;li&gt;Tiling: &lt;strong&gt;Enabled&lt;/strong&gt; (needed to generate textures that tile seamlessly; extremely important)&lt;/li&gt;
&lt;li&gt;Hires. fix: &lt;strong&gt;Disabled&lt;/strong&gt; (I had bad luck with everything I tried with this enabled)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You&amp;rsquo;ll probably have to experiment yourself to find params that work well for your use-case, but these might work as a good baseline when getting started.&lt;/p&gt;
&lt;p&gt;One other thing I observed is that enabling tiling causes generated images to be pretty significantly different overall.  This makes it tricky when, say, generating images with the Stable Diffusion Dream Studio online to take advantage of their fast GPUs and then trying to replicate those results locally.  They don&amp;rsquo;t have the tiling option in their hosted UI and don&amp;rsquo;t allow tuning some params like sampler, so getting an exact match is difficult.&lt;/p&gt;
&lt;h3 id=&#34;converting-textures-from-1k-to-4k&#34;&gt;Converting Textures from 1K to 4K&lt;/h3&gt;
&lt;p&gt;Once I found a good prompt and good params, I just set it to generate a bunch images and let it go!  I let it run for around 2 hours and god a selection of ~50 images.  A lot are bad, but there are more than enough gems in the bunch to work with.&lt;/p&gt;
&lt;p&gt;One thing you may have noted is that I said &amp;ldquo;4K textures&amp;rdquo; in the title of this post but all the images I&amp;rsquo;ve generated so far are 1K.  Well if you thought I was going to say that I did something along the line of upscaling using one of the many possible methods, you&amp;rsquo;d be wrong actually!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I created a unique method for combining AI-generated textures together to generate higher-resolution outputs.  The resulting textures retain the seamless/infinitely tiling property of the source images.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I built a tool that runs in the browser to do this in an easy way.  You can use it here (it&amp;rsquo;s 100% free and &lt;a href=&#34;https://github.com/ameobea/texture-utils&#34;&gt;open source&lt;/a&gt;): &lt;a href=&#34;https://texture-utils.ameo.design/seamless-stitcher/&#34;&gt;https://texture-utils.ameo.design/seamless-stitcher/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The process is very simple.  You drag and drop 4 similar-looking seamless-tiling Stable Diffusion-generated textures and it combines them together.  As I mentioned, the output will also be seamless-tiling as well and go up in resolution by 4x.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what the UI for the tool looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/bdp.png&#34; alt=&#34;A screenshot of the seamless texture crossfade stitcher web UI.  Shows the generated stitched 4K texture on the left and a grid controlling the rotations, offsets, and selections of sub-images in the grid on the right.&#34;&gt;&lt;/p&gt;
&lt;p&gt;You can right-click the generated image and select &amp;ldquo;Save Image As&amp;rdquo; to download the result as a PNG.  The outputs by default are extremely large, so you may want to use a tool like &lt;a href=&#34;https://squoosh.app/&#34;&gt;squoosh&lt;/a&gt; to compress/optimize it.&lt;/p&gt;
&lt;p&gt;Compared to Stable Diffusion and other AI tools, the implementation is actually quite low-tech.  But I&amp;rsquo;ve found that the results are very good overall!&lt;/p&gt;
&lt;p&gt;It works best with images that look quite similar to each other.  If there are big differences in style, color, etc. it can be obvious in the resulting output and look bad.&lt;/p&gt;
&lt;p&gt;A great way to get very similar-looking source images is to find a generated image that you like and then generate variations of it.  To do this, take the generated image and upload it to the &amp;ldquo;PNG Info&amp;rdquo; tab of the AUTO1111 web UI.  Then, click the &amp;ldquo;send to txt2img&amp;rdquo; button and it will pre-populate the UI with all the params used to generate it.&lt;/p&gt;
&lt;p&gt;Then, tick the &amp;ldquo;Extra&amp;rdquo; checkbox on the txt2img tab.  Set &amp;ldquo;Variation strength&amp;rdquo; to something small like 0.1-0.3 and generate a dozen or so images.  They should look similar to the original overall but with different details.  If you choose 4 of those to upload to the seamless texture stitcher tool, I&amp;rsquo;ve found that the output usually looks good.&lt;/p&gt;
&lt;h3 id=&#34;building-pbr-textures&#34;&gt;Building PBR Textures&lt;/h3&gt;
&lt;p&gt;Once you have a stitched 4K image you&amp;rsquo;re happy with, it&amp;rsquo;s time to make them into full PBR textures.&lt;/p&gt;
&lt;p&gt;For this, I use a tool called &lt;a href=&#34;https://withpoly.com/textures/edit&#34;&gt;Poly&lt;/a&gt;.  They provide an AI-powered texture generator that takes an image as input and generates normal, height, ambient occlusion, metalness, and roughness maps for it.  They also provide their own prompt-based generation tools, but I personally prefer the control that generating it myself provides.&lt;/p&gt;
&lt;p&gt;Their tool lets you generate normal and height maps for free, but they charge you $20/month to generate others.  I do personally pay for that subscription right now, but you don&amp;rsquo;t have to in order to get good results.  The normal map is the most important part, can you can either set global roughness/metalness values for your texture (this is all you need a lot of the time) or write a custom shader to generate them on the fly from pixel values.&lt;/p&gt;
&lt;p&gt;Another method you can use to generate normal maps is &lt;a href=&#34;https://www.smart-page.net/smartnormal/&#34;&gt;SmartNormap&lt;/a&gt;.  It uses a non-AI approach to programmatically generate normal maps for any source image.  It has some params you can tweak, and the results are OK but generally less good than the AI-powered Poly tool overall in my experience.&lt;/p&gt;
&lt;p&gt;Anyway, yeah at that point you can download your maps and plug them into Blender, Three.JS, or whatever other 3D software you want.&lt;/p&gt;
&lt;p&gt;If you want to get even crazier with it, the seamless 4K output texture can be used with a hex tiling algorithm to make it tile infinitely with no repetition whatsoever.  I&amp;rsquo;m working on a Three.JS library to handle this automatically, and here&amp;rsquo;s what it looks like laid out on some terrain I generated in Blender:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/ccz.avif&#34; alt=&#34;Screenshot of some mountainous terrain generated with Blender and rendered in Three.JS.  The black and gold rocky texture I generated earlier is applied to the ground with the PBR maps included.  The texture is tiled using a hex tiling algorithm so it repeats infinitely without any obvious repetition.&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It really feels like the floodgates are opened here.  I personally find that using AI image generates for textures and other building-block assets rather than full images or artwork to be the way to go.  There&amp;rsquo;s a lot of room for my creativity and input to guide the entire process.  There are infinite possibilities to explore, and the whole process is quite fun in my opinion.&lt;/p&gt;
&lt;p&gt;Anyway, I hope you found some of this helpful, and I wish you good luck if you decide to try this out for yourself.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Building RNN Architecture Visualizations With TikZ</title>
      <link>https://cprimozic.net/notes/posts/building-rnn-architecture-visualizations-with-tikz/</link>
      <pubDate>Tue, 15 Aug 2023 00:42:47 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/building-rnn-architecture-visualizations-with-tikz/</guid>
      <description>&lt;p&gt;I recently finished &lt;a href=&#34;https://cprimozic.net/blog/growing-sparse-computational-graphs-with-rnns/&#34;&gt;a big blog post&lt;/a&gt; about growing sparse computational graphs with RNNs.&lt;/p&gt;
&lt;p&gt;An important part of that work involved creating a custom RNN architecture to facilitate the growth of extremely sparse networks.  To help explain that custom RNN architecture in the blog post, I created some visualizations that looked like this:&lt;/p&gt;
&lt;img src=&#34;https://i.ameo.link/bb2.svg&#34; style=&#34;width: 100%; filter: invert(0.9); background-color: #fff; margin-top: 30px; margin-bottom: 30px&#34; alt=&#34;TikZ-generated visualization of the custom RNN architecture I developed for this project.  Shows a single cell of the custom RNN.  Internally, there are nodes for things like kernels and biases, operations like dot product, add, custom activation function A, and nodes between them indicating the flow of data through the network.&#34; /&gt;
&lt;p&gt;These images are SVGs, so they scale infinitely without getting pixelated or blurry.  I looked into a few different options for generating these including tools like draw.io, manually drawing them in a vector editor like Inkscape, and Graphviz.&lt;/p&gt;</description>
      <content>&lt;p&gt;I recently finished &lt;a href=&#34;https://cprimozic.net/blog/growing-sparse-computational-graphs-with-rnns/&#34;&gt;a big blog post&lt;/a&gt; about growing sparse computational graphs with RNNs.&lt;/p&gt;
&lt;p&gt;An important part of that work involved creating a custom RNN architecture to facilitate the growth of extremely sparse networks.  To help explain that custom RNN architecture in the blog post, I created some visualizations that looked like this:&lt;/p&gt;
&lt;img src=&#34;https://i.ameo.link/bb2.svg&#34; style=&#34;width: 100%; filter: invert(0.9); background-color: #fff; margin-top: 30px; margin-bottom: 30px&#34; alt=&#34;TikZ-generated visualization of the custom RNN architecture I developed for this project.  Shows a single cell of the custom RNN.  Internally, there are nodes for things like kernels and biases, operations like dot product, add, custom activation function A, and nodes between them indicating the flow of data through the network.&#34; /&gt;
&lt;p&gt;These images are SVGs, so they scale infinitely without getting pixelated or blurry.  I looked into a few different options for generating these including tools like draw.io, manually drawing them in a vector editor like Inkscape, and Graphviz.&lt;/p&gt;
&lt;p&gt;It seems that most of the really good-looking RNN visualizations I&amp;rsquo;ve seen in popular blog posts were created by hand in Inkscape, but I really wanted to avoid doing that if I could.  I got some &lt;em&gt;decent&lt;/em&gt; results when I tried using Graphviz, but I struggled to get some of the fine-grained styling and layout controls that I wanted.&lt;/p&gt;
&lt;h2 id=&#34;tikz&#34;&gt;TikZ&lt;/h2&gt;
&lt;p&gt;The tool/language I ended up using to build my visualizations is called &lt;strong&gt;TikZ&lt;/strong&gt;.  &lt;strong&gt;TikZ&lt;/strong&gt; is a sort of programming language and toolkit for programmatically generating vector graphics.  It&amp;rsquo;s a sort of extension to LaTeX, so it&amp;rsquo;s commonly used for creating graphics and visualizations for research papers.&lt;/p&gt;
&lt;p&gt;To create a standalone SVG using TikZ, it starts by writing some LaTeX code in a .tex file.  Here&amp;rsquo;s the start of the code for the visualization above :&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-tex&#34; data-lang=&#34;tex&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\documentclass&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[tikz,border=3mm]&lt;/span&gt;{standalone}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\usetikzlibrary&lt;/span&gt;{positioning, shapes, fit}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\begin&lt;/span&gt;{document}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\begin&lt;/span&gt;{tikzpicture}[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    every node/.style={draw, thick},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    operation/.style={circle, inner sep=2pt},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    kernel/.style={rectangle, fill=gray!20},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    point/.style={coordinate},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    cell/.style={rectangle, draw, thick, dashed, inner sep=2mm, fit=#1}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This sets up the LaTeX document, imports TikZ + some other helper libraries, defines some global default styling for the graph.  &lt;code&gt;kernel&lt;/code&gt;, &lt;code&gt;point&lt;/code&gt;, &lt;code&gt;cell&lt;/code&gt;, etc. are sort of like CSS classes and can be used to apply styles to elements without having to copy-paste them for each one.&lt;/p&gt;
&lt;p&gt;For my purposes, everything else I needed to visualize was either a node or an edge.  When creating nodes in TikZ, you can specify their positions relative to other objects in the graphic like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-tex&#34; data-lang=&#34;tex&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\node&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[kernel]&lt;/span&gt; (c0_kernel) {Kernel};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\node&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[operation, below=5mm of c0_kernel]&lt;/span&gt; (c0_dot1) {·};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\node&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[operation, right=15mm of c0_dot1, scale=0.73]&lt;/span&gt; (c0_add1) {+};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\node&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[kernel, above=5mm of c0_add1]&lt;/span&gt; (c0_bias) {Bias};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\node&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[operation, right=8mm of c0_add1, fill=blue!20]&lt;/span&gt; (c0_activation1) {&lt;span style=&#34;color:#e6db74&#34;&gt;$&lt;/span&gt;A&lt;span style=&#34;color:#e6db74&#34;&gt;$&lt;/span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates 5 nodes and positions them all relative to each other, except the &lt;code&gt;c0_kernel&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Edges are created like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-tex&#34; data-lang=&#34;tex&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\draw&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[-&amp;gt;]&lt;/span&gt; (c0_recur_kernel) -- (c0_dot2) -- (c0_add2) -- (c0_activation2);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\draw&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[-&amp;gt;]&lt;/span&gt; (c0_recur_bias) -- (c0_add2);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\draw&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[-, dashed, draw=blue!90]&lt;/span&gt; (c0_new_state) -- (c0_new_state_bridge);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\draw&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[-&amp;gt;, dashed, draw=blue!90]&lt;/span&gt; (c0_new_state_bridge) -- (c0_state);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see, it&amp;rsquo;s possible to choose whether to use arrows or lines to join nodes and set styles on individual edges.  It&amp;rsquo;s also possible to define multiple edges on the same line, similar to Graphviz.&lt;/p&gt;
&lt;p&gt;However, for some of the edges my visualization, I wanted to manually control the routing of edges in order to make it look better.  There are a few different ways to accomplish this in TikZ, but the method I chose was to define &lt;code&gt;coordinate&lt;/code&gt;s.&lt;/p&gt;
&lt;p&gt;In TikZ, &lt;code&gt;coordinate&lt;/code&gt;s are invisible objects that are not displayed in any way but can instead be used as sources/destinations for edges.  So, to create an angled edge like the one from &lt;code&gt;New State&lt;/code&gt; to &lt;code&gt;State&lt;/code&gt; in the visualization, I did this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-tex&#34; data-lang=&#34;tex&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;% Define new state and state nodes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\node&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[kernel, left=3mm of c0_state_bridge]&lt;/span&gt; (c0_state) {State};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\node&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[kernel, below=5mm of c0_activation2]&lt;/span&gt; (c0_new_state) {New State};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;% Define coordinate at the bend of the edge
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\coordinate&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[left=49.4mm of c0_new_state]&lt;/span&gt; (c0_new_state_bridge) {};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;% Draw the edge between New State and State in two parts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\draw&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[-, dashed, draw=blue!90]&lt;/span&gt; (c0_new_state) -- (c0_new_state_bridge);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\draw&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[-&amp;gt;, dashed, draw=blue!90]&lt;/span&gt; (c0_new_state_bridge) -- (c0_state);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The final touch I did to create the visualization was draw the dashed bounding box around the whole cell:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-tex&#34; data-lang=&#34;tex&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;\node&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;[cell=(c0_kernel)(c0_state)(c0_new_state), label=above:Cell 0]&lt;/span&gt; {};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I followed a pretty similar process for the other RNN visualization in the blog post.&lt;/p&gt;
&lt;p&gt;By using this method, it&amp;rsquo;s possible to have fine-grained control of the visualization&amp;rsquo;s layout and the rendering of all of its components.  There also seem to be a pretty selection of resources available for TikZ which helped out a lot.  It definitely looks &lt;em&gt;way&lt;/em&gt; better than the graphviz version I had before.  It did take a good amount of time tweaking the node positions to be just right, but certainly worth it for the result.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Stale Queries When Hitting BigQuery Streaming Buffer When Using BI Engine</title>
      <link>https://cprimozic.net/notes/posts/bigquery-bi-engine-streaming-buffer-issues/</link>
      <pubDate>Thu, 06 Jul 2023 16:12:49 -0500</pubDate>
      <guid>https://cprimozic.net/notes/posts/bigquery-bi-engine-streaming-buffer-issues/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2023-08-15&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;Some engineers at Google reached out to me via e-mail after I submitted some feedback about this issue on the GCP console and linked to this blog post.&lt;/p&gt;
&lt;p&gt;After a few back and forth messages, they were able to diagnose the problem and put out a mitigation that completely fixed it for us!&lt;/p&gt;
&lt;p&gt;The issue seems to have stemmed from cursors tracking the position in the streaming buffer getting out of sync between the BI engine and base BigQuery.&lt;/p&gt;</description>
      <content>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2023-08-15&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;Some engineers at Google reached out to me via e-mail after I submitted some feedback about this issue on the GCP console and linked to this blog post.&lt;/p&gt;
&lt;p&gt;After a few back and forth messages, they were able to diagnose the problem and put out a mitigation that completely fixed it for us!&lt;/p&gt;
&lt;p&gt;The issue seems to have stemmed from cursors tracking the position in the streaming buffer getting out of sync between the BI engine and base BigQuery.&lt;/p&gt;
&lt;p&gt;This was happening due to a complex self-join in the query we were running; half the query ran in BI engine, and the other half ran on backend BigQuery.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not 100% sure if the mitigation is deployed for all users at this time, but it should be soon if not.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re still running into this issue, I suggest reaching out to Google via the feedback form in GCP that comes up when you disable BI engine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;We use BigQuery extensively at my job at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt;. We use it for a variety of use cases and both read and write to it very often using complex dynamically generated queries.&lt;/p&gt;
&lt;p&gt;To increase performance and reduce costs, we use &lt;a href=&#34;https://cloud.google.com/bigquery/docs/bi-engine-intro&#34;&gt;BI Engine&lt;/a&gt; for some of our tables. As described in the BI engine docs,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;BigQuery BI Engine is a fast, in-memory analysis service that accelerates many SQL queries in BigQuery by intelligently caching the data you use most frequently.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The table on which we ran into this issue uses &lt;a href=&#34;https://cloud.google.com/bigquery/docs/partitioned-tables#ingestion_time&#34;&gt;ingestion time partitioning&lt;/a&gt;. It lumps data together in its storage backend based on the timestamp at which it was inserted.&lt;/p&gt;
&lt;p&gt;When new data is inserted into ingestion time partitioned tables in BigQuery, it is added to a streaming insert buffer. Its &lt;code&gt;_PARTITIONTIME&lt;/code&gt; field is set to null until a point in the future when it&amp;rsquo;s asynchronously written to a partition and given a permanent &lt;code&gt;_PARTITIONTIME&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We explicitly create our queries to include data in the streaming buffer like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SELECT&lt;/span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WHERE&lt;/span&gt; _PARTITIONTIME &lt;span style=&#34;color:#66d9ef&#34;&gt;IS&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;OR&lt;/span&gt; _PARTITIONTIME &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;=&lt;/span&gt; TIMESTAMP_TRUNC(&lt;span style=&#34;color:#66d9ef&#34;&gt;TIMESTAMP&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;2023-07-06 22:23:38&amp;#39;&lt;/span&gt;), HOUR);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This used to work just fine. Recently inserted data would show up in query results seconds after it was written. However, at some point recently, that stopped happening.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;We were running into issues where some of our queries would fail to include data from recently inserted rows. We&amp;rsquo;d send the insert rows request to BigQuery, get a 200 response code, but then queries that look at that data would fail to include some of it until much later - sometimes up to an hour.&lt;/p&gt;
&lt;p&gt;The only setup where we noticed this happening was when we had all of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BigQuery table set up using &lt;a href=&#34;https://cloud.google.com/bigquery/docs/partitioned-tables#ingestion_time&#34;&gt;ingestion time partitioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;BI Engine enabled for the table&lt;/li&gt;
&lt;li&gt;Inserting data dynamically into the table and query that tries to read it from the streaming insert buffer (&lt;code&gt;_PARTITIONTIME IS NULL&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;&amp;hellip; We shut of BI engine and the problem immediately stopped.&lt;/p&gt;
&lt;p&gt;The thing is, this &lt;em&gt;definitely&lt;/em&gt; used to work for us in the past. We&amp;rsquo;re almost certain we made no changes to our code or our infrastructure that would cause this change in behavior, so our only remaining explanation is that something changed on GCP&amp;rsquo;s end.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if it can be considered a bug or not. Maybe the previous behavior was incorrect and this is just BI Engine&amp;rsquo;s caching working as intended.&lt;/p&gt;
&lt;p&gt;In any case, if you&amp;rsquo;re running into strange issues with data not showing up and have a scenario like this with BI engine enabled, try shutting it off and seeing if it fixes the issue for you.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Machine Learning Benchmarks on the 7900 XTX</title>
      <link>https://cprimozic.net/notes/posts/machine-learning-benchmarks-on-the-7900-xtx/</link>
      <pubDate>Sat, 01 Jul 2023 10:09:06 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/machine-learning-benchmarks-on-the-7900-xtx/</guid>
      <description>&lt;p&gt;I &lt;a href=&#34;https://cprimozic.net/notes/posts/upgrading-5700xt-to-7900xtx/&#34;&gt;recently upgraded&lt;/a&gt; to a 7900 XTX GPU. Besides being great for gaming, I wanted to try it out for some machine learning.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s well known that NVIDIA is the clear leader in AI hardware currently. Most ML frameworks have NVIDIA support via CUDA as their primary (or only) option for acceleration. OpenCL has not been up to the same level in either support or performance.&lt;/p&gt;
&lt;p&gt;That being said, the 7900 XTX is a very powerful card. It has 24GB of VRAM, a theoretical 60 TFLOPS of f32, and 120 TFLOPS of f16. The recent AI hype wave is also incentivizing AMD to beef ML support on their cards, and they &lt;a href=&#34;https://twitter.com/LisaSu/status/1669848494637735936?s=20&#34;&gt;seem to be making real investments&lt;/a&gt; in that space.&lt;/p&gt;</description>
      <content>&lt;p&gt;I &lt;a href=&#34;https://cprimozic.net/notes/posts/upgrading-5700xt-to-7900xtx/&#34;&gt;recently upgraded&lt;/a&gt; to a 7900 XTX GPU. Besides being great for gaming, I wanted to try it out for some machine learning.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s well known that NVIDIA is the clear leader in AI hardware currently. Most ML frameworks have NVIDIA support via CUDA as their primary (or only) option for acceleration. OpenCL has not been up to the same level in either support or performance.&lt;/p&gt;
&lt;p&gt;That being said, the 7900 XTX is a very powerful card. It has 24GB of VRAM, a theoretical 60 TFLOPS of f32, and 120 TFLOPS of f16. The recent AI hype wave is also incentivizing AMD to beef ML support on their cards, and they &lt;a href=&#34;https://twitter.com/LisaSu/status/1669848494637735936?s=20&#34;&gt;seem to be making real investments&lt;/a&gt; in that space.&lt;/p&gt;
&lt;p&gt;I ran some benchmarks to get a feel for its real-world performance right now.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;These benchmarks were done in June 2023. It&amp;rsquo;s very possible that these results will change dramatically over time, even in the short term.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;simple-tensorflow-neural-network-training-benchmark&#34;&gt;Simple TensorFlow Neural Network Training Benchmark&lt;/h2&gt;
&lt;p&gt;I wanted to get a feel for the actual performance that I can get with the 7900 XTX for a realistic ML training scenario. I started out by looking for some existing benchmarks that I could pull in to compare.&lt;/p&gt;
&lt;p&gt;I found &lt;a href=&#34;https://www.reddit.com/r/hardware/comments/qkc6n8/6900xt_tensorflow_mlai_benchmarks_using_rocm/&#34;&gt;a Reddit post&lt;/a&gt; by &lt;a href=&#34;https://www.reddit.com/user/cherryteastain&#34;&gt;cherryteastain&lt;/a&gt; that uses TensorFlow to run a few different ML benchmarks on a 6900XT card.&lt;/p&gt;
&lt;p&gt;One of the cases they benchmarked is training a very simple multi-layer neural network using random data. Although it&amp;rsquo;s simple, it has over 7 million parameters:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Model: &amp;#34;sequential&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_________________________________________________________________
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; Layer (type)                Output Shape              Param #
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;=================================================================
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; dense (Dense)               (None, 2500)              252500
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; dense_1 (Dense)             (None, 2500)              6252500
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; dense_2 (Dense)             (None, 250)               625250
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; dense_3 (Dense)             (None, 10)                2510
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;=================================================================
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Total params: 7,132,760
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Trainable params: 7,132,760
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Non-trainable params: 0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once I &lt;a href=&#34;https://cprimozic.net/notes/posts/setting-up-tensorflow-with-rocm-on-7900-xtx/&#34;&gt;set up ROCm and TensorFlow&lt;/a&gt;, I was able to run &lt;a href=&#34;https://pastebin.com/65z30rLs&#34;&gt;the exact script&lt;/a&gt; as-is. Just in case that pastebin gets deleted in the future, I&amp;rsquo;ll include the whole thing here since it&amp;rsquo;s so short anyway:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-py&#34; data-lang=&#34;py&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; tensorflow &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; tf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; numpy &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; np
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gpus &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;experimental&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;list_physical_devices(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GPU&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; gpus:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; gpu &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; gpus:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;experimental&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_memory_growth(gpu, &lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RuntimeError&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nclasses &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nsamples &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3000000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bsize &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; nsamples&lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;inp_units &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mod &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;keras&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Sequential([tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;keras&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;layers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;InputLayer(inp_units), tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;keras&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;layers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Dense(&lt;span style=&#34;color:#ae81ff&#34;&gt;2500&lt;/span&gt;, activation&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;relu&amp;#39;&lt;/span&gt;), tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;keras&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;layers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Dense(&lt;span style=&#34;color:#ae81ff&#34;&gt;2500&lt;/span&gt;, activation&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;relu&amp;#39;&lt;/span&gt;), tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;keras&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;layers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Dense(&lt;span style=&#34;color:#ae81ff&#34;&gt;250&lt;/span&gt;, activation&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;relu&amp;#39;&lt;/span&gt;), tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;keras&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;layers&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Dense(nclasses, activation&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;softmax&amp;#39;&lt;/span&gt;)])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mod&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;compile(loss&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;sparse_categorical_crossentropy&amp;#39;&lt;/span&gt;, optimizer&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;adam&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;inpt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; np&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;random&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;rand(nsamples,inp_units)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gtt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; np&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;random&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;randint(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,nclasses&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,nsamples)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;dset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;data&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Dataset&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;from_tensor_slices((inpt,gtt))&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;batch(bsize)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mod&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;fit(dset, epochs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;results&#34;&gt;Results&lt;/h3&gt;
&lt;p&gt;On my 7900 XTX GPU, I achieved 24 seconds per epoch. The author of the Reddit post included some timings for a variety of other cards that they tested:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;6900XT/System 1&lt;/th&gt;
          &lt;th&gt;Titan V/System 2&lt;/th&gt;
          &lt;th&gt;V100/System 3&lt;/th&gt;
          &lt;th&gt;7900 XTX (Mine)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;26s (1254ms/step)&lt;/td&gt;
          &lt;td&gt;19s (946ms/step)&lt;/td&gt;
          &lt;td&gt;20s (996ms/step)&lt;/td&gt;
          &lt;td&gt;24s (~1180ms/step)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;hellip; Not what I wanted to see. My card is performing barely better than the 6900 XT tested by the author. The 6900 XT has a theoretical max of &lt;a href=&#34;https://www.techpowerup.com/gpu-specs/radeon-rx-6900-xt.c3481&#34;&gt;23 TFLOPS&lt;/a&gt; of FP32 performance - less than 40% of &lt;a href=&#34;https://www.techpowerup.com/gpu-specs/radeon-rx-7900-xtx.c3941&#34;&gt;the 7900 XTX&lt;/a&gt; which has 61 TFLOPS of FP32 performance.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure why the performance is so bad. One possibility is that it&amp;rsquo;s something to do with the &lt;a href=&#34;https://cprimozic.net/notes/posts/setting-up-tensorflow-with-rocm-on-7900-xtx/&#34;&gt;hacky way&lt;/a&gt; I compiled TensorFlow to work with ROCm 5.5 and the 7900 XTX. I feel like it&amp;rsquo;s quite possible that there will be some changes to the ROCm TensorFlow fork in the future or ROCm drivers themselves that fix this performance to be more in line with the card&amp;rsquo;s actual power.&lt;/p&gt;
&lt;h2 id=&#34;resnet50-via-tf_cnn_benchmarks&#34;&gt;&lt;code&gt;resnet50&lt;/code&gt; via &lt;code&gt;tf_cnn_benchmarks&lt;/code&gt;&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# Needed by my hacky TensorFlow setup
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt; export HSA_OVERRIDE_GFX_VERSION=11.0.0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt; git clone https://github.com/tensorflow/benchmarks.git
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt; cd benchmarks/scripts/tf_cnn_benchmarks
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# I edited the `tf_cnn_benchmarks.py` file add the bit of code to
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# enable GPU memory growth so that my computer doesn&amp;#39;t entirely
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# lock up while it&amp;#39;s running
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt; python3 tf_cnn_benchmarks.py --num_gpus=1 --batch_size=128 --model=resnet50
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;... debug output ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Running warm up
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Done warm up
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Step    Img/sec total_loss
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;1       images/sec: 126.3 +/- 0.0 (jitter = 0.0)        7.442
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;10      images/sec: 126.3 +/- 0.1 (jitter = 0.3)        7.423
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;20      images/sec: 126.1 +/- 0.1 (jitter = 0.3)        7.469
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;30      images/sec: 125.5 +/- 0.4 (jitter = 0.2)        7.568
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;40      images/sec: 125.7 +/- 0.3 (jitter = 0.2)        7.515
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;50      images/sec: 125.8 +/- 0.3 (jitter = 0.3)        7.452
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So, around 126 images/sec for resnet50. A &lt;a href=&#34;https://www.reddit.com/r/Amd/comments/asdyon/radeon_vii_tensorflow_deep_learning_results_huge/&#34;&gt;Reddit thread from 4 years ago&lt;/a&gt; that ran the same benchmark on a Radeon VII - a &amp;gt;4-year-old card with 13.4 TFLOPS FP32 performance - resulted in a score of 147 back then.&lt;/p&gt;
&lt;p&gt;This leads me to believe that there&amp;rsquo;s a software issue at some point. Maybe it&amp;rsquo;s my janky TensorFlow setup, maybe it&amp;rsquo;s poor ROCm/driver support for the 7900 XTX, or maybe it&amp;rsquo;s some some obscure boot param I added to my system 3 years ago. I really don&amp;rsquo;t know.&lt;/p&gt;
&lt;p&gt;One thing is clear though: My TensorFlow performance is not anywhere near where it should be for this hardware.&lt;/p&gt;
&lt;h2 id=&#34;benchmarking-7900-xtx-raw-fp32-flops&#34;&gt;Benchmarking 7900 XTX Raw FP32 FLOPS&lt;/h2&gt;
&lt;p&gt;After that bad result, I wanted to see if I could actually reach the 61 TFLOPS of FP32 performance advertised for the card.&lt;/p&gt;
&lt;p&gt;While poking around online, I discovered the &lt;a href=&#34;https://tinygrad.org&#34;&gt;tinygrad&lt;/a&gt; library. It&amp;rsquo;s a minimalist ML framework built from the ground up on a very tiny foundation of basic operations. That being said, it&amp;rsquo;s still quite capable and is able to run full-scale complex networks like &lt;a href=&#34;https://github.com/geohot/tinygrad/blob/master/examples/stable_diffusion.py&#34;&gt;Stable Diffusion&lt;/a&gt; natively.&lt;/p&gt;
&lt;p&gt;Tinygrad targets AMD GPUs as one of their backends. They support both OpenCL-based kernels as well as a work-in-progress &lt;a href=&#34;https://github.com/geohot/tinygrad/blob/master/tinygrad/codegen/assembly_rdna.py&#34;&gt;RDNA3 Assembler&lt;/a&gt; backend.&lt;/p&gt;
&lt;p&gt;The native RDNA3 backend was very interesting to me. Pretty recently after the 7900 XTX was released, Chips and Cheese put out a &lt;a href=&#34;https://chipsandcheese.com/2023/01/07/microbenchmarking-amds-rdna-3-graphics-architecture/&#34;&gt;detailed microbenchmarking post&lt;/a&gt; for the 7900 XTX that used OpenCL to test various different microarchitectural properties of the card and raw performance numbers. One thing they noted was that the OpenCL compiler at the time was doing a poor job of making use of the &amp;ldquo;dual issue&amp;rdquo; mode of RRDNA3 to execute multiple instructions in parallel:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;I’m guessing RDNA 3’s dual issue mode will have limited impact. It relies heavily on the compiler to find VOPD possibilities, and compilers are frustratingly stupid at seeing very simple optimizations.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Using dual issue, RDNA 3 GPUs like the 7900 XTX are able to kick off two &lt;code&gt;v_dual_fmac_f32&lt;/code&gt; (fused multiply-accumulate) instructions at once. That results in assembly that looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-asm&#34; data-lang=&#34;asm&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;v_dual_fmac_f32&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;v108&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;v109&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;v110&lt;/span&gt; :: &lt;span style=&#34;color:#66d9ef&#34;&gt;v_dual_fmac_f32&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;v111&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;v112&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;v113&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since each of these &lt;code&gt;fmac&lt;/code&gt; instructions results in 2 floating point operations (multiply + add) and two of them are dispatched per cycle, that comes out to 4 FLOPS of throughput per cycle. And since GPUs are SIMD machines, many of these instructions are executed in parallel across the GPU at the same time.&lt;/p&gt;
&lt;h3 id=&#34;tinygrad-rdna3-matrix-multiplication-benchmark&#34;&gt;Tinygrad RDNA3 Matrix Multiplication Benchmark&lt;/h3&gt;
&lt;p&gt;I don&amp;rsquo;t remember how I found it, but Tinygrad has a &lt;a href=&#34;https://github.com/geohot/tinygrad/blob/master/extra/rocm/rdna3/asm.py&#34;&gt;benchmark script&lt;/a&gt; for raw RDNA3 floating point throughput.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s about as low-level as it gets. There&amp;rsquo;s a raw assembly file that has some boilerplate and loop handling code, and then the script dynamically generates the loop body out of nothing but &lt;code&gt;v_dual_fmac_f32 :: v_dual_fmac_f32&lt;/code&gt; instructions. There is no data loading or memory interaction at all; all operations read from and write to registers directly. This is far from anything &amp;ldquo;real-world&amp;rdquo; of course, but since the goal is to determine the max F32 throughput it&amp;rsquo;s perfect.&lt;/p&gt;
&lt;p&gt;Somewhat miraculously, I was able to run the script as-is on my own 7900 XTX directly. Here&amp;rsquo;s what it output:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt; # Exclude iGPU from being used by OpenCL; run code on the 7900 XTX only
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt; export CL_EXCLUDE=gfx1036
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt; DEBUGCL=5 DEBUG=5 GPU=1 OPTLOCAL=1 PRINT_KERNEL=1 python3 asm.py
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;... lots of debug info and disassembly ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ran in 77.72 ms, 56663.33 GFLOPS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ran in 73.30 ms, 60082.85 GFLOPS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ran in 71.29 ms, 61775.70 GFLOPS
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Well there it is! After a couple warmup iterations, it&amp;rsquo;s able to hit the 61 TFLOPS mark.&lt;/p&gt;
&lt;p&gt;As I said before, this is far from any kind of a real-world benchmark. Memory is usually the bottleneck for any real model. However, this does validate the claims of peak theoretical floating-point throughput.&lt;/p&gt;
&lt;h3 id=&#34;16-bit-floating-point&#34;&gt;16-bit Floating Point&lt;/h3&gt;
&lt;p&gt;After seeing that result, I looked into testing the FP16 support as well. It turns out that the only way to achieve that massive 120 TFLOPS of FP16 advertised is by using the tensor/&amp;ldquo;AI&amp;rdquo; cores. These are specialized for matrix multiplication using FP16, BF16, IU8, and IU4 data types.&lt;/p&gt;
&lt;p&gt;They can be interacted with via &lt;a href=&#34;https://gpuopen.com/learn/wmma_on_rdna3/&#34;&gt;WMMA instructions and compiler intrinsics&lt;/a&gt;. So, benchmarking FP16 on the 7900 XTX would require me to write some assembly code.&lt;/p&gt;
&lt;p&gt;At the time of writing this, that&amp;rsquo;s not something I want to devote the (possibly copious amounts of) time into right now, so I&amp;rsquo;ll put that in the future work pile.&lt;/p&gt;
&lt;h2 id=&#34;porting-the-tensorflow-benchmark-to-tinygrad&#34;&gt;Porting the TensorFlow benchmark to TinyGrad&lt;/h2&gt;
&lt;p&gt;Now TensorFlow seems to be the weak link, I wanted to see if I could replicate the simple neural network training example with Tinygrad. There is great interest in that community to support AMD cards natively and give them a more first-class-like experience for machine learning, so I figured it might be able to offer better performance.&lt;/p&gt;
&lt;p&gt;I did my best to port the TensorFlow code over to Tinygrad directly. Tinygrad does have support for everything needed by the benchmark (which is a good thing since it&amp;rsquo;s so simple).&lt;/p&gt;
&lt;p&gt;When I ran it, though, the Tinygrad port was very slow - around ~2x slower than the TensorFlow version.&lt;/p&gt;
&lt;p&gt;After researching the project and learning more about it, I created an improved version which uses Tinygrad&amp;rsquo;s &lt;a href=&#34;https://github.com/geohot/tinygrad/blob/master/tinygrad/jit.py&#34;&gt;built-in JIT compilation&lt;/a&gt; to speed it up. After tweaking and tuning that a bit, I eventually got a version that beat TensorFlow by ~10%. However, that really did require me to do a lot of experimentation and delving into the Tinygrad internals and source code.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a lot of heuristics and magic going on that greatly impacts the performance of Tinygrad. I think that if I spent a similar amount of time modifying the TF benchmark and tuning it similarly, I&amp;rsquo;d be able to get that or more of a boost as well.&lt;/p&gt;
&lt;p&gt;Also, Tinygrad changes dramatically from day to day. When I re-ran the benchmark a week later without changing the benchmark code at all, performance had again regressed significantly to the point where it&amp;rsquo;s many times slower than TensorFlow.&lt;/p&gt;
&lt;p&gt;I also tried out the &lt;a href=&#34;https://github.com/geohot/tinygrad/blob/master/tinygrad/codegen/assembly_rdna.py&#34;&gt;RDNA3 Assembler backend&lt;/a&gt;. There are known limitations in the OpenCL compiler for AMD used by the default Tinygrad backend, so it&amp;rsquo;s possible that generating RDNA3 assembly code directly could yield results much closer to theoretical maximums.&lt;/p&gt;
&lt;p&gt;This backend is far from complete, though, and failed to compile the small benchmark example. I went as far as to &lt;a href=&#34;https://github.com/geohot/tinygrad/pull/1012&#34;&gt;put up a PR&lt;/a&gt; myself to Tinygrad to try to extend its functionality enough to compile the benchmark. However, I hit another roadblock that was too complex for me to implement myself, so I gave up on that work.&lt;/p&gt;
&lt;p&gt;The takeaway is that even if Tinygrad is capable of beating Tensorflow sometimes, there are a good deal of quirks and issues that make it difficult to recommend right now. That being said, development on the project really is proceeding at an immense pace, and I wouldn&amp;rsquo;t be surprised if in a few months it&amp;rsquo;s able to handily beat TensorFlow on AMD GPUs across a majority of benchmarks.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s obviously a lot to be desired for machine learning on AMD GPUs at the current point in time. A couple days ago, &lt;a href=&#34;https://github.com/RadeonOpenCompute/ROCm/releases/tag/rocm-5.6.0&#34;&gt;ROCm 5.6 has been released&lt;/a&gt;. I&amp;rsquo;ve not been able to install it and try it out yet, but it&amp;rsquo;s possible it might bring some performance improvements.&lt;/p&gt;
&lt;p&gt;AMD has also said that they plan on adding official RDNA3 support to ROCm by this fall of 2023. Since RDNA3 isn&amp;rsquo;t even technically supported by ROCm right now, I feel like there&amp;rsquo;s definitely a ton of room for improvement on that side of things and a lot of hope things will get better.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll continue to keep an eye on this space until then!
`&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fixing GCS REST API Error &#34;Your client has issued a malformed or illegal request&#34;</title>
      <link>https://cprimozic.net/notes/posts/gcs-upload-client-malformed-or-illegal-request/</link>
      <pubDate>Sat, 01 Jul 2023 09:39:30 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/gcs-upload-client-malformed-or-illegal-request/</guid>
      <description>&lt;p&gt;We ran into this error at my job at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt;. We upload files to GCS using their &lt;a href=&#34;https://cloud.google.com/storage/docs/json_api/v1&#34;&gt;JSON-based REST API&lt;/a&gt;. Everything was working just fine until we tried uploading a large-ish file of ~2.5GB.&lt;/p&gt;
&lt;p&gt;We upload to this API route: &lt;code&gt;https://storage.googleapis.com/upload/storage/v1/b/bucket-name/o?name=file_name.csv&amp;amp;uploadType=media&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;When we tried to upload the data, we got a HTML page as a response with a 400 error code and this unhelpful error message:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;400. That&amp;#39;s an error.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Your client has issued a malformed or illegal request.  That&amp;#39;s all we know.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;We were about to fix the issue by adding a &lt;code&gt;Content-Length&lt;/code&gt; header to the request. It seems that for some requests, it isn&amp;rsquo;t necessary, but it is for others.&lt;/p&gt;</description>
      <content>&lt;p&gt;We ran into this error at my job at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt;. We upload files to GCS using their &lt;a href=&#34;https://cloud.google.com/storage/docs/json_api/v1&#34;&gt;JSON-based REST API&lt;/a&gt;. Everything was working just fine until we tried uploading a large-ish file of ~2.5GB.&lt;/p&gt;
&lt;p&gt;We upload to this API route: &lt;code&gt;https://storage.googleapis.com/upload/storage/v1/b/bucket-name/o?name=file_name.csv&amp;amp;uploadType=media&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;When we tried to upload the data, we got a HTML page as a response with a 400 error code and this unhelpful error message:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;400. That&amp;#39;s an error.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Your client has issued a malformed or illegal request.  That&amp;#39;s all we know.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;We were about to fix the issue by adding a &lt;code&gt;Content-Length&lt;/code&gt; header to the request. It seems that for some requests, it isn&amp;rsquo;t necessary, but it is for others.&lt;/p&gt;
&lt;p&gt;The interface that we use to make this request the &lt;a href=&#34;https://docs.rs/reqwest/latest/reqwest/&#34;&gt;&lt;code&gt;reqwest&lt;/code&gt;&lt;/a&gt; library from Rust to make the HTTP request. We use a Rust stream as the body, which may contribute to the issue.&lt;/p&gt;
&lt;p&gt;For this particular case, we do know the size of the data ahead of time so we can provide the &lt;code&gt;Content-Length&lt;/code&gt; header accurately. However, for cases where large amounts of data needs to be uploaded to GCS as a stream without a known size ahead of time, I&amp;rsquo;m not sure what the solution is.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fix for League of Legends Lutris Mesa DRI Driver &#34;Not From This Mesa Build&#34; Error</title>
      <link>https://cprimozic.net/notes/posts/league-of-legends-lutris-mesa-dri-driver-not-from-this-mesa-build/</link>
      <pubDate>Sun, 18 Jun 2023 23:22:46 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/league-of-legends-lutris-mesa-dri-driver-not-from-this-mesa-build/</guid>
      <description>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I recently updated the packages on my Debian Linux install with &lt;code&gt;sudo apt upgrade&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After that, I rebooted and tried to launch League of Legends through &lt;a href=&#34;https://lutris.net/&#34;&gt;Lutris&lt;/a&gt; as I have hundreds of times.  The client failed to launch with this error printed in the logs:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;DRI driver not from this Mesa build (&amp;#39;23.1.0-devel&amp;#39; vs &amp;#39;23.1.2-1&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;I &lt;a href=&#34;https://cprimozic.net/notes/posts/setting-up-tensorflow-with-rocm-on-7900-xtx/&#34;&gt;recently installed AMD ROCm&lt;/a&gt; using &lt;a href=&#34;https://amdgpu-install.readthedocs.io/en/latest/&#34;&gt;&lt;code&gt;amdgpu-install&lt;/code&gt;&lt;/a&gt; so I could do some machine learning with my AMD 7900 XTX GPU.&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;I recently updated the packages on my Debian Linux install with &lt;code&gt;sudo apt upgrade&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After that, I rebooted and tried to launch League of Legends through &lt;a href=&#34;https://lutris.net/&#34;&gt;Lutris&lt;/a&gt; as I have hundreds of times.  The client failed to launch with this error printed in the logs:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;DRI driver not from this Mesa build (&amp;#39;23.1.0-devel&amp;#39; vs &amp;#39;23.1.2-1&amp;#39;)
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;I &lt;a href=&#34;https://cprimozic.net/notes/posts/setting-up-tensorflow-with-rocm-on-7900-xtx/&#34;&gt;recently installed AMD ROCm&lt;/a&gt; using &lt;a href=&#34;https://amdgpu-install.readthedocs.io/en/latest/&#34;&gt;&lt;code&gt;amdgpu-install&lt;/code&gt;&lt;/a&gt; so I could do some machine learning with my AMD 7900 XTX GPU.&lt;/p&gt;
&lt;p&gt;I installed &lt;code&gt;amdgpu-install&lt;/code&gt; version &lt;code&gt;5.5.50500-1&lt;/code&gt;.  This seems to force a specific version of some part of Mesa that&amp;rsquo;s incompatible with the one needed by Lutris, or Wine, or whatever other layer of the arcane Linux Graphics Stack is causing this issue.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I uninstalled ROCm by running:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo amdgpu-install --uninstall
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I then rebooted, and now I&amp;rsquo;m back tearing it up on the rift.&lt;/p&gt;
&lt;p&gt;I tried installing a newer version of &lt;code&gt;amdgpu-install&lt;/code&gt;, but that failed with dozens of dependency version conflicts, so it doesn&amp;rsquo;t seem like a viable fix right now.  Hopefully one or the other will be updated to make the versions compatible in the future.&lt;/p&gt;
&lt;p&gt;I will re-install ROCm tomorrow when I get in the mood to be a productive human again.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fix for AMD OpenCL Devices Showing Up as the Same Name</title>
      <link>https://cprimozic.net/notes/posts/all-amd-opencl-devices-showing-up-as-same-name/</link>
      <pubDate>Sun, 18 Jun 2023 14:10:17 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/all-amd-opencl-devices-showing-up-as-same-name/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been experimenting with OpenCL via &lt;a href=&#34;https://documen.tician.de/pyopencl/&#34;&gt;&lt;code&gt;pyopencl&lt;/code&gt;&lt;/a&gt; recently.  They provide a nice interface for enumerating available devices and getting information about them, and then using them to run OpenCL code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; import pyopencl as cl
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; platform = cl.get_platforms()[0]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; platform.get_devices()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;pyopencl.Device &amp;#39;gfx1100&amp;#39; on &amp;#39;AMD Accelerated Parallel Processing&amp;#39; at 0x56353125bd70&amp;gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;pyopencl.Device &amp;#39;gfx1036&amp;#39; on &amp;#39;AMD Accelerated Parallel Processing&amp;#39; at 0x5635312ec670&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are two devices for me because I have one discrete 7900 XTX GPU as well as an integrated GPU on my 7950X CPU.  &lt;code&gt;gfx1100&lt;/code&gt; is the Shader ISA &lt;a href=&#34;https://www.techpowerup.com/gpu-specs/amd-navi-31.g998&#34;&gt;for the 7900 XTX&lt;/a&gt;, and &lt;code&gt;gfx1036&lt;/code&gt; is &lt;a href=&#34;https://www.techpowerup.com/gpu-specs/radeon-graphics-128sp.c3993&#34;&gt;for the iGPU&lt;/a&gt;.&lt;/p&gt;</description>
      <content>&lt;p&gt;I&amp;rsquo;ve been experimenting with OpenCL via &lt;a href=&#34;https://documen.tician.de/pyopencl/&#34;&gt;&lt;code&gt;pyopencl&lt;/code&gt;&lt;/a&gt; recently.  They provide a nice interface for enumerating available devices and getting information about them, and then using them to run OpenCL code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; import pyopencl as cl
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; platform = cl.get_platforms()[0]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; platform.get_devices()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;pyopencl.Device &amp;#39;gfx1100&amp;#39; on &amp;#39;AMD Accelerated Parallel Processing&amp;#39; at 0x56353125bd70&amp;gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;pyopencl.Device &amp;#39;gfx1036&amp;#39; on &amp;#39;AMD Accelerated Parallel Processing&amp;#39; at 0x5635312ec670&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are two devices for me because I have one discrete 7900 XTX GPU as well as an integrated GPU on my 7950X CPU.  &lt;code&gt;gfx1100&lt;/code&gt; is the Shader ISA &lt;a href=&#34;https://www.techpowerup.com/gpu-specs/amd-navi-31.g998&#34;&gt;for the 7900 XTX&lt;/a&gt;, and &lt;code&gt;gfx1036&lt;/code&gt; is &lt;a href=&#34;https://www.techpowerup.com/gpu-specs/radeon-graphics-128sp.c3993&#34;&gt;for the iGPU&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;At some point, &lt;code&gt;pyopencl&lt;/code&gt; started returning &lt;code&gt;gfx1100&lt;/code&gt; as the name for both of my devices:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; import pyopencl as cl
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; platform = cl.get_platforms()[0]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; platform.get_devices()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;pyopencl.Device &amp;#39;gfx1100&amp;#39; on &amp;#39;AMD Accelerated Parallel Processing&amp;#39; at 0x555cc5a8ed60&amp;gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;pyopencl.Device &amp;#39;gfx1100&amp;#39; on &amp;#39;AMD Accelerated Parallel Processing&amp;#39; at 0x555cc5b1f5f0&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; [d.hashable_model_and_version_identifier for d in platform.get_devices()]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&amp;#39;v1&amp;#39;, &amp;#39;Advanced Micro Devices, Inc.&amp;#39;, 4098, &amp;#39;gfx1100&amp;#39;, &amp;#39;OpenCL 2.0 &amp;#39;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&amp;#39;v1&amp;#39;, &amp;#39;Advanced Micro Devices, Inc.&amp;#39;, 4098, &amp;#39;gfx1100&amp;#39;, &amp;#39;OpenCL 2.0 &amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There was no way to tell them apart.  I needed my code to only run on my 7900 XTX and not on the iGPU, and this caused the filtering my code was doing on &lt;code&gt;device.name&lt;/code&gt; to not work since I had no way to tell the devices apart.&lt;/p&gt;
&lt;h2 id=&#34;the-cause--the-fix&#34;&gt;The Cause + The Fix&lt;/h2&gt;
&lt;p&gt;It turns out that I had exported the environment variable &lt;code&gt;HSA_OVERRIDE_GFX_VERSION=11.0.0&lt;/code&gt;.  I did this to support &lt;a href=&#34;https://cprimozic.net/notes/posts/setting-up-tensorflow-with-rocm-on-7900-xtx/&#34;&gt;building a custom TensorFlow package&lt;/a&gt; that works on the 7900 XTX with ROCm 5.5.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It turns out that setting that environment variable causes some driver in the stack to pretend that &lt;em&gt;all&lt;/em&gt; the devices support &lt;code&gt;gfx1100&lt;/code&gt; and changes their names as well.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For this OpenCL code, I wasn&amp;rsquo;t using TensorFlow and so didn&amp;rsquo;t need to export that anymore.  I expect that in the near future, that hack won&amp;rsquo;t be necessary anymore anyway.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;unset&lt;/code&gt;ting that environment variable fixed the problem, and my cards are now properly identified by OpenCL again.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Setting Up TensorFlow with ROCm on the 7900 XTX</title>
      <link>https://cprimozic.net/notes/posts/setting-up-tensorflow-with-rocm-on-7900-xtx/</link>
      <pubDate>Fri, 09 Jun 2023 13:13:58 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/setting-up-tensorflow-with-rocm-on-7900-xtx/</guid>
      <description>&lt;p&gt;I recently upgraded to a 7900 XTX GPU.  The upgrade itself went &lt;a href=&#34;https://cprimozic.net/notes/posts/upgrading-5700xt-to-7900xtx/&#34;&gt;quite smoothly&lt;/a&gt; from both a hardware and software perspective.  Games worked great out of the box with no driver or other configuration needed - as plug and play as it could possibly get.&lt;/p&gt;
&lt;p&gt;However, I wanted to try out some machine learning on it.  I&amp;rsquo;d been &lt;a href=&#34;https://cprimozic.net/blog/fullstack-sveltekit-recommendation-app-middle-end-development/#tensorflowjs&#34;&gt;using TensorFlow.JS&lt;/a&gt; to train models using my GPU all in the browser, but that approach is limited compared to what&amp;rsquo;s possible when running it natively.&lt;/p&gt;</description>
      <content>&lt;p&gt;I recently upgraded to a 7900 XTX GPU.  The upgrade itself went &lt;a href=&#34;https://cprimozic.net/notes/posts/upgrading-5700xt-to-7900xtx/&#34;&gt;quite smoothly&lt;/a&gt; from both a hardware and software perspective.  Games worked great out of the box with no driver or other configuration needed - as plug and play as it could possibly get.&lt;/p&gt;
&lt;p&gt;However, I wanted to try out some machine learning on it.  I&amp;rsquo;d been &lt;a href=&#34;https://cprimozic.net/blog/fullstack-sveltekit-recommendation-app-middle-end-development/#tensorflowjs&#34;&gt;using TensorFlow.JS&lt;/a&gt; to train models using my GPU all in the browser, but that approach is limited compared to what&amp;rsquo;s possible when running it natively.&lt;/p&gt;
&lt;p&gt;Since TensorFlow is the framework I&amp;rsquo;m most familiar with, it&amp;rsquo;s what I wanted to focus on getting working first.&lt;/p&gt;
&lt;p&gt;AMD&amp;rsquo;s provided method for doing this is called &lt;a href=&#34;https://docs.amd.com/category/ROCm_v5.5&#34;&gt;ROCm&lt;/a&gt;.  It&amp;rsquo;s a combination of mostly-open libraries, toolkits, frameworks, compilers, and other software to facilitate heterogenous/GPGPU compute on AMD hardware.&lt;/p&gt;
&lt;h2 id=&#34;installing-rocm&#34;&gt;Installing ROCm&lt;/h2&gt;
&lt;p&gt;This part of the process took me the longest to figure out, but it ended up being quite simple.  AMD provides a tool called &lt;a href=&#34;https://docs.amd.com/bundle/ROCm-Installation-Guide-v5.5/page/How_to_Install_ROCm.html&#34;&gt;amdgpu-install&lt;/a&gt; which handles installing ROCm and other AMD software and drivers.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.phoronix.com/news/ROCm-5.5-Released&#34;&gt;ROCm version 5.5&lt;/a&gt; is the most recent version available at the time of release.  It contains improvements for RX 7000 series / RDNA3 GPU support which includes the 7900 XTX.  Version 5.6 is &lt;a href=&#34;https://www.reddit.com/r/Amd/comments/12z0tme/rocm_docs_560_alpha_on_windows/&#34;&gt;in the works&lt;/a&gt; as well, and there are rumors it will bring even more mature RDNA3 support and hopefully better performance as well.&lt;/p&gt;
&lt;p&gt;I installed &lt;code&gt;amdgpu-install&lt;/code&gt; by running &lt;code&gt;curl https://repo.radeon.com/amdgpu-install/5.5/ubuntu/jammy/amdgpu-install_5.5.50500-1_all.deb &amp;gt; /tmp/amdgpu-install.deb &amp;amp;&amp;amp; sudo dpkg -i /tmp/amdgpu-install.deb&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The ROCm install tutorial says to run &lt;code&gt;amdgpu-install --usecase=rocm&lt;/code&gt;.  For me, this ended up causing a lot of issues.&lt;/p&gt;
&lt;p&gt;The reason for that is that by default, that command attempts to build and install the &lt;code&gt;amdgpu-dkim&lt;/code&gt; kernel module.  I don&amp;rsquo;t understand how/if that differs from the &lt;code&gt;amdgpu&lt;/code&gt; kernel module that comes built-into the Linux kernel already.&lt;/p&gt;
&lt;p&gt;Anyway, the build for that kernel module failed for me because of differences in the kernel version targeted by ROCm 5.5 and my current kernel (Linux 6.3).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It turns out that building the kernel module isn&amp;rsquo;t necessary at all to get ROCm installed.  Running &lt;code&gt;sudo amdgpu-install --usecase=rocm --no-dkms&lt;/code&gt; worked for me.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I followed the rest of the instructions setting some user groups and rebooting, and things seemed to be in good shape.&lt;/p&gt;
&lt;h2 id=&#34;building-7900-xtx-compatible-tensorflow&#34;&gt;Building 7900 XTX-Compatible TensorFlow&lt;/h2&gt;
&lt;p&gt;The next step was building a custom TensorFlow that works with ROCm version 5.5 and the 7900 XTX.&lt;/p&gt;
&lt;p&gt;AMD maintains &lt;a href=&#34;https://github.com/ROCmSoftwarePlatform/tensorflow-upstream&#34;&gt;a TensorFlow fork&lt;/a&gt; for this, but at the time of writing this (June 9, 2023) it&amp;rsquo;s not yet updated for ROCm 5.5.  So, we have to compile our own.&lt;/p&gt;
&lt;p&gt;Luckily, some other people have done the vast majority of the hard work already!&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/Mushoz&#34;&gt;@Mushoz&lt;/a&gt; posted &lt;a href=&#34;https://github.com/RadeonOpenCompute/ROCm/issues/1880#issuecomment-1547838227&#34;&gt;a comment&lt;/a&gt; with a Dockerfile and instructions for handling this whole process.  It patches TensorFlow to work with gfx1100 (used by the 7900 XTX), compiles it all, and produces a Docker image as output.&lt;/p&gt;
&lt;p&gt;I followed their instructions exactly and was able to get it to work with no issues.&lt;/p&gt;
&lt;p&gt;When I went to test TensorFlow&amp;rsquo;s GPU functionality from inside the container, though, I ran into some more problems:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; import tensorflow as tf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; tf.test.is_gpu_available()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WARNING:tensorflow:From &amp;lt;stdin&amp;gt;:1: is_gpu_available (from tensorflow.python.framework.test_util) is deprecated and will be removed in a future version.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Instructions for updating:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Use `tf.config.list_physical_devices(&amp;#39;GPU&amp;#39;)` instead.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 09:16:04.455832: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  SSE3 SSE4.1 SSE4.2 AVX AVX2 AVX512F AVX512_VNNI AVX512_BF16 FMA
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 09:16:04.456591: E tensorflow/compiler/xla/stream_executor/rocm/rocm_driver.cc:302] failed call to hipInit: HIP_ERROR_InvalidDevice
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 09:16:04.456599: I tensorflow/compiler/xla/stream_executor/rocm/rocm_diagnostics.cc:112] retrieving ROCM diagnostic information for host: devitra
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 09:16:04.456602: I tensorflow/compiler/xla/stream_executor/rocm/rocm_diagnostics.cc:119] hostname: devitra
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 09:16:04.456631: I tensorflow/compiler/xla/stream_executor/rocm/rocm_diagnostics.cc:142] librocm reported version is: NOT_FOUND: was unable to find librocm.so DSO loaded into this program
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 09:16:04.456635: I tensorflow/compiler/xla/stream_executor/rocm/rocm_diagnostics.cc:146] kernel reported version is: UNIMPLEMENTED: kernel reported driver version not implemented
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;False
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This seemed to indicate some kind of driver version mismatch or other issue.  I still don&amp;rsquo;t know what the exact issue was (Mushoz didn&amp;rsquo;t seem to run into this), but I was able to work around it.&lt;/p&gt;
&lt;p&gt;The workaround involves copying the built TensorFlow Python wheel out of the container and running it directly on my host inside a virtual environment.  Here&amp;rsquo;s how I accomplished that:&lt;/p&gt;
&lt;p&gt;On host:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;chmod &lt;span style=&#34;color:#ae81ff&#34;&gt;777&lt;/span&gt; ~/dockerx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In container:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cp /tmp/tensorflow_pkg/tensorflow_rocm-2.11.1.-cp310-cp310-linux_x86_64.whl ~/dockerx/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Back on host:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pyenv install 3.10.6
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pyenv virtualenv 3.10.6 tf-test
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pyenv activate tf-test
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip3 install ~/dockerx/tensorflow_rocm-2.11.1.-cp310-cp310-linux_x86_64.whl
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;export HSA_OVERRIDE_GFX_VERSION&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;11.0.0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After doing that and re-running the same Python commands to test TensorFlow&amp;rsquo;s GPU integration, it properly detected my GPU as supported.  Being able to do all of that directly on the host is actually much better for me as well since I can use that virtual environment directly for notebooks from VS Code.&lt;/p&gt;
&lt;p&gt;I did run into segfaults from inside &lt;code&gt;libamdhip64.so.5&lt;/code&gt; when I actually tried to run some TensorFlow code using the GPU:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#0  0x00007fa1d3cd9d25 in ?? () from /opt/rocm-5.5.0/lib/libamdhip64.so.5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#1  0x00007fa1d3cd9ead in ?? () from /opt/rocm-5.5.0/lib/libamdhip64.so.5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#2  0x00007fa1d3cdda41 in ?? () from /opt/rocm-5.5.0/lib/libamdhip64.so.5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#3  0x00007fa1d3c8f9de in ?? () from /opt/rocm-5.5.0/lib/libamdhip64.so.5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#4  0x00007fa1d3e404ab in ?? () from /opt/rocm-5.5.0/lib/libamdhip64.so.5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#5  0x00007fa1d3e117f6 in ?? () from /opt/rocm-5.5.0/lib/libamdhip64.so.5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#6  0x00007fa1d3e1db86 in hipLaunchKernel () from /opt/rocm-5.5.0/lib/libamdhip64.so.5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#7  0x00007fa1b7197496 in Eigen::internal::TensorExecutor&amp;lt;Eigen::TensorAssignOp&amp;lt;Eigen::TensorMap&amp;lt;Eigen::Tensor&amp;lt;float, 1, 1, int&amp;gt;, 16, Eigen::MakePointer&amp;gt;, Eigen::TensorCwiseNullaryOp&amp;lt;Eigen::internal::scalar_const_op&amp;lt;float&amp;gt;, Eigen::TensorMap&amp;lt;Eigen::Tensor&amp;lt;float, 1, 1, int&amp;gt;, 16, Eigen::MakePointer&amp;gt; const&amp;gt; const&amp;gt; const, Eigen::GpuDevice, true, (Eigen::internal::TiledEvaluation)0&amp;gt;::run(Eigen::TensorAssignOp&amp;lt;Eigen::TensorMap&amp;lt;Eigen::Tensor&amp;lt;float, 1, 1, int&amp;gt;, 16, Eigen::MakePointer&amp;gt;, Eigen::TensorCwiseNullaryOp&amp;lt;Eigen::internal::scalar_const_op&amp;lt;float&amp;gt;, Eigen::TensorMap&amp;lt;Eigen::Tensor&amp;lt;float, 1, 1, int&amp;gt;, 16, Eigen::MakePointer&amp;gt; const&amp;gt; const&amp;gt; const&amp;amp;, Eigen::GpuDevice const&amp;amp;) () from /home/casey/.pyenv/versions/tf-test/lib/python3.10/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#8  0x00007fa1b7192a51 in tensorflow::functor::FillFunctor&amp;lt;Eigen::GpuDevice, float&amp;gt;::operator()(Eigen::GpuDevice const&amp;amp;, Eigen::TensorMap&amp;lt;Eigen::Tensor&amp;lt;float, 1, 1, long&amp;gt;, 16, Eigen::MakePointer&amp;gt;, Eigen::TensorMap&amp;lt;Eigen::TensorFixedSize&amp;lt;float const, Eigen::Sizes&amp;lt;&amp;gt;, 1, long&amp;gt;, 16, Eigen::MakePointer&amp;gt;) () from /home/casey/.pyenv/versions/tf-test/lib/python3.10/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The fix turned out to be adding that &lt;code&gt;export HSA_OVERRIDE_GFX_VERSION=11.0.0&lt;/code&gt; line.  I learned that this is needed from &lt;a href=&#34;https://are-we-gfx1100-yet.github.io/post/automatic/#launch&#34;&gt;a blog post&lt;/a&gt; about running Stable Diffusion on the 7900 XTX.  The author of that writes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;HSA_OVERRIDE_GFX_VERSION&lt;/code&gt; defaults to 10.3.0 and will fail our gfx1100 if we don&amp;rsquo;t set it explicitly&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There are a lot of other good tips and troubleshooting info for running PyTorch and other ML libraries on the 7900 XTX - I highly suggest checking it out if you run into any other problems.&lt;/p&gt;
&lt;p&gt;That was the final piece I needed.  I am now able to use full GPU-accelerated TensorFlow with my 7900 XTX GPU:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; import tensorflow as tf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:27.588883: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  SSE3 SSE4.1 SSE4.2 AVX AVX2 AVX512F AVX512_VNNI AVX512_BF16 FMA
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:27.654869: I tensorflow/core/util/port.cc:104] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt; tf.add(tf.ones([2,2]), tf.ones([2,2])).numpy()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:32.378116: I tensorflow/compiler/xla/stream_executor/rocm/rocm_gpu_executor.cc:843] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:32.378152: I tensorflow/compiler/xla/stream_executor/rocm/rocm_gpu_executor.cc:843] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:32.385399: I tensorflow/core/common_runtime/gpu/gpu_device.cc:2006] Ignoring visible gpu device (device: 1, name: AMD Radeon Graphics, pci bus id: 0000:1a:00.0) with core count: 1. The minimum required count is 8. You can adjust this requirement with the env var TF_MIN_GPU_MULTIPROCESSOR_COUNT.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:32.385608: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  SSE3 SSE4.1 SSE4.2 AVX AVX2 AVX512F AVX512_VNNI AVX512_BF16 FMA
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:32.386372: I tensorflow/compiler/xla/stream_executor/rocm/rocm_gpu_executor.cc:843] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:32.386509: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1613] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 23986 MB memory:  -&amp;gt; device: 0, name: Radeon RX 7900 XTX, pci bus id: 0000:03:00.0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:32.506584: I tensorflow/core/common_runtime/gpu_fusion_pass.cc:507] ROCm Fusion is enabled.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2023-06-09 17:05:37.839580: I tensorflow/core/common_runtime/gpu_fusion_pass.cc:507] ROCm Fusion is enabled.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;array([[2., 2.],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       [2., 2.]], dtype=float32)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;limiting-tensorflow-memory-usage&#34;&gt;Limiting TensorFlow Memory Usage&lt;/h2&gt;
&lt;p&gt;One final thing I ran into when testing out TensorFlow on the 7900 XTX was that my desktop was basically unusable while TensorFlow was running.  Clicks took many seconds to go through, windows would freeze, etc.&lt;/p&gt;
&lt;p&gt;This is because TensorFlow allocates all of the GPU&amp;rsquo;s memory by default, leaving nothing for the desktop environment and any other apps to use.&lt;/p&gt;
&lt;p&gt;Luckily, the fix is easy.  Just add this Python code to the start of the script/notebook before any other TensorFlow code is run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-py&#34; data-lang=&#34;py&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; tensorflow &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; tf
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gpus &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;experimental&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;list_physical_devices(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GPU&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; gpus:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; gpu &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; gpus:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      tf&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;experimental&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;set_memory_growth(gpu, &lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RuntimeError&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(e)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is from &lt;a href=&#34;https://stackoverflow.com/questions/70782399/tensorflow-is-it-normal-that-my-gpu-is-using-all-its-memory-but-is-not-under-fu&#34;&gt;a StackOverflow answer&lt;/a&gt; that Mushoz &lt;a href=&#34;https://github.com/RadeonOpenCompute/ROCm/issues/1880#issuecomment-1550604957&#34;&gt;linked&lt;/a&gt; in that same Github issue from before.  Huge thanks again to him for all the help getting this working!&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This whole process was quite difficult.  It took me more than a full day of effort.&lt;/p&gt;
&lt;p&gt;I think/hope that this will be getting much better soon.  Once the TensorFlow ROCm fork gains true ROCm 5.5 or better yet 5.6 support, none of that custom build process should be necessary and should be installable directly from a Python package registry.&lt;/p&gt;
&lt;p&gt;That being said, machine learning on modern AMD hardware is very much possible.  Once the setup is out of the way, TensorFlow seems to be working just like normal and running existing code with no modifications needed.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Fix for Broken Boot After Failed amdgpu-dkim Install</title>
      <link>https://cprimozic.net/notes/posts/amdgpu-dkim-broken-boot-fix/</link>
      <pubDate>Thu, 08 Jun 2023 17:35:41 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/amdgpu-dkim-broken-boot-fix/</guid>
      <description>&lt;p&gt;I &lt;a href=&#34;https://cprimozic.net/notes/posts/upgrading-5700xt-to-7900xtx/&#34;&gt;recently upgraded&lt;/a&gt; to the 7900 XTX GPU which was a totally issue-free experience.  Then today, I tried to install &lt;a href=&#34;https://docs.amd.com/category/ROCm_v5.5&#34;&gt;AMD ROCm&lt;/a&gt; so I could try out &lt;a href=&#34;https://github.com/ROCmSoftwarePlatform/tensorflow-upstream&#34;&gt;AMD&amp;rsquo;s TensorFlow fork&lt;/a&gt; that works with AMD GPUs.&lt;/p&gt;
&lt;p&gt;I ran into a lot of issues with this that resulted in my computer not being able to boot for a while.  I eventually figured it out, but it was quite a struggle.&lt;/p&gt;
&lt;p&gt;It started after I downloaded and ran &lt;a href=&#34;https://docs.amd.com/bundle/ROCm-Installation-Guide-v5.5/page/How_to_Install_ROCm.html&#34;&gt;&lt;code&gt;amdgpu-install&lt;/code&gt;&lt;/a&gt; - AMD&amp;rsquo;s tool for installing drivers and other software for use with their hardware.&lt;/p&gt;</description>
      <content>&lt;p&gt;I &lt;a href=&#34;https://cprimozic.net/notes/posts/upgrading-5700xt-to-7900xtx/&#34;&gt;recently upgraded&lt;/a&gt; to the 7900 XTX GPU which was a totally issue-free experience.  Then today, I tried to install &lt;a href=&#34;https://docs.amd.com/category/ROCm_v5.5&#34;&gt;AMD ROCm&lt;/a&gt; so I could try out &lt;a href=&#34;https://github.com/ROCmSoftwarePlatform/tensorflow-upstream&#34;&gt;AMD&amp;rsquo;s TensorFlow fork&lt;/a&gt; that works with AMD GPUs.&lt;/p&gt;
&lt;p&gt;I ran into a lot of issues with this that resulted in my computer not being able to boot for a while.  I eventually figured it out, but it was quite a struggle.&lt;/p&gt;
&lt;p&gt;It started after I downloaded and ran &lt;a href=&#34;https://docs.amd.com/bundle/ROCm-Installation-Guide-v5.5/page/How_to_Install_ROCm.html&#34;&gt;&lt;code&gt;amdgpu-install&lt;/code&gt;&lt;/a&gt; - AMD&amp;rsquo;s tool for installing drivers and other software for use with their hardware.&lt;/p&gt;
&lt;p&gt;I ran a variety of different commands with that - &lt;code&gt;sudo amdgpu-install --usecase=rocm&lt;/code&gt;, &lt;code&gt;sudo amdgpu-install --uninstall&lt;/code&gt;, &lt;code&gt;sudo amdgpu-install --usecase=graphics,rocm&lt;/code&gt; through different stages of debugging stuff.&lt;/p&gt;
&lt;p&gt;The install itself failed because the kernel version needed by the &lt;code&gt;amdgpu-dkim&lt;/code&gt; component of ROCm (5.x) was different than my Kernel version (6.3) so the module build failed.  &lt;code&gt;amdgpu-dkim&lt;/code&gt; is a kernel module for amdgpu, and I didn&amp;rsquo;t and still don&amp;rsquo;t really understand how or if it differs from the &lt;code&gt;amdgpu&lt;/code&gt; kernel module that comes built-in to the kernel.&lt;/p&gt;
&lt;h2 id=&#34;symptoms&#34;&gt;Symptoms&lt;/h2&gt;
&lt;p&gt;At some point, I rebooted my computer.  When I tried to reboot, the boot hung at the &lt;code&gt;dmesg&lt;/code&gt; output which is displayed before my desktop environment pops up.  I looked into a few red herring errors in the logs that turned out to have nothing to do with the failure to boot.&lt;/p&gt;
&lt;p&gt;I eventually figured out a way to get the boot to work: Pressing the &amp;ldquo;e&amp;rdquo; key on the grub menu option and adding &lt;code&gt;nomodeset&lt;/code&gt; to the list of boot args.&lt;/p&gt;
&lt;p&gt;Although it did boot and most things worked alright, it was clear that nothing was GPU accelerated.  Only one of my three monitors worked, Xorg had high CPU usage since it was clearly not accelerating anything with the GPU, and &lt;code&gt;glxgears&lt;/code&gt; was running with software rasterizer.&lt;/p&gt;
&lt;h2 id=&#34;the-cause&#34;&gt;The Cause&lt;/h2&gt;
&lt;p&gt;At some point, I ran &lt;code&gt;lsmod | grep gpu&lt;/code&gt; to try to figure out if maybe there was some weird alternate kernel module running that was dropped by &lt;code&gt;amdgpu-install&lt;/code&gt; which was conflicting with the kernel&amp;rsquo;s built-in one.&lt;/p&gt;
&lt;p&gt;However, what I saw was that there were &lt;em&gt;no&lt;/em&gt; kernel modules at all for &lt;code&gt;amdgpu&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;After a good bit of googling, I found a &lt;a href=&#34;https://github.com/KeenS/KeenS.github.io/blob/5004f414f38c12c5d05f9d4c191c63232ef7f99b/content/post/Ubuntudeamdgpunodoraibainsuto_runishippaishitaatoGPUgatsukaenakunattatokinotaishohou.md?plain=1#L14&#34;&gt;blog post written in Japanese&lt;/a&gt; which talks about this exact situation.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It turns out that when the &lt;code&gt;amdgpu-dkim&lt;/code&gt; kernel module build fails, a file &lt;code&gt;/etc/modprobe.d/blacklist-amdgpu.conf&lt;/code&gt; will get created.  This results in the &lt;code&gt;amdgpu&lt;/code&gt; kernel module getting forced to not load during boot and results in the boot failing (unless the &lt;code&gt;nomodeset&lt;/code&gt; boot param is set).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After deleting that file, the computer booted normally.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve given up on getting ROCm working for now, but might give it another go in the future.  I have a hope that it will maybe work without installing the kernel module which caused all of these issues, but we&amp;rsquo;ll see!&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Upgrading from a 5700 XT to 7900 XTX on Linux</title>
      <link>https://cprimozic.net/notes/posts/upgrading-5700xt-to-7900xtx/</link>
      <pubDate>Wed, 07 Jun 2023 22:06:45 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/upgrading-5700xt-to-7900xtx/</guid>
      <description>&lt;p&gt;Just today, I switched to the 7900 XTX GPU. I mostly just wanted an upgrade, but I also secretly hoped it would fix a lot of the weird GPU-related issues I&amp;rsquo;ve had over the past years.&lt;/p&gt;
&lt;p&gt;The 5700 XT is a rather buggy GPU as far as I can tell - especially on Linux which is my only OS on my desktop. I&amp;rsquo;ve run into &lt;a href=&#34;https://gitlab.freedesktop.org/drm/amd/-/issues/2173#note_1595510&#34;&gt;multiple&lt;/a&gt; &lt;a href=&#34;https://gitlab.freedesktop.org/drm/amd/-/issues/1915#note_1597223&#34;&gt;bugs&lt;/a&gt; with drivers and other mysterious green-screen crashes:&lt;/p&gt;</description>
      <content>&lt;p&gt;Just today, I switched to the 7900 XTX GPU. I mostly just wanted an upgrade, but I also secretly hoped it would fix a lot of the weird GPU-related issues I&amp;rsquo;ve had over the past years.&lt;/p&gt;
&lt;p&gt;The 5700 XT is a rather buggy GPU as far as I can tell - especially on Linux which is my only OS on my desktop. I&amp;rsquo;ve run into &lt;a href=&#34;https://gitlab.freedesktop.org/drm/amd/-/issues/2173#note_1595510&#34;&gt;multiple&lt;/a&gt; &lt;a href=&#34;https://gitlab.freedesktop.org/drm/amd/-/issues/1915#note_1597223&#34;&gt;bugs&lt;/a&gt; with drivers and other mysterious green-screen crashes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/azq.jpg&#34; alt=&#34;Photo of a computer with three monitors.  Two of the monitors are entirely green, and the rightmost monitor is black.  The monitors are on a black desk, there’s a window with the blinds closed behind it, and there are some art prints on the wall along with one on the desk.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I opted to go for another AMD card rather than switch to NVIDIA despite the crashes. I had decent reason to believe that the crashes were mostly limited to teh 5700 family cards, and I hoped that the 9000 series would be safe. The card is quite close in performance to the comparable NVIDIA card, but a couple hundred dollars cheaper.&lt;/p&gt;
&lt;h2 id=&#34;results&#34;&gt;Results&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;This turned out to be the easiest &lt;del&gt;and most successful&lt;/del&gt; GPU upgrades I&amp;rsquo;ve ever done. &lt;del&gt;No issues whatsoever so far.&lt;/del&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;del&gt;There were zero issues through the whole thing. I actually replaced my computer&amp;rsquo;s PSU as well with a 1000 watt unit, and even with that it all went perfectly.&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;There were &lt;em&gt;zero&lt;/em&gt; software changes I needed to do. I rebooted it after installing the new card and everything worked perfectly. No driver issues at all, &lt;del&gt;no crashes so far&lt;/del&gt;, 10/10.&lt;/p&gt;
&lt;p&gt;My &lt;a href=&#34;https://github.com/clbr/radeontop&#34;&gt;radeontop&lt;/a&gt; doesn&amp;rsquo;t know what model the card is, but it works just fine for measuring its utilization.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/b6i.png&#34; alt=&#34;Screenshot of radeontop output in a terminal for 7900 XTX GPU.  Shows the card name as UNKNOWN_CHIP bus 03 with some bars, utilization percentages, and values for things like Graphics Pipe, Vertex Group &amp;#43; Tesselator, Texture Addresser, etc.&#34;&gt;&lt;/p&gt;
&lt;p&gt;So yeah - here&amp;rsquo;s to hoping the driver bugs and crashes don&amp;rsquo;t come back at some point in the future, but I&amp;rsquo;m very happy with everything so far.&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;EDIT 2023-06-18:&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been getting blackscreen crashes and kernel panics. They mostly tend to happen when I&amp;rsquo;m AFK, but still very annoying and an issue for sure.&lt;/p&gt;
&lt;p&gt;One is a very specific issue that seems to happen only if you have &amp;gt;=2 monitors plugged in that have significant differences between them in frame rate and/or resolution: &lt;a href=&#34;https://gitlab.freedesktop.org/drm/amd/-/issues/2609&#34;&gt;https://gitlab.freedesktop.org/drm/amd/-/issues/2609&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The black-screen no-log full computer reboots are still happening as well. No idea what&amp;rsquo;s causing them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;EDIT 2023-08-15:&lt;/p&gt;
&lt;p&gt;Well, I did a few &lt;code&gt;sudo apt full-upgrade&lt;/code&gt;s and reboots later, and&amp;hellip; my crashes just stopped!&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t know what happened, but for the past ~month I&amp;rsquo;ve not had a single driver crash even with heavy gaming in an 85 degree Fahrenheit apartment.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
    </item>
    
    <item>
      <title>My Thoughts on the uPlot Charting Library</title>
      <link>https://cprimozic.net/notes/posts/my-thoughts-on-the-uplot-charting-library/</link>
      <pubDate>Tue, 06 Jun 2023 11:50:23 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/my-thoughts-on-the-uplot-charting-library/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;uPlot is a Spartan charting library that focuses intensely on minimalism and performance. It feels very much like a tool made for hackers, and it lacks many of the features and embellishments of fully-featured charting libraries.&lt;/p&gt;
&lt;p&gt;The main downside is that it has quite terrible docs and sometimes has confusing APIs&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m personally a big fan of its aesthetic and design goals, and I will probably be sticking with it as my primary charting library for the web for the forseeable future.&lt;/p&gt;</description>
      <content>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;uPlot is a Spartan charting library that focuses intensely on minimalism and performance. It feels very much like a tool made for hackers, and it lacks many of the features and embellishments of fully-featured charting libraries.&lt;/p&gt;
&lt;p&gt;The main downside is that it has quite terrible docs and sometimes has confusing APIs&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m personally a big fan of its aesthetic and design goals, and I will probably be sticking with it as my primary charting library for the web for the forseeable future.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A lot of my projects end up needing to render some kind of chart in the browser, often using dynamic data. Over the years, I&amp;rsquo;ve explored a range of different charting libraries. I started off using &lt;a href=&#34;https://www.highcharts.com/&#34;&gt;Highcharts&lt;/a&gt;, then &lt;a href=&#34;https://echarts.apache.org/&#34;&gt;ECharts&lt;/a&gt; was my favorite for a few years, and most recently my go-to choice is &lt;a href=&#34;https://github.com/leeoniya/uPlot&#34;&gt;uPlot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;uPlot self-describes as &amp;ldquo;A small, fast chart for time series, lines, areas, ohlc &amp;amp; bars&amp;rdquo;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s certainly a very minimal charting library compared to complex offerings like ECharts, but it more than makes up for that. Its bundle size is under 50KB for one which is really impressive. Compare that to the ~1MB bundle size for ECharts.&lt;/p&gt;
&lt;p&gt;That being said, I can&amp;rsquo;t think of a time I&amp;rsquo;ve hit a wall with uPlot because it was lacking some feature I needed. I feel that it covers &amp;gt;95% of the surface area for common and uncommon charting needs for the chart types it supports.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also intensely focused on performance. It renders to a canvas using WebGL which makes it possible to scale way further than libraries like D3 - which render to SVG - can ever reach.&lt;/p&gt;
&lt;p&gt;uPlot is optimized to both have an extremely fast initial render as well as scale to supporting huge amounts of data points at 60FPS+. It is seriously fast - here&amp;rsquo;s one of their demos that effortlessly renders millions of data points: &lt;a href=&#34;https://leeoniya.github.io/uPlot/bench/uPlot-10M.html&#34;&gt;https://leeoniya.github.io/uPlot/bench/uPlot-10M.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Its UI design is quite minimalistic, and that&amp;rsquo;s certainly their goal. The library explicitly avoids things like fancy animations when rendering things in or design flourishes in favor of minimizing latency and responsiveness. Here&amp;rsquo;s an example of the kind of line plot you will get out of it with more or less default settings for things:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/b5s.png&#34; alt=&#34;A screenshot of a &#34;&gt;&lt;/p&gt;
&lt;p&gt;Personally, I&amp;rsquo;m a big fan of its aesthetic. It fits well with a lot of the kinds of web apps I end up building, and there&amp;rsquo;s very little work necessary configuring styles or theming. For comparison, bigger libraries like Highcharts or ECharts often require me to set broader theming settings in order to make the plots look decent on a dark background.&lt;/p&gt;
&lt;p&gt;With uPlot, there&amp;rsquo;s simply less things to style, so I end up only really having to define series colors - if even that.&lt;/p&gt;
&lt;h2 id=&#34;drawbacks&#34;&gt;Drawbacks&lt;/h2&gt;
&lt;p&gt;The main downside that uPlot has is that &lt;strong&gt;the docs are not good at all&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Pretty much the only docs that actually exist consist of &lt;a href=&#34;https://github.com/leeoniya/uPlot/tree/master/docs&#34;&gt;one small Markdown doc&lt;/a&gt;. Other than that, you have to rely on reverse engineering the examples on their website or trying to figure things out based on the TypeScript type definitions (which are quite complete to be fair).&lt;/p&gt;
&lt;p&gt;This is a real annoyance, though, and probably makes the library very unattractive to certain people. Setting up a basic chart is unsurprisingly very simple, but as soon as you want to do something like add a custom label formatter, tweak the tick spacing behavior, or use a non-standard line drawing mode, it&amp;rsquo;s a slog of finding an applicable example and reverse engineering it every time.&lt;/p&gt;
&lt;p&gt;Compare that to the &lt;a href=&#34;https://echarts.apache.org/en/option.html#title&#34;&gt;ECharts Docs&lt;/a&gt;. They&amp;rsquo;re extremely complete, very easy to search + browse, and have examples and codepens for pretty much every possible use case.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As I&amp;rsquo;ve said before, I really like uPlot and I use it all the time. It&amp;rsquo;s the first choice I reach for in all my new projects, and I&amp;rsquo;m largely very happy with it. For the kinds of things I build, I have no need of the vast majority of the features that bigger libraries provide, and I&amp;rsquo;m happy to pay the tradeoff in exchange for the extremely tiny bundle size and great performance.&lt;/p&gt;
&lt;p&gt;I really wish the docs were better, though. I&amp;rsquo;ve burned many hours digging through examples and trying to cobble together functionality from &amp;ldquo;View Source&amp;rdquo; in Chrome, and it feels like a big waste of time.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Using a Ramdisk for Rust Dev</title>
      <link>https://cprimozic.net/notes/posts/using-a-ramdisk-for-rust-dev/</link>
      <pubDate>Sun, 04 Jun 2023 00:43:49 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/using-a-ramdisk-for-rust-dev/</guid>
      <description>&lt;p&gt;The main Rust workspace for my job at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt; is very large.  It has several thousand dependencies, does copious compile time codegen from gRPC protobuf definitions, and makes extensive use of macros from crates like &lt;code&gt;serde&lt;/code&gt;, &lt;a href=&#34;https://docs.rs/async-stream/latest/async_stream/&#34;&gt;&lt;code&gt;async-stream&lt;/code&gt;&lt;/a&gt;, and many others.&lt;/p&gt;
&lt;p&gt;While it&amp;rsquo;s really convenient having all of our code in one place, this results in a lot of work being done by the Rust compiler as well as &lt;code&gt;rust-analyzer&lt;/code&gt; during normal development.  For the most part, this isn&amp;rsquo;t too much of an issue.&lt;/p&gt;</description>
      <content>&lt;p&gt;The main Rust workspace for my job at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt; is very large.  It has several thousand dependencies, does copious compile time codegen from gRPC protobuf definitions, and makes extensive use of macros from crates like &lt;code&gt;serde&lt;/code&gt;, &lt;a href=&#34;https://docs.rs/async-stream/latest/async_stream/&#34;&gt;&lt;code&gt;async-stream&lt;/code&gt;&lt;/a&gt;, and many others.&lt;/p&gt;
&lt;p&gt;While it&amp;rsquo;s really convenient having all of our code in one place, this results in a lot of work being done by the Rust compiler as well as &lt;code&gt;rust-analyzer&lt;/code&gt; during normal development.  For the most part, this isn&amp;rsquo;t too much of an issue.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m lucky to have very good hardware for the computer I develop on, but even so compile times and editor features like go to definition can be quite slow at times.&lt;/p&gt;
&lt;p&gt;Another thing I worry about is wear on my SSD from the large amount of reads and writes that occur.  The generated intermediate compilation artifacts and output binaries are very large.  Just opening the project up in VS code and saving a file results in ~2.5GB of writes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/b52.png&#34; alt=&#34;Screenshot of output from the btm system monitoring CLI tool.  Shows the current resource usage from several different processes including VS code, rust-analyzer, and cargo.  Shows 2.6GB of disk writes for rust-analyzer.&#34;&gt;&lt;/p&gt;
&lt;p&gt;I decided to try out using a Ramdisk to speed things up and maybe reduce the amount of disk activity incurred during Rust development.  Luckily, there&amp;rsquo;s a tool that exists which makes it easy called &lt;a href=&#34;https://github.com/PauMAVA/cargo-ramdisk&#34;&gt;&lt;code&gt;cargo-ramdisk&lt;/code&gt;&lt;/a&gt;.  Installing it was as simple as running &lt;code&gt;cargo install cargo-ramdisk&lt;/code&gt;, and activating it for a project is as easy as running &lt;code&gt;cargo ramdisk mount&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;results&#34;&gt;Results&lt;/h2&gt;
&lt;p&gt;On the plus side, &lt;code&gt;cargo-ramdisk&lt;/code&gt; did indeed work to pretty much eliminate disk reads and writes as a result of compilation and development.  Unfortunately, that was pretty much the only advantage for my setup.&lt;/p&gt;
&lt;p&gt;There was very minimal reduction in compile time - on the order of a few percent even when doing a full clean &lt;code&gt;cargo check&lt;/code&gt; of the project.  I do have a very fast SSD, so this may be different for other people.&lt;/p&gt;
&lt;p&gt;Another downside is that the build cache is lost every time I restart my computer, so I end up having to wait for all the dependencies to re-compile from scratch whenever I start developing again after a reboot.&lt;/p&gt;
&lt;p&gt;The final issue I ran into was the ramdisk getting so big that my computer starts to run out of memory.  This only happened after having the project open for a few consecutive workdays, but it did end up being a problem a few times.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;After trying it out for a couple of weeks, I ended up dropping the ramdisk from my development workflow.  For my specific case, it created more hassle than it solved.  Maybe I&amp;rsquo;ll regret that, though, if my SSD fails in a year or two!&lt;/p&gt;
&lt;p&gt;If you have a less-than-amazing SSD and are struggling with high compile times or &lt;code&gt;rust-analyzer&lt;/code&gt; functionality, &lt;code&gt;cargo-ramdisk&lt;/code&gt; might be worth checking out.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Rust Tonic Request/Response Size Limit Footgun</title>
      <link>https://cprimozic.net/notes/posts/rust-tonic-request-response-size-limits/</link>
      <pubDate>Sat, 03 Jun 2023 18:26:33 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/rust-tonic-request-response-size-limits/</guid>
      <description>&lt;p&gt;I recently encountered a bug in one of our services at my job at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt;. Our service is written in Rust and connects to &lt;a href=&#34;https://cloud.google.com/pubsub&#34;&gt;GCP PubSub&lt;/a&gt; via its gRPC interface.&lt;/p&gt;
&lt;p&gt;We were running into errors in our logs like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Error, message length too large: found 5360866 bytes, the limit is: 4194304 bytes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The service in question had been running for over 2 years without seeing this issue before, and the message size limits shown are smaller than the PubSub message size cap of 10MB.&lt;/p&gt;</description>
      <content>&lt;p&gt;I recently encountered a bug in one of our services at my job at &lt;a href=&#34;https://osmos.io/&#34;&gt;Osmos&lt;/a&gt;. Our service is written in Rust and connects to &lt;a href=&#34;https://cloud.google.com/pubsub&#34;&gt;GCP PubSub&lt;/a&gt; via its gRPC interface.&lt;/p&gt;
&lt;p&gt;We were running into errors in our logs like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-txt&#34; data-lang=&#34;txt&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Error, message length too large: found 5360866 bytes, the limit is: 4194304 bytes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The service in question had been running for over 2 years without seeing this issue before, and the message size limits shown are smaller than the PubSub message size cap of 10MB.&lt;/p&gt;
&lt;h2 id=&#34;root-cause&#34;&gt;Root Cause&lt;/h2&gt;
&lt;p&gt;After a good bit of investigation, we discovered that the error message was actually coming from &lt;a href=&#34;https://github.com/hyperium/tonic/blob/1934825ff52bff26bb88b709aee9ac73d3ea51c0/tonic/src/codec/decode.rs#L184&#34;&gt;inside &lt;code&gt;tonic&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tonic&lt;/code&gt; is a popular Rust gRPC client and server. We use it extensively, including for the GCP PubSub client.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It turns out that we&amp;rsquo;d recently upgraded to &lt;code&gt;tonic&lt;/code&gt; version 0.9. This release included a breaking change that adds size limits of 4MB by default when decoding messages.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is what was causing the bug. As far as I can tell, these limits are in place to prevent DOS attacks on gRPC servers, so it&amp;rsquo;s probably a good idea to have them. Before, the limits were either very high or nonexistent, so we never ran into this issue in the past.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tonic&lt;/code&gt; is still on major version 0, so as per semver, minor version bumps can introduce breaking changes. &lt;code&gt;tonic&lt;/code&gt; did also include this &lt;a href=&#34;https://github.com/hyperium/tonic/blob/master/CHANGELOG.md#v090-2023-03-31&#34;&gt;in their changelog&lt;/a&gt;, so they didn&amp;rsquo;t do anything wrong in this case in my opinion.&lt;/p&gt;
&lt;p&gt;That being said, it was a very unexpected break and I figured I&amp;rsquo;d write this up to maybe show up in search results for other people hitting this issue.&lt;/p&gt;
&lt;h2 id=&#34;the-fix&#34;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;The fix was simple - just specify a higher limits for message decoding and encoding:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SubscriberClient::new(auth_service)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .accept_compressed(CompressionEncoding::Gzip)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// added these two following lines
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .max_decoding_message_size(&lt;span style=&#34;color:#ae81ff&#34;&gt;256&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  .max_encoding_message_size(&lt;span style=&#34;color:#ae81ff&#34;&gt;256&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1024&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This bug actually made me realize that things have gotten a lot more stable in the Rust async ecosystem since I started using it extensively ~3 years ago.&lt;/p&gt;
&lt;p&gt;There used to be lots of showstopping bugs, annoying incompatibilities, and stuff like that. Now, things are stable to the point where bugs like this are out of the ordinary - and this bug wasn&amp;rsquo;t even technically &lt;code&gt;tonic&lt;/code&gt;&amp;rsquo;s fault.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>My Self-Hosted Ngrok Alternative</title>
      <link>https://cprimozic.net/notes/posts/self-hosted-ngrok-alternative/</link>
      <pubDate>Sat, 03 Jun 2023 15:03:59 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/self-hosted-ngrok-alternative/</guid>
      <description>&lt;p&gt;I often need to expose some service running locally on my computer to the public internet for some reason or another.  Demoing a website, exposing an API, giving someone access to download some local files, stuff like that.&lt;/p&gt;
&lt;p&gt;The popular solution for this is tools like &lt;a href=&#34;https://ngrok.com&#34;&gt;ngrok&lt;/a&gt;.  You download their CLI application to your computer, specify a port, and your local service is available at a URL like &lt;code&gt;https://a78f837.ngrok.io/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The downside of this is that there are limits for the free versions of these services.  You&amp;rsquo;ll get limits on session duration, get a random subdomain every time, have network speed limits, or other stuff.  You can pay for them as well to make most of these limits go away, of course, and that&amp;rsquo;s what I used to do with an alternative service called &amp;lt;tunnelto.dev&amp;gt;.&lt;/p&gt;</description>
      <content>&lt;p&gt;I often need to expose some service running locally on my computer to the public internet for some reason or another.  Demoing a website, exposing an API, giving someone access to download some local files, stuff like that.&lt;/p&gt;
&lt;p&gt;The popular solution for this is tools like &lt;a href=&#34;https://ngrok.com&#34;&gt;ngrok&lt;/a&gt;.  You download their CLI application to your computer, specify a port, and your local service is available at a URL like &lt;code&gt;https://a78f837.ngrok.io/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The downside of this is that there are limits for the free versions of these services.  You&amp;rsquo;ll get limits on session duration, get a random subdomain every time, have network speed limits, or other stuff.  You can pay for them as well to make most of these limits go away, of course, and that&amp;rsquo;s what I used to do with an alternative service called &amp;lt;tunnelto.dev&amp;gt;.&lt;/p&gt;
&lt;p&gt;However, I ended up encountering &lt;a href=&#34;https://cprimozic.net/notes/posts/tunnelto-woes/&#34;&gt;many problems&lt;/a&gt; with the quality of their service, and I ended up giving up on that as well.&lt;/p&gt;
&lt;h2 id=&#34;self-hosted-alternative&#34;&gt;Self-Hosted Alternative&lt;/h2&gt;
&lt;p&gt;Since the core concept of these services seems so simple, I figured it wouldn&amp;rsquo;t be too hard to self host my own.&lt;/p&gt;
&lt;p&gt;My setup uses a tool called &lt;a href=&#34;https://github.com/fatedier/frp&#34;&gt;&lt;code&gt;frp&lt;/code&gt;&lt;/a&gt;.  &lt;code&gt;frp&lt;/code&gt; described as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Exactly what I was looking for.&lt;/p&gt;
&lt;h3 id=&#34;server-configuration&#34;&gt;Server Configuration&lt;/h3&gt;
&lt;p&gt;Setting it up turned out to be pretty simple.  I have a dedicated server on which I host &lt;a href=&#34;https://cprimozic.net/blog/my-selfhosted-websites-architecture/&#34;&gt;all my websites and services&lt;/a&gt;, and I set up an instance of frpc there via Docker:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker run -d --name frps &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --restart&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;always &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -p 5100:7000 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -p 5101:7500 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -p 5102:6000 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -p 5103:9888 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -v /opt/conf/frp/frps.ini:/etc/frp/frps.ini &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  snowdreamtech/frps:0.37.1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The required config for that in &lt;code&gt;frps.ini&lt;/code&gt; is very minimal:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;common&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;bind_port&lt;/span&gt; = &lt;span style=&#34;color:#ae81ff&#34;&gt;7000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;vhost_http_port&lt;/span&gt; = &lt;span style=&#34;color:#ae81ff&#34;&gt;9888&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;authentication_method&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt; = &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;secret&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only other part on the server I needed to set up was adding some config to my main NGINX instance to create a reverse proxy to the frps server&amp;rsquo;s port:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;server&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;server_name&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;subdomain-name.yoursite.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;location&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;proxy_set_header&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;X-Forwarded-For&lt;/span&gt; $proxy_add_x_forwarded_for;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;proxy_pass&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;http://localhost:5103/&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# &amp;lt;letsencrypt, QUIC, and other common config&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;client-configuration&#34;&gt;Client Configuration&lt;/h3&gt;
&lt;p&gt;Setting up the client was even easier.  All I had to do was download the &lt;a href=&#34;https://github.com/fatedier/frp/releases&#34;&gt;latest release&lt;/a&gt; of the client portion of &lt;code&gt;frp&lt;/code&gt; called &lt;code&gt;frpc&lt;/code&gt; and create another tiny config file for it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;common&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;server_addr&lt;/span&gt; = &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;address&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;server&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;running&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;`&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;frps&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;`&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;server_port&lt;/span&gt; = &lt;span style=&#34;color:#ae81ff&#34;&gt;5100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;authentication_method&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt; = &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;same&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;secret&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;the&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;server&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;web&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;local_port&lt;/span&gt; = &lt;span style=&#34;color:#ae81ff&#34;&gt;4298&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;custom_domains&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;localhost&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Whenever I want to expose a local service, I edit that config file (which I put at &lt;code&gt;~/frpc.ini&lt;/code&gt;) and set the &lt;code&gt;local_port&lt;/code&gt; to whatever port the service is running on.&lt;/p&gt;
&lt;p&gt;Then, I start &lt;code&gt;frpc&lt;/code&gt; using this command:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;frpc -c ~/frpc.ini
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s all there is to it!  The service will be proxied to the subdomain and available publicly.&lt;/p&gt;
&lt;p&gt;Definitely a bit more complex than just paying ngrok or someone else to do it, but this way you get full control over the server, the subdomain, and the whole setup.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Tunnelto Woes</title>
      <link>https://cprimozic.net/notes/posts/tunnelto-woes/</link>
      <pubDate>Fri, 02 Jun 2023 16:15:46 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/tunnelto-woes/</guid>
      <description>&lt;p&gt;There&amp;rsquo;s a service &lt;a href=&#34;https://tunnelto.dev/&#34;&gt;https://tunnelto.dev/&lt;/a&gt; which I&amp;rsquo;ve used in the past.  It&amp;rsquo;s an &lt;a href=&#34;https://ngrok.com/&#34;&gt;ngrok&lt;/a&gt; alternative for exposing local services publically for things like demos, testing, etc.&lt;/p&gt;
&lt;p&gt;I heard about tunnelto.dev when it was announced &lt;a href=&#34;https://news.ycombinator.com/item?id=23618456&#34;&gt;on Hacker News&lt;/a&gt; and gave it a try along with several others on my team at &lt;a href=&#34;https://osmos.io&#34;&gt;Osmos&lt;/a&gt;.  It looked fresh, was written Rust, and was at least partially &lt;a href=&#34;https://github.com/agrinman/tunnelto&#34;&gt;open source&lt;/a&gt; - all great.  I even signed up for the (very cheap) paid plan which gives custom subdomains and some other stuff.&lt;/p&gt;</description>
      <content>&lt;p&gt;There&amp;rsquo;s a service &lt;a href=&#34;https://tunnelto.dev/&#34;&gt;https://tunnelto.dev/&lt;/a&gt; which I&amp;rsquo;ve used in the past.  It&amp;rsquo;s an &lt;a href=&#34;https://ngrok.com/&#34;&gt;ngrok&lt;/a&gt; alternative for exposing local services publically for things like demos, testing, etc.&lt;/p&gt;
&lt;p&gt;I heard about tunnelto.dev when it was announced &lt;a href=&#34;https://news.ycombinator.com/item?id=23618456&#34;&gt;on Hacker News&lt;/a&gt; and gave it a try along with several others on my team at &lt;a href=&#34;https://osmos.io&#34;&gt;Osmos&lt;/a&gt;.  It looked fresh, was written Rust, and was at least partially &lt;a href=&#34;https://github.com/agrinman/tunnelto&#34;&gt;open source&lt;/a&gt; - all great.  I even signed up for the (very cheap) paid plan which gives custom subdomains and some other stuff.&lt;/p&gt;
&lt;h2 id=&#34;issues&#34;&gt;Issues&lt;/h2&gt;
&lt;p&gt;I really wanted to like this service and use it, but it&amp;rsquo;s been a source of constant issues.  It&amp;rsquo;s a highly unreliable service as far as I can tell, and that hasn&amp;rsquo;t really improved over the past ~3 years we&amp;rsquo;ve been using it.  I kind of feel bad writing this, but I think it&amp;rsquo;s important to say at this point:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&amp;rsquo;re having issues with tunnelto.dev (incredibly slow responses, networking issues, weird jank, etc.) it&amp;rsquo;s probably not you, and tunnelto (or one of tunnelto&amp;rsquo;s hosts) is to blame.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;rsquo;ve emailed the dev behind tunnelto multiple times in the past when I was encountering issues with their service.  I did get a response, saying that they were related to an upstream provider issue.&lt;/p&gt;
&lt;p&gt;I know that tunnelto.dev is deployed on &lt;a href=&#34;https://fly.io/&#34;&gt;fly.io&lt;/a&gt;, and fly.io has been experiencing some &lt;a href=&#34;https://community.fly.io/t/reliability-its-not-great/11253&#34;&gt;bad growing pains&lt;/a&gt; and having a lot of difficulty scaling their service.  I think there&amp;rsquo;s a very good chance a lot of the issues tunnelto is experiencing can be attributed to fly.io, but I have no way of being sure.&lt;/p&gt;
&lt;p&gt;Anyway, the point of this post wasn&amp;rsquo;t to flame tunnelto or fly.io or anything, but more to put some written record that these issues with tunnelto do exist for anyone else that might be running into them.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I&amp;rsquo;ve not used tunnelto myself in over a year, and with this latest round of issues I&amp;rsquo;ve been helping my coworkers debug I think they&amp;rsquo;re all going to be switching off as well.&lt;/p&gt;
&lt;p&gt;As an alternative, I&amp;rsquo;m self-hosting an instance of [frp] on my personal VPS and using that for all my tunneling needs.  It&amp;rsquo;s pretty simple to set up, but definitely more work than an off-the-shelf solution like ngrok or tunnelto.&lt;/p&gt;
&lt;p&gt;Maybe I&amp;rsquo;ll write something up about my setup for that in the future.&lt;/p&gt;
</content>
    </item>
    
    <item>
      <title>Drawing Graphviz Edge Splines</title>
      <link>https://cprimozic.net/notes/posts/graphviz-spline-drawing/</link>
      <pubDate>Wed, 31 May 2023 21:32:17 -0700</pubDate>
      <guid>https://cprimozic.net/notes/posts/graphviz-spline-drawing/</guid>
      <description>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I recently had the need to build a custom renderer for graphs generated by graphviz for &lt;a href=&#34;https://github.com/ameobea/rnn-viz&#34;&gt;a project&lt;/a&gt; visualizing computation graphs for RNNs.&lt;/p&gt;
&lt;p&gt;Graphviz has an output format called &lt;code&gt;plain&lt;/code&gt; and &lt;code&gt;plain-ext&lt;/code&gt; which generates a very simple text-based layout specification for all nodes and edges in the input graph. It&amp;rsquo;s pretty well spec&amp;rsquo;d out in the official docs for the most part: &lt;a href=&#34;https://graphviz.org/docs/outputs/plain/&#34;&gt;https://graphviz.org/docs/outputs/plain/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of what &lt;code&gt;plain-ext&lt;/code&gt; output format looks like for a simple graph:&lt;/p&gt;</description>
      <content>&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;I recently had the need to build a custom renderer for graphs generated by graphviz for &lt;a href=&#34;https://github.com/ameobea/rnn-viz&#34;&gt;a project&lt;/a&gt; visualizing computation graphs for RNNs.&lt;/p&gt;
&lt;p&gt;Graphviz has an output format called &lt;code&gt;plain&lt;/code&gt; and &lt;code&gt;plain-ext&lt;/code&gt; which generates a very simple text-based layout specification for all nodes and edges in the input graph. It&amp;rsquo;s pretty well spec&amp;rsquo;d out in the official docs for the most part: &lt;a href=&#34;https://graphviz.org/docs/outputs/plain/&#34;&gt;https://graphviz.org/docs/outputs/plain/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of what &lt;code&gt;plain-ext&lt;/code&gt; output format looks like for a simple graph:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;graph 1 5.875 8.3333
node output_0 1.5694 0.47222 1.2604 0.5 output_0 solid ellipse black lightgrey
node layer_0_state_0 1.5694 4.7833 2.0049 0.5 layer_0_state_0 solid ellipse black lightgrey
node layer_0_state_1 3.8194 4.7833 2.0049 0.5 layer_0_state_1 solid ellipse black lightgrey
node layer_0_recurrent_0 1.5694 6.5444 2.4812 0.5 layer_0_recurrent_0 solid ellipse black lightgrey
node layer_0_recurrent_1 4.3056 6.5444 2.4812 0.5 layer_0_recurrent_1 solid ellipse black lightgrey
node layer_0_output_1 1.5694 3.0222 2.1989 0.5 layer_0_output_1 solid ellipse black lightgrey
node post_layer_output_0 1.5694 1.7056 2.5119 0.5 post_layer_output_0 solid ellipse black lightgrey
node input_0 4.3056 7.8611 1.1263 0.5 input_0 solid ellipse black lightgrey
edge layer_0_state_0 layer_0_output_1 4 1.5694 4.531 1.5694 4.2394 1.5694 3.7559 1.5694 3.415 -1 1.6504 3.9028 solid black
edge layer_0_state_1 layer_0_recurrent_0 4 3.5129 5.0233 3.1202 5.3307 2.4377 5.8649 1.9949 6.2114 1 2.8542 5.6639 solid black
edge layer_0_recurrent_0 layer_0_state_0 4 1.5694 6.2921 1.5694 6.0005 1.5694 5.517 1.5694 5.1761 1 1.6181 5.6639 solid black
edge layer_0_recurrent_1 layer_0_state_1 4 4.2359 6.2921 4.155 5.9992 4.0208 5.5127 3.9266 5.1715 1 4.1319 5.6639 solid black
edge layer_0_output_1 post_layer_output_0 4 1.5694 2.7684 1.5694 2.5789 1.5694 2.3165 1.5694 2.0999 -1 1.6504 2.3222 solid black
edge post_layer_output_0 output_0 4 1.5694 1.45 1.5694 1.2819 1.5694 1.0584 1.5694 0.8676 1 1.6181 1.0889 solid black
edge input_0 layer_0_recurrent_1 4 4.3056 7.6072 4.3056 7.4178 4.3056 7.1554 4.3056 6.9388 -1 4.3865 7.2444 solid black
stop
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I used graphviz to consume the input graph in native graphviz &lt;a href=&#34;https://graphviz.org/doc/info/lang.html&#34;&gt;dot language&lt;/a&gt; and then generate &lt;code&gt;plain-ext&lt;/code&gt; output text.&lt;/p&gt;
&lt;p&gt;Graphviz is an excellent library and largely does a great job creating good-looking graphs. One of the things it handles doing is drawing complex edges that twist and turn to avoid colliding with nodes or labels:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/b4r.svg&#34; alt=&#34;graphviz-generated graph showing a ~17 nodes connected with ~30 edges.  Some of the edges loop around prominently in order to avoid intersecting the labels and nodes, making multiple turns before finally reaching their destinations.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what the docs page says about the data provided for edge layout:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;n&lt;/code&gt; is the number of control points defining the B-spline forming the edge. This is followed by 2*n numbers giving the x and y coordinates of the control points in order from tail to head.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I had trouble figuring out how to render these edges with &lt;a href=&#34;https://pixijs.com/&#34;&gt;PIXI.JS&lt;/a&gt;, the library I was using to build the visualization.&lt;/p&gt;
&lt;p&gt;I did some research, and it seems that &lt;a href=&#34;https://en.wikipedia.org/wiki/B-spline&#34;&gt;B-splines&lt;/a&gt; a.k.a. Basis Splines are relatives of bezier curves but with more power. They&amp;rsquo;re able to express curves that bezier curves can&amp;rsquo;t create on their own.&lt;/p&gt;
&lt;p&gt;However, I knew that it was possible to render the edge paths generated by graphviz with just plain bezier curves since that&amp;rsquo;s what the generated SVGs were using for their paths.&lt;/p&gt;
&lt;p&gt;I looked for other solutions, and I only really found &lt;a href=&#34;https://stackoverflow.com/questions/53934876/how-to-draw-a-graphviz-spline-in-d3&#34;&gt;one post&lt;/a&gt; on Stack Overflow from someone who was able to render them using &lt;code&gt;curveBasis&lt;/code&gt; from D3. The D3 &lt;a href=&#34;https://github.com/d3/d3-shape/blob/main/src/curve/basis.js&#34;&gt;source code for that function&lt;/a&gt; only really has calls to &lt;code&gt;bezierCurveTo&lt;/code&gt;, but they were doing weird things I didn&amp;rsquo;t understand to compute bezier control points from the spline control points.&lt;/p&gt;
&lt;h2 id=&#34;solution&#34;&gt;Solution&lt;/h2&gt;
&lt;p&gt;After much more research and trial and error, I figured out how to do it. The list of control points generated by graphviz for each edge spline always totaled 3n + 1 which was a pretty big hint. The first point is the start point of the spline and the last is the end point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It turns out that the &amp;ldquo;b-splines&amp;rdquo; used by graphviz here are quite simple and constrained and in fact are just composed out of a bunch of cubic bezier curves connected together end to end.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So, to render the edge path, all I had to do was move to the starting point of the edge, loop through the points in windows of 3, and draw a bezier curve for each set. The starting point of the next curve is the end point of the last one.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the code I ended up with to accomplish that:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;controlPoints&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;graphics&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;moveTo&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;startPoint&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;controlPoints&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;graphics&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;moveTo&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;startPoint&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;startPoint&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p1&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Point&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p2&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Point&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p3&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Point&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;controlPoints&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;p1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;controlPoints&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;p2&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;controlPoints&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;p3&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;controlPoints&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;graphics&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;bezierCurveTo&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p2&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p2&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p3&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p3&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;None of those fancy &amp;ldquo;knot vectors&amp;rdquo; or other crazy b-spline features are needed at all - just plain old cubic bezier curves.&lt;/p&gt;
&lt;p&gt;The results look great and exactly match the SVG generated by graphviz natively!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://i.ameo.link/b4s.jpg&#34; alt=&#34;Screenshot of a graphviz-generated graph rendered using PIXI.JS.  It has roughly the same shape as the graph above, but the nodes are colored red and blue as are the edges.  There is a color scale in the top right corner showing the values corresponding to different colors with dark blue for -1, fading to white at 0, growing to dark red at 1.&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;drawing-arrowheads&#34;&gt;Drawing Arrowheads&lt;/h2&gt;
&lt;p&gt;One additional piece that I wanted was arrowheads. My graphs are directed with lots of loops since they represent the computation graphs of RNNs, so knowing the direction of edges was important.&lt;/p&gt;
&lt;p&gt;I accomplished this by computing the angle of the last spline at its endpoint where it touches the target node and rendering a triangle pointing in that same direction.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the code I wrote to do that:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Computes the direction of the last bezier curve in the spline to determine
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// the arrowhead direction
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;end&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;prev1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dy&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;end&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;prev1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;angle&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;atan2&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;dy&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;dx&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;point1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;end.x&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ArrowheadSize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;cos&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;angle&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; (Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;PI&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ArrowheadHeightRatio&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;end.y&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ArrowheadSize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;sin&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;angle&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; (Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;PI&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ArrowheadHeightRatio&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;point2&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;end.x&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ArrowheadSize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;cos&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;angle&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; (Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;PI&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ArrowheadHeightRatio&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;end.y&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ArrowheadSize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;sin&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;angle&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; (Math.&lt;span style=&#34;color:#a6e22e&#34;&gt;PI&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Conf&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ArrowheadHeightRatio&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;graphics&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;lineStyle&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;color&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;graphics&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;beginFill&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;color&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;graphics&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;drawPolygon&lt;/span&gt;([&lt;span style=&#34;color:#a6e22e&#34;&gt;end&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;end&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;point1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;point1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;point2&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;point2&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;y&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;graphics&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;endFill&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It works well, and the arrowheads look good! Note that config option used to elongate the triangles for the arrowheads a bit so that they are longer since equilateral triangles looked too fat.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;That&amp;rsquo;s all! I hope this ends up helping someone out doing something similar in the future.&lt;/p&gt;
</content>
    </item>
    
  </channel>
</rss>
