<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://fluff.blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fluff.blog/" rel="alternate" type="text/html" /><updated>2026-04-07T05:48:31+00:00</updated><id>https://fluff.blog/feed.xml</id><title type="html">Technical Fluff</title><subtitle>Dan&apos;s notes on software, design, maths or whatever.</subtitle><entry><title type="html">~200$ in, here’s what I think about Claude</title><link href="https://fluff.blog/2026/04/06/about-200-usd-in-heres-what-i-think-about-claude.html" rel="alternate" type="text/html" title="~200$ in, here’s what I think about Claude" /><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>https://fluff.blog/2026/04/06/about-200-usd-in-heres-what-i-think-about-claude</id><content type="html" xml:base="https://fluff.blog/2026/04/06/about-200-usd-in-heres-what-i-think-about-claude.html"><![CDATA[<p>I was first introduced to Claude at work (because I work for a Big Tech Company). Originally, I was pretty sceptical of anything to do with AI, and generally leaned negative on it due to ethical and moral concerns around the conduct of the companies involved.</p>

<p>To this day, I still have my reservations, though my position on IP law as a whole is relatively conflicted as I’m both opposed to its existence but also opposed to how it’s been trampled to hurt artists.</p>

<p>Anyway, that’s a topic for another post. I’m going to need to simmer for longer on that one.</p>

<p>Something strange happened last year. As people around me started seemingly using these systems <em>effectively</em> - something I didn’t know how they could even do - I was interested in learning more about how to use these systems for myself, so that I properly understood how people can evangelise these things so thoroughly, when from my perspective, they were mostly just semi-useful stochastic parrots.</p>

<p>So over the last week, I invested about $200 USD into creating a reference Vulkan renderer for <a href="https://caveygame.com/">Cavey</a>, my voxel game I’m working on for fun. There’s going to be no hyperbole and no glaze in this post; I sunk my own earned money into this thing, and in exchange for that money I expect results.</p>

<p>Let’s see how Claude did on my personal, army-of-one project. Interspersed between the anecdotes, I’ll share my thoughts on various parts of the process.</p>
<h2 id="baby-steps-porting-a-webgpu-renderer-to-vulkan">Baby steps: porting a WebGPU renderer to Vulkan</h2>

<p>If you know me, you know exactly how disillusioned I am about WebGPU. It’s a great <em>idea</em>, and certainly good enough for <em>many</em> projects, but it’s a very slow-moving graphics library with lowest-common-denominator features and a few painfully design-by-committee choices, like the endless annoyance of not using an industry-standard shader format - for strangely secretive reasons?</p>

<p>I had had enough. WebGPU wasn’t going to cut it for me or for this project, so it was time to move on. The obvious choice for my target spec device - the Steam Deck OLED - would be Vulkan, which natively exposes all of the features of that device (including HDR rendering, which I was particularly excited about). It’s more general while still being widely supported, but unfortunately also more complex as it foists more responsibility onto the programmer (with dubious payoff).</p>

<p>To start weaning Cavey off of WebGPU and towards Vulkan, I had built version 3 of Cavey’s high-level rendering abstractions to be <em>completely opaque</em>. Anything built on this renderer wouldn’t know what graphics library it was interfacing with. I had already set up some basic-enough plumbing to get a splash screen rendering under WebGPU, but nothing more:</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/splash-screen.png" alt="Splash screen" /></p>

<p>Under the hood, the frontend API was simple and declarative; it’s a static render-graph-style API, just minimally flexible enough for a renderer that’d be GPU driven. Very much YAGNI-style:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span>
	<span class="n">frontend</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">RenderFrontend</span><span class="o">&lt;</span><span class="n">ChosenBackend</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">window_size</span><span class="p">:</span> <span class="n">PhysicalSize</span><span class="o">&lt;</span><span class="nb">u32</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="k">Self</span><span class="o">&gt;</span> <span class="p">{</span>
	<span class="nd">profiler_span!</span><span class="p">(</span><span class="s">"SplashScreenRenderer::new"</span><span class="p">);</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">g</span> <span class="o">=</span> <span class="nn">RenderGraph</span><span class="p">::</span><span class="nf">empty</span><span class="p">();</span>

	<span class="k">let</span> <span class="n">sync_logo</span> <span class="o">=</span> <span class="nn">SyncSource</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nf">load_splash_logo</span><span class="p">()</span><span class="o">?</span><span class="p">);</span>
	<span class="k">let</span> <span class="n">logo</span> <span class="o">=</span> <span class="n">g</span><span class="nf">.texture</span><span class="p">(</span><span class="nn">Texture</span><span class="p">::</span><span class="n">Uninit</span> <span class="p">{</span>
		<span class="n">label</span><span class="p">:</span> <span class="s">"Splash Screen Logo"</span><span class="nf">.to_string</span><span class="p">(),</span>
		<span class="n">size</span><span class="p">:</span> <span class="nn">TextureSize</span><span class="p">::</span><span class="n">D2</span> <span class="p">{</span> <span class="n">size</span><span class="p">:</span> <span class="k">Self</span><span class="p">::</span><span class="n">SPLASH_SIZE</span><span class="nf">.as_u16vec2</span><span class="p">(),</span> <span class="n">array</span><span class="p">:</span> <span class="mi">0</span> <span class="p">},</span>
		<span class="n">data_type</span><span class="p">:</span> <span class="nn">TextureDataType</span><span class="p">::</span><span class="n">Unorm8x4</span>
	<span class="p">});</span>
	<span class="n">g</span><span class="nf">.act</span><span class="p">(</span><span class="nn">Act</span><span class="p">::</span><span class="n">SyncTexture</span> <span class="p">{</span> <span class="n">texture</span><span class="p">:</span> <span class="n">logo</span><span class="p">,</span> <span class="n">source</span><span class="p">:</span> <span class="n">sync_logo</span> <span class="p">});</span>

	<span class="k">let</span> <span class="n">sync_uniforms</span> <span class="o">=</span> <span class="nn">SyncSource</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="k">Self</span><span class="p">::</span><span class="nf">create_uniforms</span><span class="p">(</span><span class="n">window_size</span><span class="p">));</span>
	<span class="k">let</span> <span class="n">uniforms</span> <span class="o">=</span> <span class="n">g</span><span class="nf">.buffer</span><span class="p">(</span><span class="n">Buffer</span> <span class="p">{</span>
		<span class="n">label</span><span class="p">:</span> <span class="s">"Splash Screen Uniforms"</span><span class="nf">.to_string</span><span class="p">(),</span>
		<span class="n">initial_content</span><span class="p">:</span> <span class="nn">BufferData</span><span class="p">::</span><span class="nn">zeroed</span><span class="p">::</span><span class="o">&lt;</span><span class="n">SplashScreenUniforms</span><span class="o">&gt;</span><span class="p">()</span>
	<span class="p">});</span>
	<span class="n">g</span><span class="nf">.act</span><span class="p">(</span><span class="nn">Act</span><span class="p">::</span><span class="n">SyncBuffer</span> <span class="p">{</span> <span class="n">buffer</span><span class="p">:</span> <span class="n">uniforms</span><span class="p">,</span> <span class="n">source</span><span class="p">:</span> <span class="n">sync_uniforms</span><span class="nf">.clone</span><span class="p">()</span> <span class="p">});</span>

	<span class="k">let</span> <span class="n">surface</span> <span class="o">=</span> <span class="n">g</span><span class="nf">.texture</span><span class="p">(</span><span class="nn">Texture</span><span class="p">::</span><span class="n">Surface</span><span class="p">);</span>
	<span class="n">g</span><span class="nf">.act</span><span class="p">(</span><span class="nn">Act</span><span class="p">::</span><span class="n">Raster</span> <span class="p">{</span>
		<span class="n">label</span><span class="p">:</span> <span class="s">"Splash Screen"</span><span class="nf">.to_string</span><span class="p">(),</span>
		<span class="n">binds</span><span class="p">:</span> <span class="nd">hashmap!</span> <span class="p">{</span>
			<span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">RasterBind</span><span class="p">::</span><span class="n">UniformBuffer</span> <span class="p">{</span> <span class="n">buffer_id</span><span class="p">:</span> <span class="n">uniforms</span> <span class="p">},</span>
			<span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">RasterBind</span><span class="p">::</span><span class="n">SampledTexture</span> <span class="p">{</span> <span class="n">texture_id</span><span class="p">:</span> <span class="n">logo</span> <span class="p">}</span>
		<span class="p">},</span>
		<span class="n">push</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
		<span class="n">vertices</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
		<span class="n">assembly</span><span class="p">:</span> <span class="nn">RasterAssembly</span><span class="p">::</span><span class="n">Triangles</span><span class="p">,</span>
		<span class="n">cull_back</span><span class="p">:</span> <span class="k">false</span><span class="p">,</span>
		<span class="n">shader</span><span class="p">:</span> <span class="nn">RasterShader</span><span class="p">::</span><span class="nf">bundled</span><span class="p">(</span><span class="s">"splash_screen::vertex"</span><span class="p">,</span> <span class="nf">Some</span><span class="p">(</span><span class="s">"splash_screen::fragment"</span><span class="p">)),</span>
		<span class="n">colour</span><span class="p">:</span> <span class="nf">Some</span><span class="p">(</span><span class="n">RasterColour</span> <span class="p">{</span>
			<span class="n">target</span><span class="p">:</span> <span class="n">surface</span><span class="p">,</span>
			<span class="n">blend</span><span class="p">:</span> <span class="nb">None</span>
		<span class="p">}),</span>
		<span class="n">depth</span><span class="p">:</span> <span class="nb">None</span><span class="p">,</span>
		<span class="n">draw</span><span class="p">:</span> <span class="nn">RasterDraw</span><span class="p">::</span><span class="n">FullScreenQuad</span>
	<span class="p">});</span>
	<span class="k">let</span> <span class="n">act_present</span> <span class="o">=</span> <span class="n">g</span><span class="nf">.executable_act</span><span class="p">(</span><span class="nn">Act</span><span class="p">::</span><span class="n">Present</span> <span class="p">{</span> <span class="n">surface</span> <span class="p">});</span>

	<span class="k">let</span> <span class="n">proc</span> <span class="o">=</span> <span class="n">frontend</span><span class="nf">.compile</span><span class="p">(</span><span class="n">g</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
	<span class="nf">Ok</span><span class="p">(</span><span class="k">Self</span> <span class="p">{</span> <span class="n">proc</span><span class="p">,</span> <span class="n">sync_uniforms</span><span class="p">,</span> <span class="n">act_present</span> <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Claude’s job here was to write a second <em>backend</em> for this API that would do <em>exactly</em> the same thing that the WebGPU backend did. This would not be a 1:1 translation however; Claude would have to reason about more than what the WebGPU code provided, for example handling the synchronisation between host and device, managing the swapchain itself, and handling resource state transitions, to name a few things.</p>

<p>It started on the 13th of March with a technical doc, written as a collaboration between myself and Claude on my laptop. I didn’t have a working desk setup as I had only just moved across the Atlantic:</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/migration-doc.png" alt="Migration doc" /></p>

<p>By the 25th of March, I had my desktop in basic working order and could hit the ground running. I put Claude in the driver’s seat and paid a finite amount of attention to what it was doing. My job was to specify what Claude should do and to direct high-level design decisions; I wasn’t particularly interested in specifics of how Claude chose to set up the device or other minutiae - those aren’t the bits worth caring about. Essentially, I was a principal engineer pair programming with a digital intern.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/migration-claude.png" alt="Migration claude" /></p>

<p>The next day, we together had successfully burned through a massive number of the tasks, and by the end of the day, had recreated the splash screen fully, with some back-and-forth to resolve some of the bugs and edge cases that cropped up. All of our plans were documented in Markdown files and we ticked off each set of steps as we completed them.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/migration-burndown.png" alt="Migration burndown" /></p>

<p>For context: this Vulkan transition is a leap I had put off for <em>months</em>, not just because I had to move countries, but because it was a mentally tiring / overwhelming task. I knew the <em>outline</em> of what needed to be done. I could do it if I bothered to, but I didn’t have the executive function to do it when there was always <em>something else</em> to be thinking about.</p>

<p>That said, working it out with Claude made all the difference, because of the way you have to use these bots to get value out of them.</p>

<p>Claude is not boundlessly superintelligent. I’d compare it to an adequately capable university student. If you ask Claude to synthesise whole codebases from nothing, it’ll do a <em>serviceable</em> job, but probably not a <em>good</em> job. Tick the boxes, hand in the assignment, leave it to bitrot.</p>

<p>Whenever I’ve worked with Claude at my day job, I’ve figured out that really, <em>you’re still the software architect, because Claude can’t be.</em> At the end of the day, the latent space of possible code generations is far more populated with bad solutions than good ones as a pure statistical fact (there are more ways to screw up code than there are ways to write it correctly), so your job is to bias the code generation in a direction that lands you in a good part of the latent space, for what your definition of “good code” is.</p>

<p>So, I’ve learned to essentially “code in English” by precisely and comprehensively specifying what the software should do, chunking it down into tiny bite-sized steps, and getting Claude to <em>just do them</em>, with no grand architecture or plan in mind - essentially, doing <a href="https://caseymuratori.com/blog_0015">Semantic Compression</a> but with a robot.</p>

<p>It turns out this approach is good for both humans and for AI. By breaking down the problem into manageable chunks, I could better understand what steps needed to be taken, and Claude could expose areas where I didn’t specify enough at the desired level of detail. For those gaps, I would go out and find lectures on YouTube, or blog posts on the internet, and synthesise a strategy to come back to Claude with and finish the plan.</p>

<p>It’s a kind of synergy where Claude points out the problems, I figure out the solutions, and Claude fixes the problem with my solution.</p>

<p>It worked really well. I got the thing ported in a day. I would happily use Claude for this again.</p>

<h2 id="extending-further-building-a-slang-voxel-ray-tracer">Extending further: building a Slang voxel ray tracer</h2>

<p>I wasn’t about to build out the whole render graph API with no project to apply it to. Instead, I was fully intending to build the railroad as the train was moving.</p>

<p>So, I dove straight into Claude’s main assignment: <em>building out a new reference ray-tracing renderer for my voxel game.</em></p>

<p>I took a similar approach; asymmetric pair programming with comprehensively planned documents prepared upfront. However, I did something slightly different this time; I <em>also</em> got Claude to do a round of research specifically into 64-trees and fast voxel ray tracing, so that I could come up with a good acceleration structure using them. I specifically asked for evidence to back each technique and approach it researched, which yielded good results out of the gate:</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/render_hq_spec.png" alt="Render HQ spec" /></p>

<p>By April Fool’s Day, we had set up a simple ray-traced scene, ready for integrating these ideas into. I didn’t have to recall any of my memorised equations or look up the Ray Tracing books online; Claude is one of the best math equation recallers in the world, even if it struggles to actually do the math. (Just like a uni student, one might say…)</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/ray-traced-scene.png" alt="Ray traced scene" /></p>

<p>The next day, I had it implement a routine for tracing a single 64-bit 4x4x4 chunk, and then had it extend to a whole contiguous world. At this stage, it was still completely separate from the rest of the game, so this terrain wasn’t “real”, but it was good enough to test the ray tracing approaches we had co-designed. Performance was great:</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/PBa2MQ3WkQc?si=MYuW6V8CgwAxDe1s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:opabrm62lmb4hpoyw3t7sehv/app.bsky.feed.post/3mikyaoducs2n" data-bluesky-cid="bafyreidirktjnl5ugvojdwmst3sg4xtfvwnfazrdiq4e5rz3peutqi4siy" data-bluesky-embed-color-mode="system"><p lang="en">Runs at 90fps on Deck, pretty goooood<br /><br /><a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mikyaoducs2n?ref_src=embed">[image or embed]</a></p>&mdash; Daniel P H Fox (<a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv?ref_src=embed">@phfox.net</a>) <a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mikyaoducs2n?ref_src=embed">2 April 2026 at 22:26</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>By the very next day, we had planned and executed on the steps necessary to wire in actual voxel data from the rest of the game. At this point, I identified some latent bugs in the code and struggled a little bit to get Claude to fix them.</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/U_wN9_yTFiE?si=vvF4PfiIAUIAmACH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>Here’s where we talk about Claude’s tendency to go off the rails sometimes. Much like a naïve uni student, it will often try and hack its way around a problem rather than taking the principled path, and oftentimes will run with a completely unjustified diagnosis that can often end up being completely wrong.</p>

<p>That is to say, you should not trust these things to be correct. They’re very productive idiots.</p>

<p>Throughout this process, I have spent a good amount of time intervening in what Claude is doing. I’m quick to interrupt and quick to correct, and if I feel like something has gone far off the tracks, I know how to rewind to an earlier point in the conversation and nuke the code changes.</p>

<p>I’ve often had to remind Claude that we’re allowed to build out the <code class="language-plaintext highlighter-rouge">rengraph</code> API, or that things are meant to happen on the GPU and should not be synced back to the CPU, or that recompiling the graph every frame defeats the point of having a static compiled graph and that it should prefer declarative solutions.</p>

<p>Some of that is wisdom, some of that is taste. Claude needs it to stay on the rails.</p>

<p>This is why I say that you can’t let Claude be completely in the driver’s seat. Left alone, it will make counterproductive choices that don’t fit into a larger vision of the system, especially across chat compaction boundaries or across sessions. Pretty much: enjoy Claude’s memory of what it did while it remembers because it will be gone in an hour’s time.</p>

<h2 id="end-game-a-beautiful-reference-renderer">End game: a beautiful reference renderer</h2>

<p>Anyhow, with the core ray tracing routine set up, it was time to start decorating the voxel scene and working towards something with the graphical fidelity needed to produce reference renders for assisting in future rendering decisions.</p>

<p>On the 4th of April, we started experimenting with porting the old renderer’s UV mapping code to render albedo colours. There were bumps in the road because Claude hadn’t kept track of the different coordinate spaces, so for a while, the UVs were all camera relative:</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/uv-bug.jpg" alt="UV bug" /></p>

<p>The bug was quickly identified and fixed, and not long after, we had incorporated the normal and AO maps alongside albedo to get a more complete-looking scene with very little difficulty.</p>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:opabrm62lmb4hpoyw3t7sehv/app.bsky.feed.post/3mipqg3hoic2k" data-bluesky-cid="bafyreiezqmjd7xe2jjwmcnpisufekgrjvzo76clqqmxuusmhofngyip2ye" data-bluesky-embed-color-mode="system"><p lang="en">Now rendering albedo, normal and combined AO/emissive maps in the ray tracing path! Still no water pass as that&#x27;s more complex to pull off.

Next up: going to start setting up a proper tonemapping pipeline and get some antialiasing in there.<br /><br /><a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mipqg3hoic2k?ref_src=embed">[image or embed]</a></p>&mdash; Daniel P H Fox (<a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv?ref_src=embed">@phfox.net</a>) <a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mipqg3hoic2k?ref_src=embed">4 April 2026 at 19:49</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>I also got Claude to port over the “smart bevels” code that would bend the normals of texels that lie along the exposed edges of blocks. This worked flawlessly first try with no planning needed.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/smart-bevels.png" alt="Smart bevels" /></p>

<p>From there, I turned my attention to tonemapping. I had Claude do a bunch of research into which swapchain format should be used for HDR, and how to query parameters like peak display brightness.</p>

<p>I often find myself having extended conversations with Claude about technical decisions like these. At first, Claude advocated for the use of PQ with physically-based luminance values, which I respected, but ultimately didn’t see much of a point in pursuing as I had less use for declaring absolute nit values for my renderer. Instead, I pushed back and advocated to Claude that the scene should internally be rendered with physically sensible lighting intensities, but that the output doesn’t have to be calibrated for a reference display; it just needs to look good on people’s real-world devices, which meant having a properly calibrated paper-white in the scene.</p>

<p>We ended up settling on scRGB with the Uchimura tonemapping curve applied per-channel for simplicity, parameterised by the properties of the display so that HDR-capable devices like the Steam Deck could punch above paper-white while SDR displays would look reasonably consistent, minus the shine.</p>

<p>Simultaneously, I took the opportunity to tell Claude to implement a jittered accumulation buffer for some antialiasing, as an input to the tonemapping pipeline:</p>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:opabrm62lmb4hpoyw3t7sehv/app.bsky.feed.post/3mirgvfvl3c2o" data-bluesky-cid="bafyreihwfum52nk7xkqueilmpqakoh2y5xbknxuz4544pxel2zqty624fi" data-bluesky-embed-color-mode="system"><p lang="en">Accumulating multiple samples per pixel in the ray traced path now. (Not TAA, no reprojection - just to get cleaner reference stills.)<br /><br /><a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mirgvfvl3c2o?ref_src=embed">[image or embed]</a></p>&mdash; Daniel P H Fox (<a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv?ref_src=embed">@phfox.net</a>) <a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mirgvfvl3c2o?ref_src=embed">5 April 2026 at 12:04</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>Up until this point, it had all been smooth sailing. Now for the biggest challenge so far; building out a physically based lighting model.</p>

<p>My end goal here was not necessarily photorealism, but instead physical plausibility. I wanted to avoid lighting “hacks” as much as possible in my renderer, so that it would be easy to progressively enhance or swap the lighting techniques later down the line without interfering with content authored for older lighting systems. Having a physically plausible renderer would also aid in handing off assets between programs; for example, allowing me to author the PBR textures externally.</p>

<p>The goal then was to implement a Monte Carlo integrator that would sample the whole hemisphere of lighting, combine it with a physically based material to attenuate incoming light rays, then customise it to taste with a variety of stylistic tweaks, most notably a pixelation effect to match the art style of the game.</p>

<p>I started by getting Claude to implement random hemisphere sampling to capture the effect of sky light, plus a sample towards the sun for clear directional shadows. When discussing the physical correctness of this, Claude was able to justify it as a kind of Next-Event Estimation. At this point, we were firmly leaving the realm of full-framerate rendering and moving towards “interactive offline rendering”.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/simple-rt-sunlight.png" alt="Simple RT sunlight" /></p>

<p>Once combined with a physically based material, this already produced some very pleasant effects; for example, by lowering material roughness, you could get nice wet-looking surfaces with smooth specular effects. Claude handled the microfacet equations here; I asked it to review the old PBR implementation and it pointed out some deficiencies that could then be trivially fixed. The results look good to my eyes, but I didn’t inspect quite so carefully.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/shiny-stone.png" alt="Shiny stone" /></p>

<p>At this point, I started hitting my stride with Claude, going through the plan and execute loop at great speed (and expense). I regularly hit the daily limit, and just started spending into extra usage to keep the velocity up. I think it’s worth talking about what happened there.</p>

<p>When it comes to all of these lighting concepts, I’m more than familiar with them. I’ve spent a very long time thinking about lighting models for Cavey, and more generally have been interested in light transport for a long time now; coming up on half a decade.</p>

<p>However, I always found that ray traced lighting techniques quickly escape from the realm of easy intuiting into the realm of abstract maths and more difficult reasoning. You quickly start running into integrals and clever sampling/reuse tricks that ultimately suck a lot of the accessibility out of these lighting methods, except to those who sink a lot of time attempting to implement them and get familiar.</p>

<p>This is where I found Claude really helps. Because Claude has a bunch of knowledge baked into itself, it can easily execute on any lighting technique you know to ask about, and since you can ask Claude clarifying questions, it can also patiently explain the mechanics of how they work. I found this invaluable.</p>

<p>What’s more, since there’s pretty reasonably clear answers on how to build up these kinds of “physically correct” lighting models, Claude got a whole lot better at doing what it was supposed to do first try, so long as I kept it on the rails architecture-wise. The region of latent space associated with malfunctioning PBR equations seems to be smaller than the region associated with badly-structured code.</p>

<p>Back to the results. By sampling the block hit by the hemisphere ray for emissive colour and doing a secondary sunlight check, a single bounce of sunlight could be trivially added, which helped to fill in a lot of missing ambient light. Claude did this on my behalf with no trouble.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/sun-bounce.png" alt="Sun bounce" /></p>

<p>I then told Claude to port over the physically-based Hillaire sky model I had previously implemented for the old renderer, but with a twist; it should adjust the constants so that they match the new physically-correct lighting values in this renderer. It got it right on its second try; the first try made the sky a few hundred times too bright, which I had to report to it diligently.</p>

<blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:opabrm62lmb4hpoyw3t7sehv/app.bsky.feed.post/3mislfitxys27" data-bluesky-cid="bafyreiew26cefvxg4kfzf2e3szz3lp34pokchb6xzpvkaiiryia6fgbndm" data-bluesky-embed-color-mode="system"><p lang="en">the new reference renderer is so good<br /><br /><a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mislfitxys27?ref_src=embed">[image or embed]</a></p>&mdash; Daniel P H Fox (<a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv?ref_src=embed">@phfox.net</a>) <a href="https://bsky.app/profile/did:plc:opabrm62lmb4hpoyw3t7sehv/post/3mislfitxys27?ref_src=embed">5 April 2026 at 22:57</a></blockquote>
<script async="" src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>

<p>That pixelated sun effect was the result of me and Claude going back-and-forth for about an hour debating what the best way of pixelating the sun disc would be. The general idea was established from the start; sample at multiple points, tonemap to predict what the on-screen colour would be, average to get antialiasing, then inverse tonemap to return back to physically-accurate values. Claude went down a rabbit hole with it and started introducing overcomplex tuning factors, but eventually I just told it to stop and reason from first principles. That pretty much directly led to the current zero-parameter solution, which looks the best and preserves its antialiased look no matter how intense the sun is.</p>

<p>I also briefly mentioned that sunsets were too bright, at which point it was able to immediately identify that the Mie scattering coefficient was too high. With approximately no effort on my part, sunsets immediately skyrocketed in visual quality to now looking the best they ever have. This is code that I will for-sure be reusing for the full-performance renderer.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/sunset.png" alt="Sunset" /></p>

<p>Auto-exposure and physically based bloom were next in line. These required a lot of manual tweaking by myself in order to get right, and will still require yet more tweaking over time. However, Claude was able to get functioning versions of these right off the bat, even if they were not initially tuned well.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/bloom-exposure.png" alt="Bloom &amp; exposure" /></p>

<p>And finally, right as I was approaching my monthly spend limit I had set for this project, I implemented atmospheric fog, which was the icing on the cake. Claude executed on this flawlessly, and I did yet more manual tuning after the fact just to tone down the effect to my liking.</p>

<p><img src="/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/volumetrics.png" alt="Volumetrics" /></p>

<p>As a small disclaimer; since the purpose of this project was to produce a reference renderer, these last steps (especially volumetrics) really sent the performance over the edge for the Steam Deck. Even my 4090 in my desktop could only muster around 20 frames a second here, so I wouldn’t take this as Claude revolutionising real-time graphics or anything. However, for the purposes which I set out to utilise it for, this is now a highly competent and tuned reference renderer which I can use to set up further visual testing.</p>

<h2 id="conclusions">Conclusions</h2>

<p>My feelings on AI coding agents is no longer that they are incapable of producing good work. My questions are now primarily about what exactly the speed-up is, practically, for realistic teams.</p>

<p>For individuals like myself with clear direction and autonomy, it seems pretty clear that these tools let you work at the speed of design, rather than the speed of code. For these kinds of small teams without much bureaucracy, I have no doubt that code is some degree of bottleneck, and these tools do realistically offer good speedups, even if you don’t succumb to the “vibe-coding exponentials” and instead pursue a pair programming approach.</p>

<p>That said, I’m still unconvinced that code is truly the bottleneck at many organisations. I can see value in Claude being able to deliver cheaper prototypes more quickly as discussion artifacts when getting alignment across teams, but I’m not sure how much of this translates to faster release processes. How does Claude speed up a sign-off from Legal?</p>

<p>Overall though, this experience has shown me that I need to use the tools that I have an opinion about. There’s two groups of people who I think are about to be badly hurt by the progression of this technology. One of those groups is obviously the investor-hype types that grandiosely overstate what this tech can do. But the other group belong to the staunchly moralistic anti-AI school of thought, denying outright that this tech can do anything useful at all.</p>

<p>I understand the moral, ethical and legal reservations, and do still share them. I’m not running to this tech, exactly. That said, I don’t see any fundamental reason why there couldn’t be a morally, ethically and legally <em>legitimate</em> version of this tech if it were developed and trained by authors that properly (and auditably) handled training data, and which developed it as a public good rather than as a tool of profit.</p>

<p>This is not technology I would bet against. Say what you want about the companies (and I have a lot of choice words for them), but there is some sense in which coding has changed. Just maybe don’t listen to the hype lords, either - go use this tech for yourself and find your own voice.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I was first introduced to Claude at work (because I work for a Big Tech Company). Originally, I was pretty sceptical of anything to do with AI, and generally leaned negative on it due to ethical and moral concerns around the conduct of the companies involved.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/about-200-usd-in-heres-what-i-think-about-claude/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Flip caches &amp;amp; information loss</title><link href="https://fluff.blog/2026/01/07/flip-caches-and-information-loss.html" rel="alternate" type="text/html" title="Flip caches &amp;amp; information loss" /><published>2026-01-07T00:00:00+00:00</published><updated>2026-01-07T00:00:00+00:00</updated><id>https://fluff.blog/2026/01/07/flip-caches-and-information-loss</id><content type="html" xml:base="https://fluff.blog/2026/01/07/flip-caches-and-information-loss.html"><![CDATA[<p>I’ve been exploring ways of introducing caching and incremental recomputation to immediate-mode UI systems, in line with a new information loss principle I’m thinking about.</p>

<h2 id="vdom">VDOM</h2>

<p>For a decade now, I haven’t been a fan of UI approaches that involve comparing trees of elements. Often, these are referred to as “virtual DOMs” or VDOMs (this is terminology inherited from the web).</p>

<p>My main criticism of these approaches is that they throw away granularity information that exists at the time of building the VDOM. By design, the VDOM assumes that everything can change between re-renders of UI, and so must compare everything to retrieve the actual minimal set of changes desired.</p>

<p>If the original builder of the VDOM specified the minimal change they wanted, this work is totally avoidable. In its absence, even a simple property change can balloon into a lot of work done by the CPU just to re-infer the information.</p>

<h2 id="fine-grained-reactivity">Fine-grained reactivity</h2>

<p>My goal when building Fusion (my fine-grained reactivity framework) five years ago was to eliminate this re-inference step. Instead of comparing trees of elements, my idea was to explicitly allow bindings to be made between properties of elements and objects in the code. If the object receives a change, the property is updated.</p>

<p>The key innovation of the system was to allow objects to bind to other objects. You could define computations that take the current state of some objects and returns a newly bindable state. When used in a UI, this established a granular chain of cause-and-effect through the UI, and allowed for changes to be applied directly without re-inferring anything.</p>

<p>However, this still didn’t satisfy me completely. The most obvious issue is a colouring problem; code that is written “reactively” has more ceremony than code which is written “traditionally”. The cost to the programmer of managing a fleet of objects required supporting concepts in memory management and helper utilities for improving ergonomics.</p>

<p>There’s also a more subtle cost in frameworks like this; they tend to over-memoise information by their nature. Since every object is a binding, every object has a state that needs to be synchronised to its users. This typically means the object is a state container. As a result, you effectively memoise every causal node in the system, which often means memoising every piece of dynamic state, even trivially inferrable ones. The cost of this memory has been shown to be notable in practice - it would be more ideal to create a scheme that more naturally encourages recomputation of trivial states rather than universal memoisation.</p>

<h2 id="the-principle-of-information-loss">The principle of information loss</h2>

<p>From my experience, using fine-grained reactivity has led to almost universally more predictable and favourable performance compared to VDOM systems. Many of the highest-performance frameworks today adopt similar principles.</p>

<p>But why is it better? In my eyes, it’s because it moves memoisation work closer to the subject being memoised.</p>

<p>Since the memoisation done by the reactive objects can take advantage of information available in the local context, they’re able to track changes granularly without re-inferring any missing information. The bindings and objects involved are directly and trivially known.</p>

<p>Attempting to apply this principle, I would extrapolate that the best memoisation techniques are likely ones which deduplicate calculations “as they happen” - parts of the computation which wish to reuse historical data can elect to do so on their own, in the same path of computation that returns the result of the computation. To the end user, the computation simply provides a result - caching details are encapsulated in some kind of context side-effect.</p>

<p>That’s what I’ve been exploring next, as part of a reductive approach I call “flip caches”.</p>

<h2 id="flip-caches">Flip caches</h2>

<p>Let’s consider an example of a gallery app. We want to show a bunch of photos with different widths and heights in a list view, similar to Google Images.</p>

<p>Our state may look like this:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Photo</span> <span class="p">{</span>
	<span class="n">width</span><span class="p">:</span> <span class="nb">f32</span><span class="p">,</span>
	<span class="n">height</span><span class="p">:</span> <span class="nb">f32</span><span class="p">,</span>
	<span class="n">image_content</span><span class="p">:</span> <span class="n">Texture</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">UIState</span> <span class="p">{</span>
	<span class="n">photos</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Photo</span><span class="o">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s write a naive immediate-mode function that implements a wrapping layout.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">build</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">UIState</span><span class="p">,</span> <span class="n">renderer</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Renderer</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">const</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">100.0</span><span class="p">;</span>
	<span class="k">const</span> <span class="n">GAP</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">4.0</span><span class="p">;</span>

	 <span class="c1">// Break photos onto rows bounded by screen width</span>
	<span class="k">let</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="p">{</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>

		<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">state</span><span class="py">.photos</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">;</span>
			<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo</span><span class="py">.width</span> <span class="o">/</span> <span class="n">photo</span><span class="py">.height</span><span class="p">)</span> <span class="o">*</span> <span class="n">height</span><span class="p">;</span>
			<span class="k">if</span> <span class="n">x</span> <span class="o">+</span> <span class="n">width</span> <span class="o">&gt;</span> <span class="n">imgui</span><span class="py">.screen_width</span> <span class="p">{</span>
				<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
				<span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
			<span class="p">}</span>
			<span class="n">photo_row</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo</span><span class="p">);</span>
			<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="k">if</span> <span class="o">!</span><span class="n">photo_row</span><span class="nf">.is_empty</span><span class="p">()</span> <span class="p">{</span>
			<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
		<span class="p">}</span>
		<span class="n">photo_rows</span>
	<span class="p">}</span>

	<span class="c1">// Render rows of photos scaled to fill screen width</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">y</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">photo_row</span> <span class="k">in</span> <span class="n">photo_rows</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">total_gap_widths</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo_row</span><span class="nf">.len</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">*</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="k">let</span> <span class="n">total_photo_widths</span> <span class="o">=</span> <span class="n">photo_row</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="n">p</span><span class="p">|</span> <span class="n">p</span><span class="py">.width</span><span class="p">)</span><span class="nf">.sum</span><span class="p">();</span>
		<span class="k">let</span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="p">(</span><span class="n">imgui</span><span class="py">.screen_width</span> <span class="o">-</span> <span class="n">total_gap_widths</span><span class="p">)</span> <span class="o">/</span> <span class="n">total_photo_widths</span><span class="p">;</span>
		<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
		<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">photo_row</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="n">photo</span><span class="py">.width</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
			<span class="n">renderer</span><span class="nf">.draw</span><span class="p">(</span><span class="nn">Draw</span><span class="p">::</span><span class="n">Texture</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">texture</span><span class="p">:</span> <span class="n">photo</span><span class="py">.image_content</span> <span class="p">});</span>
			<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="n">y</span> <span class="o">+=</span> <span class="n">height</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Every redraw event, we compute the layout in full, then directly send instructions to the renderer to draw textures where we want them. This is stateless and does not memoise, which makes it very easy to work with, but discards a lot of information. That’s especially bad when working with expensive layouts that involve many elements.</p>

<p>Let’s assume for demonstration that the <code class="language-plaintext highlighter-rouge">photo_rows</code> computation is a hotspot (perhaps it’s allocating too much, don’t ask!). Instead of immediately reaching for a VDOM or fine-grained reactivity, we could instead keep a cache between rebuilds. Maybe it looks like this:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nf">PhotoRows</span><span class="p">(</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Photo</span><span class="o">&gt;&gt;</span><span class="p">);</span>

<span class="k">fn</span> <span class="nf">build</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">UIState</span><span class="p">,</span> <span class="n">renderer</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Renderer</span><span class="p">,</span> <span class="n">cache</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Cache</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">const</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">100.0</span><span class="p">;</span>
	<span class="k">const</span> <span class="n">GAP</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">4.0</span><span class="p">;</span>

	 <span class="c1">// Break photos onto rows bounded by screen width</span>
	<span class="k">let</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="n">cache</span><span class="py">.get_or_init</span><span class="p">::</span><span class="o">&lt;</span><span class="n">PhotoRows</span><span class="o">&gt;</span><span class="p">(||</span> <span class="p">{</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
		<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">state</span><span class="py">.photos</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">;</span>
			<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo</span><span class="py">.width</span> <span class="o">/</span> <span class="n">photo</span><span class="py">.height</span><span class="p">)</span> <span class="o">*</span> <span class="n">height</span><span class="p">;</span>
			<span class="k">if</span> <span class="n">x</span> <span class="o">+</span> <span class="n">width</span> <span class="o">&gt;</span> <span class="n">imgui</span><span class="py">.screen_width</span> <span class="p">{</span>
				<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
				<span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
			<span class="p">}</span>
			<span class="n">photo_row</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo</span><span class="p">);</span>
			<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="k">if</span> <span class="o">!</span><span class="n">photo_row</span><span class="nf">.is_empty</span><span class="p">()</span> <span class="p">{</span>
			<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
		<span class="p">}</span>
		<span class="nf">PhotoRows</span><span class="p">(</span><span class="n">photo_rows</span><span class="p">)</span>
	<span class="p">});</span>

	<span class="c1">// Render rows of photos scaled to fill screen width</span>
	<span class="k">let</span> <span class="k">mut</span> <span class="n">y</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
	<span class="k">for</span> <span class="n">photo_row</span> <span class="k">in</span> <span class="n">photo_rows</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">total_gap_widths</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo_row</span><span class="nf">.len</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">*</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="k">let</span> <span class="n">total_photo_widths</span> <span class="o">=</span> <span class="n">photo_row</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="n">p</span><span class="p">|</span> <span class="n">p</span><span class="py">.width</span><span class="p">)</span><span class="nf">.sum</span><span class="p">();</span>
		<span class="k">let</span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="p">(</span><span class="n">imgui</span><span class="py">.screen_width</span> <span class="o">-</span> <span class="n">total_gap_widths</span><span class="p">)</span> <span class="o">/</span> <span class="n">total_photo_widths</span><span class="p">;</span>
		<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
		<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">photo_row</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="n">photo</span><span class="py">.width</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
			<span class="n">renderer</span><span class="nf">.draw</span><span class="p">(</span><span class="nn">Draw</span><span class="p">::</span><span class="n">Texture</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">texture</span><span class="p">:</span> <span class="n">photo</span><span class="py">.image_content</span> <span class="p">});</span>
			<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="n">y</span> <span class="o">+=</span> <span class="n">height</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The idea here is that <code class="language-plaintext highlighter-rouge">cache.get_or_init::&lt;T&gt;</code> only runs the inner closure once, then it saves the returned <code class="language-plaintext highlighter-rouge">T</code> and uses it for all subsequent calls. (The type exists to allow for multiple different pieces of data to be independently cached in a type-inferable way.)</p>

<p>This cache solves our memoisation problem for this expensive layout computation without requiring a deep structural change to any code. Since the mechanism is generic, it can encapsulate as much or as little of the code as profiling indicates is necessary.</p>

<p>For example, if constructing commands for the renderer was also expensive, that could be memoised instead:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nf">PhotoDrawInstructions</span><span class="p">(</span><span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Draw</span><span class="o">&gt;</span><span class="p">);</span>

<span class="k">fn</span> <span class="nf">build</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">UIState</span><span class="p">,</span> <span class="n">renderer</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Renderer</span><span class="p">,</span> <span class="n">cache</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">Cache</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">const</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">100.0</span><span class="p">;</span>
	<span class="k">const</span> <span class="n">GAP</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">4.0</span><span class="p">;</span>

	<span class="k">let</span> <span class="n">instructions</span> <span class="o">=</span> <span class="n">cache</span><span class="py">.get_or_init</span><span class="p">::</span><span class="o">&lt;</span><span class="n">PhotoDrawInstructions</span><span class="o">&gt;</span><span class="p">(||</span> <span class="p">{</span>
		<span class="c1">// Break photos onto rows bounded by screen width</span>
		<span class="k">let</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="p">{</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">index</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">photo_rows</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>

			<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">state</span><span class="py">.photos</span> <span class="p">{</span>
				<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span><span class="p">;</span>
				<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo</span><span class="py">.width</span> <span class="o">/</span> <span class="n">photo</span><span class="py">.height</span><span class="p">)</span> <span class="o">*</span> <span class="n">height</span><span class="p">;</span>
				<span class="k">if</span> <span class="n">x</span> <span class="o">+</span> <span class="n">width</span> <span class="o">&gt;</span> <span class="n">imgui</span><span class="py">.screen_width</span> <span class="p">{</span>
					<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
					<span class="n">photo_row</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
				<span class="p">}</span>
				<span class="n">photo_row</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo</span><span class="p">);</span>
				<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
			<span class="p">}</span>
			<span class="k">if</span> <span class="o">!</span><span class="n">photo_row</span><span class="nf">.is_empty</span><span class="p">()</span> <span class="p">{</span>
				<span class="n">photo_rows</span><span class="nf">.push</span><span class="p">(</span><span class="n">photo_row</span><span class="p">);</span>
			<span class="p">}</span>
			<span class="n">photo_rows</span>
		<span class="p">}</span>

		<span class="c1">// Render rows of photos scaled to fill screen width</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">instructions</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[];</span>
		<span class="k">let</span> <span class="k">mut</span> <span class="n">y</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
		<span class="k">for</span> <span class="n">photo_row</span> <span class="k">in</span> <span class="n">photo_rows</span> <span class="p">{</span>
			<span class="k">let</span> <span class="n">total_gap_widths</span> <span class="o">=</span> <span class="p">(</span><span class="n">photo_row</span><span class="nf">.len</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">*</span> <span class="n">GAP</span><span class="p">;</span>
			<span class="k">let</span> <span class="n">total_photo_widths</span> <span class="o">=</span> <span class="n">photo_row</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="n">p</span><span class="p">|</span> <span class="n">p</span><span class="py">.width</span><span class="p">)</span><span class="nf">.sum</span><span class="p">();</span>
			<span class="k">let</span> <span class="n">scale_factor</span> <span class="o">=</span> <span class="p">(</span><span class="n">imgui</span><span class="py">.screen_width</span> <span class="o">-</span> <span class="n">total_gap_widths</span><span class="p">)</span> <span class="o">/</span> <span class="n">total_photo_widths</span><span class="p">;</span>
			<span class="k">let</span> <span class="n">height</span> <span class="o">=</span> <span class="n">MIN_ROW_HEIGHT</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
			<span class="k">let</span> <span class="k">mut</span> <span class="n">x</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
			<span class="k">for</span> <span class="n">photo</span> <span class="k">in</span> <span class="n">photo_row</span> <span class="p">{</span>
				<span class="k">let</span> <span class="n">width</span> <span class="o">=</span> <span class="n">photo</span><span class="py">.width</span> <span class="o">*</span> <span class="n">scale_factor</span><span class="p">;</span>
				<span class="n">instructions</span><span class="nf">.push</span><span class="p">(</span><span class="nn">Draw</span><span class="p">::</span><span class="n">Texture</span> <span class="p">{</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">texture</span><span class="p">:</span> <span class="n">photo</span><span class="py">.image_content</span> <span class="p">});</span>
				<span class="n">x</span> <span class="o">+=</span> <span class="n">width</span> <span class="o">+</span> <span class="n">GAP</span><span class="p">;</span>
			<span class="p">}</span>
			<span class="n">y</span> <span class="o">+=</span> <span class="n">height</span><span class="p">;</span>
		<span class="p">}</span>

		<span class="nf">PhotoDrawInstructions</span><span class="p">(</span><span class="n">instructions</span><span class="p">)</span>
	<span class="p">});</span>

	<span class="k">for</span> <span class="n">inst</span> <span class="k">in</span> <span class="n">instructions</span> <span class="p">{</span>
		<span class="n">renderer</span><span class="nf">.draw</span><span class="p">(</span><span class="n">inst</span><span class="p">);</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The desirable property of this solution is that it obeys Tennant’s Correspondence Principle: memoising a passage of code does not require restructuring as fine-grained reactivity does. The default assumption made is that computations are trivial (which they often are).</p>

<p>Of course, the problem now is cache invalidation, but we have existing techniques to address this.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">impl</span> <span class="n">UIState</span> <span class="p">{</span>
	<span class="k">fn</span> <span class="nf">set_photos</span><span class="p">(</span><span class="o">&amp;</span><span class="k">mut</span> <span class="k">self</span><span class="p">,</span> <span class="n">photos</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Photo</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">self</span><span class="py">.photos</span> <span class="o">=</span> <span class="n">photos</span><span class="p">;</span>
		<span class="k">self</span><span class="py">.photos_changed</span> <span class="o">=</span> <span class="k">true</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="c1">// ... later ...</span>

<span class="k">if</span> <span class="k">self</span><span class="py">.photos_changed</span> <span class="p">{</span>
	<span class="n">cache</span><span class="py">.clear</span><span class="p">::</span><span class="o">&lt;</span><span class="n">PhotoDrawInstructions</span><span class="o">&gt;</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To implement such a cache, I would use a “flipping” method to allow read access to the previous version of the cache while allowing write access to a fresh version of the cache. This is why I use “flip caches” to describe this technique; it only requires space for two <code class="language-plaintext highlighter-rouge">T</code> allocations, plus a little book-keeping in the cache object itself, and flips between these allocations for predictable performance.</p>

<h2 id="why-i-think-this-could-be-useful">Why I think this could be useful</h2>

<p>I haven’t yet implemented a fuller UI system using flip caches, but I like the solution because it essentially filters rebuild events for closures which still rebuild in an immediate-mode fashion. There is no colouring problem like fine-grained reactive signals have.</p>

<p>Additionally, flip caches avoid the information loss problem of VDOM techniques; since the caching happens “inline” in the build function, diffing is unnecessary - the renderer receives the draw instructions it needs directly.</p>

<p>Of course, there’s one caveat to all of this, which is that re-constructing trees of components every rebuild could become expensive; a forest of small components can add up in a way that isn’t true of fine-grained reactive systems, which construct their trees once.</p>

<p>I’m interested to explore this system further to see whether higher-level abstractions can be built from this, to reach similar ergonomics to fine-grained reactive systems. In particular, I’m interested in orthogonal techniques to do with cache invalidation patterns, and how these caches could perhaps carry incidental UI state such as animation timelines.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve been exploring ways of introducing caching and incremental recomputation to immediate-mode UI systems, in line with a new information loss principle I’m thinking about.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/flip-caches-and-information-loss/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/flip-caches-and-information-loss/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Solving memory management</title><link href="https://fluff.blog/2025/10/17/solving-memory-management.html" rel="alternate" type="text/html" title="Solving memory management" /><published>2025-10-17T00:00:00+00:00</published><updated>2025-10-17T00:00:00+00:00</updated><id>https://fluff.blog/2025/10/17/solving-memory-management</id><content type="html" xml:base="https://fluff.blog/2025/10/17/solving-memory-management.html"><![CDATA[<p>I have devised a pretty exciting memory model for <a href="https://wolf.phfox.net/">Wolf</a>. If it works, it should be provably safe, scale close-to-optimally, and add zero language complexity.</p>

<p>I recommend you <a href="https://www.rfleury.com/p/enter-the-arena-talk">go watch this revelatory talk by Ryan Fleury.</a> The chain of logic they describe in that talk reflects pretty much my entire history with memory management up to today. I’ll continue this post assuming you know what I’m talking about.</p>

<h2 id="the-joy-of-arenas">The joy of arenas</h2>

<p>Back in 2023, while working on Fusion, <a href="https://fluff.blog/2023/08/30/the-next-ten-years-beyond-maids.html">I rediscovered arena allocation independently in a GC language.</a> Without anyone else ever having explained the concept to me, I immediately saw just how much more powerful structured memory management is, and how it leads to programs that are both trivial to reason about, and also fully deterministic.</p>

<p>It’s so utterly powerful that I managed to implement (what Fusion calls) <em>time-travelling checks</em> to predict use-after-frees, which have been a revelation for catching edge cases that I otherwise would have missed.</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/QTTRtzZxew4?si=lDU5XbiDLXRWNVKH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>It turns out these ideas are not original in the slightest! Ryan Fleury, Casey Muratori, Johnathan Blow and the rest of the gang have been out there for years singing the praises on arena allocators, and my lived experience dealing with all of this is all the proof I need that these ideas are every bit as good as each one of them said they were.</p>

<p>I love arena allocators. They’re everything I’ve loved about “maids” in the Roblox ecosystem for years, and they’re almost word-for-word identical to “scopes” (which are really just properly-done arenas that run destructors instead of freeing memory).</p>

<h2 id="the-rub">The rub</h2>

<p>There’s only one problem with arena allocators. It’s not even really <em>that much</em> of a problem, but it’s still worth mentioning IMO.</p>

<p>Arenas are still visible in your code. (Boo hoo, right? <em>Unusable.</em>)</p>

<p>When I designed “scopes” for Fusion, that was a feature - it declared a code path’s intent to allocate, and allowed you to declare their lifetime explicitly in your code, preventing whole classes of bugs. For any language that exists today, I think it’s the right approach.</p>

<p>But really, I’ve been looking for a way to make code <em>look</em> every bit as clean as garbage collection, while using a deterministic structured approach to memory management that can be analysed ahead of time and trivially checked for safety. I don’t know if today’s languages are up to that task without doing some gross things that I’d never recommend over simply <em>writing the damn thing</em>.</p>

<h2 id="wait-im-writing-a-language-already">Wait, I’m writing a language already</h2>

<p>Yeah, turns out I’m the benevolent dictator of my own still-in-flux language. And, turns out I’ve already been stewing on this problem in a different <em>context</em>.</p>

<p>One of the top features I’ve wanted in Wolf (my language) are implicit function parameters, aka <em>context</em>, so that you don’t have to do “prop drilling” to transiently pass relevant wide-area information down to deeply nested functions. I got the idea from my frontend UI experience, ironically enough - it’s a feature I implemented and loved in Fusion, which itself was somewhat stolen from React. (Hey - React’s useful for something!)</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/dOkoXYCpxMM?si=KCSyNQ2fVZeIASlH" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<p>My <em>original</em> line of thinking here was that it’d be <em>really nice</em> if you could contextually define an arena allocator to use inside of a function, a bit like how Jai (Johnathan Blow’s language) lets you do the same thing. That would make it a lot more ergonomic to have dynamically-sized literal constructors in Wolf - no need for any special syntax to tie them to a specific arena allocator. It also makes all functions generic over allocation strategy by default, which is <em>rad</em>.</p>

<p>However, I realised something pretty nice about Wolf. Since it’s a highly declarative programming language without things like mutation, pointers or references baked deeply into it, everything is lexically scoped. In theory, that makes the whole language pretty compatible with stack allocation, as you might find by default in C. It’s not a 100% solution, but a good 80% solution.</p>

<p>I was… half satisfied. Then I thought a little harder about what stack allocation <em>really</em> is.</p>

<h2 id="oops-all-arenas">Oops! All arenas</h2>

<p>Yeah, turns out stack allocators are just mini little arena allocators, aren’t they?</p>

<p>This is about when I realised that Wolf could have a really nice generic allocation strategy that does the same thing as a stack allocator, but doesn’t prescribe use of a stack per se. You just implicitly create a scratch allocator for each block, and if it passes any values out of itself, then you implicitly parameterise the block with an allocator to use for those values.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-- Pseudocode
fn [.return_alloc : allocator] (
	let local_alloc = allocator
	-- Allocated temporarily for the duration of the block
	let needs_memory = malloc [.name "steve", .age 27] in local_alloc
	-- Important: use the return allocator here
	let will_return_this = malloc [.name "baxter", .age 5] in return_alloc
	free local_alloc -&gt; will_return_this
)
</code></pre></div></div>

<p>Via induction, since all Wolf programs are composed from blocks, you can now guarantee that all memory is allocated and deallocated correctly (if sometimes conservatively, and with some extra nuances/implementation details that aren’t relevant here).</p>

<p>There’s even more nice things where that came from, too. You don’t strictly have to use one allocator per block; you could use as many as you like, wherever you like, because none of those details actually appear anywhere in the syntax.</p>

<p>For example, you can determine when the last time a value from an allocator is used in a block, and at that point, you can choose to drop the allocation right away. Alternatively, you could choose <em>not to</em> if you locally know it’ll get dropped by an allocator in a higher lexical scope. In other words, you can fit your arena allocator lifetimes more tightly or loosely to your program depending on context, all completely automatically.</p>

<p>What’s more, you could decide to use a right-sized allocator just for the parts of the block where the sizes of values are statically known, and use a more flexible allocator for the unbounded dynamically-sized things like user strings. (The fact that Wolf intentionally tries to be as compile-time-analysable as possible helps massively here.)</p>

<p>Oh right, flexible allocators for dynamically sized things… shoot.</p>

<h2 id="who-manages-the-manager">Who manages the manager?</h2>

<p>This model of memory management in Wolf works perfectly well <em>in theory</em>, when you have arenas that are <em>assumed to be</em> growable and flexible, and when you have infinite memory space.</p>

<p>In reality, you get a finite amount of virtual memory address space, and of that, a dismal fraction that’s available as physical memory.</p>

<p>At this point, I was thinking about allocation strategies for arena allocators that needing to be dynamically growable, or perhaps having a dedicated allocator <em>just</em> for growable arrays of some kind, which could then be used as a springboard for implementing dynamically sized arena allocators.</p>

<p>I was decently happy with that idea. That’s about when I discovered Ryan’s talk.</p>

<h2 id="theres-hardware-for-that-dummy">There’s hardware for that, dummy</h2>

<p>I had overcomplicated the whole thing because I wasn’t thinking about how the OS actually manages virtual address space. You can use the hardware’s MMU to just… reserve tons of contiguous memory and forget about it. It’s not like it’s going to be backed by any physical memory until you actually use it, so it’s free real estate.</p>

<p>So, the way Ryan describes the technique: make every arena allocator gigantic in virtual memory, then just forget about it. Sure, you’ll have a maximum arena size, but it’ll be something like 64 GB. No need to sweat over it when it’s orders of magnitude larger than the data your program is dealing with, and when you have 256 TB of virtual address space to carve up for arenas. Plus, this is where statically knowing the size of things can really come in clutch.</p>

<p>Ryan says that this gives you ~O(1) allocation and deallocation, all super close to the metal. I’m inclined to believe it, though I’m not about to repeat it without citing Ryan as I haven’t run through the whole analysis myself.</p>

<p>Zero overhead allocation and deallocation with fully implicit memory management that’s as clean as garbage collected code. I’m dreaming, right?</p>

<h2 id="remaining-considerations">Remaining considerations</h2>

<p>So obviously, I’m super jazzed about this. From my very brief searches online, I haven’t really found a language that does all of this together, in the way I’m describing here. Wolf may well be (one of) the first languages setting foot in this territory.</p>

<p>However, I still have a few lingering thoughts on my mind that I haven’t yet cleared up:</p>

<ul>
  <li>I suspect that I shouldn’t <em>fully</em> rely on the MMU to reserve gobs of virtual address space. I might implement a hybrid solution that chains <em>large-enough</em> blocks of contiguous space instead (something like 1GB chunks?) which might play better on platforms that don’t have as many bits of virtual address space.</li>
  <li>I’m also not entirely convinced yet that I can escape doing allocator management this way. I’m still thinking about ways of potentially carving up the address space in a way that can be used for allocating arenas of various sizes, just in case. I don’t really know.</li>
  <li>I’m also clear-eyed that people may still want to implement more specialised allocation techniques. My idea is that arena allocators are a perfectly good implicit language feature, and that through arena allocation, it should be trivial to manage the lifetime of custom allocators and their reserved memory. So I’m not thinking about them too hard right now.</li>
</ul>

<p>Overall though? I might have just <em>solved memory management</em>.</p>

<p>I’ve defeated that damn garbage collector.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I have devised a pretty exciting memory model for Wolf. If it works, it should be provably safe, scale close-to-optimally, and add zero language complexity.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/solving-memory-management/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/solving-memory-management/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Isn’t there someone you forgot to ask?</title><link href="https://fluff.blog/2025/10/13/isnt-there-someone-you-forgot-to-ask.html" rel="alternate" type="text/html" title="Isn’t there someone you forgot to ask?" /><published>2025-10-13T00:00:00+00:00</published><updated>2025-10-13T00:00:00+00:00</updated><id>https://fluff.blog/2025/10/13/isnt-there-someone-you-forgot-to-ask</id><content type="html" xml:base="https://fluff.blog/2025/10/13/isnt-there-someone-you-forgot-to-ask.html"><![CDATA[<p>I was motivated to write about my philosophy towards other people.</p>

<h2 id="the-golden-rule">The golden rule</h2>

<p>Simply: I don’t get to control what you think. I don’t get to make demands of you. If it is within the law, you are allowed to do that thing if you really want to. I won’t stop you if you’re determined. You are a human with autonomy and reasons for being the way you are.</p>

<p>Will I still have an opinion on you doing certain things? Sure thing. It’s not illegal to think about whether I would do what other people are doing. But you don’t have to ask for permission from me, you live your best free life and I’ll sit over here allowing you to do that.</p>

<p>I grade people primarily on one axis only: whether they treat others as humans or not. I want to live in a world where we see the humanity in each other.</p>

<h2 id="why-this-is-my-rule">Why this is my rule</h2>

<p>I’m a highly non-prescriptive person in general. I don’t want to be involved in what people around me do in their private lives or their private time, unless we agree to be involved in something. If all parties are consenting, it’s not my business, and I don’t feel inclined to know. That’s one reason.</p>

<p>But more broadly - I understand that we have a process for outlining things that shouldn’t be done, and that process is democracy. If I wanted to stop people from doing something because I was concerned about its impact, that’s the path I would pursue.</p>

<p>Systemic problems demand systemic solutions, not individual ones. The idea that a society of individual choices are responsible for the world’s largest problems isn’t sound to me; governments are bottlenecks through which all legal behaviour must pass. It’s the clear place to target when trying to solve problems, especially ones where responsibility is amorphous and hard to pin down. Make it blameless instead - institute a system that outlines what shouldn’t be done. If you fail to do that, then find out how to do it next time.</p>

<p>I ultimately think the behaviour of individuals should be driven by what they think they should do, not what they’re told to do by others. If I want to reduce the amount of meat in my diet, that’s wonderful and I should go do that. But there’s no logical reason why it’s the solution to a systemic problem; people don’t work like that.</p>

<p>So, if I want others to eat less meat, I can offer a <em>recommendation</em> or a <em>reason</em>, but if they say no, then that’s their choice and it would be improper to override it. It would be especially improper to treat them badly over it, publicly shame them, make people feel bad for associating with them… you get the idea.</p>

<p>It’s almost always the case that there’s a reason people are compelled to be a certain way. Their reasoning may not be <em>sound</em> or <em>rational</em>, but people don’t generally get out of bed in the morning to make the world a more miserable place. I often say that there’s a nugget of truth in every opinion, you just have to work through the dressing to find it.</p>

<p>This generally leads towards my big idea of how we should make change in the world. It’s not by dictating to others what is good for them, regardless of how obvious I think it is. It is instead by listening first, understanding what they think and believe, identifying those nuggets of truth, and figuring out where to go from there. That’s how you build a base around an idea and transform that into change.</p>

<p>That’s true even of the people I disagree with the most vigorously today. I don’t begrudge anyone, because people are complex mosaics of attributes, not single dimensional beings.</p>

<h2 id="why-im-concerned">Why I’m concerned</h2>

<p>It feels like a fair few people don’t do this anymore because they don’t believe in these ideas anymore. It’s mistaken for ideas that aren’t helpful, like centrism or conformism. I think that’s a dying shame that the difference isn’t known better.</p>

<p>It often feels like people demand others to do things for them, or be cast out. Believe in the same ideas we do. Conform to what we tell you to do. They attend gatherings where important speakers present romantic visions of a world where dissent is quieted. To some small extent, perhaps they even pride themselves on how they treat some others badly.</p>

<p>That’s not a phenomenon that’s boxed into a certain sect of society, by the way. There’s endless talks about extremes on a spectrum, but society is far more higher-dimensional than that. It’s not about simple politics or social causes, it runs deep into even the most niche topics where there isn’t unanimity. I’ve seen it in enthusiast spaces where the stakes are supposedly zero.</p>

<p>We have forgotten how to talk about our significant differences in <em>any</em> space on <em>any</em> topic. The moment I realised this, I saw it everywhere. It changed the way I view the world.</p>

<h2 id="what-im-doing">What I’m doing</h2>

<p>As you may have seen, I’m not on social media, except for YouTube. I don’t think I need to explain how it relates to this concept; it’s far too easy for people to walk in the door and shit on your plate. I don’t want to be part of the global town square if that’s what’s on offer.</p>

<p>I much prefer tighter, closer connection. When I get to know you, and we become friends, and we do more things together, I think that motivates people to place more of a value on humanity. When my friends come out with things I can’t accept, being close to them places a cost on rejection and encourages me towards listening. The same logic works when my friends disagree with me, too.</p>

<p>In general, this has made my life <em>much</em> more pleasant. By taking this stance, I haven’t compromised on how I think about the world at all. Instead, the opposite has happened: I feel like I have more conviction in my view of the world, as I understand more about the other people who live in it. I’ve become softer and less demanding of others, and it’s brought me closer to a lot of people in my life.</p>

<p>It feels like it’s been a long road of personal development to understand this. There was a time where I was insufferable and made myself miserable for no reason.</p>

<p>Writing this is a bit like documenting what I’ve learned since then, I suppose. It turns out life can be sunnier if you find the humanity in others. It’s not hard, you just have to believe a little.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I was motivated to write about my philosophy towards other people.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/isnt-there-someone-you-forgot-to-ask/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/isnt-there-someone-you-forgot-to-ask/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why I’m writing a programming language</title><link href="https://fluff.blog/2025/09/17/why-im-writing-a-programming-language.html" rel="alternate" type="text/html" title="Why I’m writing a programming language" /><published>2025-09-17T00:00:00+00:00</published><updated>2025-09-17T00:00:00+00:00</updated><id>https://fluff.blog/2025/09/17/why-im-writing-a-programming-language</id><content type="html" xml:base="https://fluff.blog/2025/09/17/why-im-writing-a-programming-language.html"><![CDATA[<p>I’m doing the thing people tell you not to do. Wolf is my small, smart programming language.</p>

<p>You’ll likely start with a very valid question…</p>

<h2 id="why">Why?</h2>

<p>I don’t believe we’re at the end of programming language history. We haven’t found the perfect language yet (does that even exist?) so why not spend time exploring more of the space? Sure, I’m not an academic, and absolutely daft, but I figure there’s still fun to be had in the process of exploring the possibility space.</p>

<p>In particular, there’s a few ideas that excite me terribly:</p>

<h3 id="true-consistency">True consistency</h3>

<p>Most programming languages are at least influenced by the syntax and semantics of C.</p>

<p>I think that C is a perfectly fine programming language! Basic perhaps, but somewhat fine. However, I think that most languages designed in C-style aren’t really <em>consistently designed</em> languages. Things that are ostensibly similar look different on the page, and things that are ostensibly different look similar on the page.</p>

<p>After a decade+ of working with the Lua language, I know how much I love a simple mental model. I wonder what that could look like when applied to a language’s syntax.</p>

<p>I want a language where you can point to language features <em>visually</em>, by pattern matching with your eyes.  Instead of building towering complexes of features, you should be able to hold a model of the whole language in your head, and it should map 1:1 with each syntax item.</p>

<h3 id="incremental-recomputation">Incremental recomputation</h3>

<p>This is probably the headline thing I think about these days. After years working on reactive signal libraries, I’m desperately tired of all the extra syntax cruft.</p>

<p>I often think about immediate-mode programming. Conceptually, it’s far and away the best choice for expressing logic clearly. The only drawback of it is that blowing away a bunch of computation just to do it all again can be incredibly wasteful, especially when you need to perform more complex layout reflow for example.</p>

<p>Why can’t a compiler optimise that?</p>

<p>Of course, the answer defies simple explanation - side effects, mutation, blah blah. But that’s just why we can’t do it <em>in existing languages</em>. What would a language built for that look like?</p>

<p>Imagine you didn’t just run through a whole function every time you needed to update the result of a previous calculation. What if you could just re-run the part that’s actually relevant?</p>

<p>Right now, I fully believe that something like this is the bridge we need between immediate-mode and retained-mode logic.</p>

<h3 id="memory-management">Memory management</h3>

<p>After implementing scopes in Fusion, I’m pretty furious we ever spent so much time on garbage collection. Structured memory management is so obviously superior it’s not even funny.</p>

<p>The problem is that doing structured memory management in Lua is also so obviously against the grain. Lua has no facilities for doing it ergonomically, because it’s built on the philosophy that garbage collection will handle almost everything for you.</p>

<p>Simultaneously, I’m a pretty happy Rust user, but I’m not about to defend Rust’s memory management approach either. Lifetimes are hell, and the language as a whole is confusing and unintuitive to learn if you’re not familiar with its concepts.</p>

<p>When I listen to Graydon Hoare and Casey Muratori, I wonder just how many trivial memory management techniques we’re missing when we think in terms of RAII and lifetimes.</p>

<p>Could you make a language that trivialises memory management by taking a broader view of the problem?</p>

<h3 id="open-objects">Open objects</h3>

<p>It seems really common these days to define objects as rigid nominal types with <em>only</em> a defined set of data on them.</p>

<p>Perhaps you’re making a 3D modelling program and you want to store some vertex data:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Mesh</span> <span class="p">{</span>
	<span class="n">vertices</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vertex</span><span class="o">&gt;</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">Vertex</span> <span class="p">{</span>
	<span class="n">position</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">normal</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">uv</span><span class="p">:</span> <span class="n">Vec2</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then perhaps, the tool has a vertex painting mode, where you want each vertex to <em>additionally</em> have a vertex colour and alpha.</p>

<p>These things logically live on the vertex, so you <em>could</em> put them there:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Vertex</span> <span class="p">{</span>
	<span class="n">position</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">normal</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">uv</span><span class="p">:</span> <span class="n">Vec2</span><span class="p">,</span>
	<span class="n">colour</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">alpha</span><span class="p">:</span> <span class="nb">f32</span>
<span class="p">}</span>
</code></pre></div></div>

<p>But what if you wanted that vertex colour and alpha to come from an interchangeable “vertex colour map”? You’d have to store them externally:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">VertexColourMap</span> <span class="p">{</span>
	<span class="n">vertices</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">VertexColour</span><span class="o">&gt;</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">VertexColour</span> <span class="p">{</span>
	<span class="n">colour</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>
	<span class="n">alpha</span><span class="p">:</span> <span class="nb">f32</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I’m going to apply a somewhat-arbitrary transformation to this code. I’m going to switch the data around to be stored in SOA format, rather than AOS.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Mesh</span> <span class="p">{</span>
	<span class="n">positions</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span>
	<span class="n">normals</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">uvs</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec2</span><span class="o">&gt;</span>
<span class="p">}</span>

<span class="k">struct</span> <span class="n">VertexColourMap</span> <span class="p">{</span>
	<span class="n">colours</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">alphas</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">f32</span><span class="o">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Notice that, by performing this switch-around, we’ve actually flattened down one of the dimensions of this problem. No matter whether we store the data in <code class="language-plaintext highlighter-rouge">Mesh</code> or <code class="language-plaintext highlighter-rouge">VertexColourMap</code>, we do not need to change the data structure at all:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// The data is stored the same way (!)</span>
<span class="k">struct</span> <span class="n">Mesh</span> <span class="p">{</span>
	<span class="n">positions</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span>
	<span class="n">normals</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">uvs</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec2</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">colours</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Vec3</span><span class="o">&gt;</span><span class="p">,</span>
	<span class="n">alphas</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">f32</span><span class="o">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This led me to a <em>really cool</em> insight - what if you could build objects by joining together fields like this?</p>

<p>Instead of having a “mesh struct” with a rigid structure, what if an object could be formed from any fields you wanted to view together? In a system like that, you could easily define extra fields for an object in your own namespace, and transparently associate them with data that comes from elsewhere in the codebase.</p>

<p>Instead of “closed objects” that can’t be extended by others, you’d have “open objects” that are just syntax sugar for array accesses anywhere in memory.</p>

<p>I view this as the ultimate expression of the open-closed principle, or perhaps just the logical conclusion of entities from ECS. Objects become mere IDs - things you use to access fields.</p>

<p>What would a language built around “open objects” look like? Could open objects be made efficient? Where would they fall short?</p>

<h2 id="introducing-wolf">Introducing Wolf</h2>

<p>To pursue these questions, late last year I decided to pull the trigger and push the first commit to a repository called <code class="language-plaintext highlighter-rouge">wolf</code>.</p>

<p><img src="/assets/posts/why-im-writing-a-programming-language/initial-commit.png" alt="The initial commit for the Wolf repository." /></p>

<p>Back then, I didn’t intend to actually implement the language I was designing. All I wanted was to <em>explore</em> what me and my good friend Trey thought “our ideal language” could look like.</p>

<p>In that time though, I’ve ruthlessly scrapped and rethought a ton of ideas. In the time that some people may have completed a whole traditional language, I’ve just barely figured out how the basic constructs of the language should even work, and even then I’m not entirely sure.</p>

<p><img src="/assets/posts/why-im-writing-a-programming-language/i-havent-written-much.png" alt="The &quot;Design&quot; page on the Wolf website. It only covers basic language features" /></p>

<p>That said, beyond the pages, there’s a few truths I seem to have landed on in my head:</p>

<h3 id="wolf-works-like-maths">Wolf works like maths</h3>

<p>Wolf is designed along the grain of mathematics. Equals is equals - there’s no assignment or mutation by default.</p>

<p>In that sense, Wolf is firmly placed in the camp of immutable functional languages built atop the lambda calculus, but not for any of the traditional reasons - I didn’t even know what the lambda calculus was before I started.</p>

<p>I don’t necessarily agree that mutation and assignment are fundamental operations that should appear in normal logic. I think most logic should be stateless, and when you’re writing stateless logic, you <em>shouldn’t</em> need mutation in theory. (The lambda calculus backs this up!)</p>

<p>The real reason I think we reach for mutation is because functional syntax sucks. Recursion is not an ergonomic replacement for a <code class="language-plaintext highlighter-rouge">for</code> loop in <em>many</em> cases. Why haven’t we tried to close that gap?</p>

<p>So, to the maximal extent possible, Wolf should let you express stateless functions the same way you express maths - as substitutable expressions.</p>

<h3 id="wolf-mutates-but-carefully">Wolf mutates, but carefully</h3>

<p>The last section may have given you the impression that I’m a Haskell enjoyer (more of an F# guy tbh), but I’ve omitted most of the story.</p>

<p>You see, I don’t think mutation is something to be avoided. After all, our computers are <em>basically</em> just overgrown Turing machines. To deny mutation in a programming language is to invent a new alternate reality.</p>

<p>Instead, I simply view things in reverse to most languages. In my head, mathematics is the base of logic, and mutation/imperative style is an <em>extension</em> to basic logic that allows it to interact with an event-based model of the outside world.</p>

<p>From that angle, I think it is perfectly rational for Wolf to support mutable dynamic objects, because they’re a reality of programming a Turing machine. The only key difference is that I think it should be tightly managed, so that both humans and machines can figure out how these dynamic objects and mutation events affect the execution of the program.</p>

<p>One particular mistake I’m not keen to re-enact is the idea of allowing unmanaged memory allocations and frees. I see no reason for there to be a “global allocator”, and have no desire to implement things like borrow checking or RAII.</p>

<p>Why are we trying so hard to make objects manage themselves internally, when we could just manage them better from the outside? We already know about concepts like arenas, object pools and slab allocators. These things trivialise memory management, but I don’t blame people for not using them when many vogue languages don’t elevate them to a prime position.</p>

<p>In Wolf, I want all dynamic features to route through structured memory management techniques like these, rather than random vibes-based resource allocations or garbage collectors.</p>

<p>But importantly, it shouldn’t feel like you’re managing memory at all - it should be transparent, almost implicit in what you’re writing. After all, it’s maths, right?</p>

<h3 id="wolf-is-ruthlessly-smart">Wolf is ruthlessly smart</h3>

<p>I think static analysis is non-negotiable in this day and age for good autocomplete and program optimisation. I don’t want another dynamic scripting language.</p>

<p>So, Wolf is designed to be 100% statically analysed, type-checked and optimised. The language avoids features that would require dynamic or run-time analysis to check the correctness of, or which lead to optimisation footguns.</p>

<p>Instead of methods, Wolf prefers pipelining and freestanding functions. When you can pipe an object into a function, you don’t need methods anymore. I already write most of my code using freestanding functions today in all programming languages I work in, and it makes modularity so much easier to achieve because it obeys the open-closed principle naturally.</p>

<p>Instead of polymorphism, Wolf prefers switching based on statically-available type information. This sort of thing should work a lot better in Wolf, because it’s built without a lot of the concepts that make indirection necessary in other languages. We know that random memory access is especially expensive when factoring in things like cache misses, so why depend so heavily on it?</p>

<p>Instead of dynamic types, Wolf prefers inferred types. If you define a minimum amount of information about your interfaces, that the compiler should be able to propagate through your implementations. Inference should be a forwards process, since code is written forwards, and that unlocks the best possible autocomplete and error messages for unfinished code.</p>

<h2 id="conclusions">Conclusions</h2>

<p>Wolf is still extremely early in its life. It doesn’t even have a reference implementation, or even a completed design. Mostly, it’s just ideas, but I have some conviction that these ideas are good to pursue further.</p>

<p>Long-term, I want to bridge the gap between lightweight scripting languages and powerful systems languages - a kind of systems-language Lua that threads the needle between expressivity and performance.</p>

<p>I don’t make any promises that this project goes anywhere, but if you see me making commits, then you know what I’m up to. Wish me luck on my misguided programming language adventures :)</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’m doing the thing people tell you not to do. Wolf is my small, smart programming language.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/why-im-writing-a-programming-language/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/why-im-writing-a-programming-language/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Towards dedicated Luau development</title><link href="https://fluff.blog/2025/05/02/towards-dedicated-luau-development.html" rel="alternate" type="text/html" title="Towards dedicated Luau development" /><published>2025-05-02T00:00:00+00:00</published><updated>2025-05-02T00:00:00+00:00</updated><id>https://fluff.blog/2025/05/02/towards-dedicated-luau-development</id><content type="html" xml:base="https://fluff.blog/2025/05/02/towards-dedicated-luau-development.html"><![CDATA[<p>I’m deciding to refocus my personal time on projects in the broader Luau ecosystem and my personal projects, rather than Roblox.</p>

<p>This is a strange post to write - Roblox tool development has served me incredibly well. I’ve been lucky enough to make a great thing out of plugin and package development; it’s covered my rent every month and it’s been consistent for me. If I were a ruthless capitalist, perhaps I’d ride out the income until it dried up.</p>

<p>But I respect you more than that.</p>

<p>The truth of the matter is that, since starting my day job last summer, I’ve found myself immersed in Roblox day in and day out. It’s great work! I enjoy it massively. But I don’t wish to take it back home with me.</p>

<p>Instead, I’ve found greater inspiration in the world of things that live outside of the engine. Whether that’s learning about voxel engines with Daydream, or thinking about the future of UI and declarative programming with Fusion, I’ve realised that’s what I want to spend my time on.</p>

<p>So I’m going to quit while I’m ahead. My Roblox projects continue to be massively successful, but I’m happy with where they’ve gone and what they’ve inspired in others. I like to say that “enough” is the most powerful word in the world; I think I have found “enough”.</p>

<h2 id="what-happens-next">What happens next</h2>

<p>I plan to wind down Studio Elttob, which has been the brand under which all of my Roblox-only projects have lived.</p>

<p>This will involve opening up most of its products, cancelling in-progress projects, and rehousing some others.</p>

<ul>
  <li><strong>Cancelled:</strong> The Elttob Atmos and Elttob InCommand plugins have been cancelled.
    <ul>
      <li>There was so much in the in-progress builds of Atmos that I was genuinely proud of! - but I don’t feel compelled to work on these projects at this stage.</li>
    </ul>
  </li>
  <li><strong>Cancelled:</strong> The VOLUMIKA 2 project has been cancelled.
    <ul>
      <li>While this was already somewhat functional, I struggled with performance issues for a long time, and those haven’t been resolved.</li>
    </ul>
  </li>
  <li><strong>Opened up:</strong> The following paid products will enjoy extended support until <strong>1 September 2025</strong>, at which point they’ll be opened up to the community so that everyone can benefit from them. This includes:
    <ul>
      <li>Elttob Relight</li>
      <li>Elttob Reclass</li>
      <li>All Classic Suite plugins</li>
      <li>VOLUMIKA</li>
    </ul>
  </li>
  <li><strong>Continuing:</strong> Fusion will continue development, but:
    <ul>
      <li>Studio Elttob branding will be removed from the project.</li>
      <li>The FusionKit for Roblox project has been cancelled.</li>
      <li>Fusion’s Roblox APIs will continue to be supported, but will be modularised over time to allow Fusion to be used for general purpose Luau development.</li>
    </ul>
  </li>
  <li><strong>Opened up:</strong> Vanilla 4 for Roblox Studio will be opened up:
    <ul>
      <li>Vanilla 4 for Roblox Studio will be opened up to the community, but new icons won’t be designed.</li>
      <li>The Vanilla compiler, previously internal and proprietary, will be open sourced.</li>
    </ul>
  </li>
  <li><strong>Going private:</strong> Vanilla itself will be taken private:
    <ul>
      <li>Plans for a commercial Vanilla icon set have been dropped.</li>
      <li>The Vanilla icons will return to being a personal icon set for my own projects.</li>
    </ul>
  </li>
</ul>

<h2 id="my-new-priorities">My new priorities</h2>

<p>With Studio Elttob going away, I can sharpen my focus on my remaining projects.</p>

<h3 id="front-burner-daydream">Front burner: Daydream</h3>

<p>Daydream can be traced all the way back to some of my earliest Roblox projects back in 2012/13. Since then, it’s enjoyed multiple transformations:</p>

<ul>
  <li>Becoming the first infinite voxel engine on Roblox capable of Minecraft-scale worlds in 2016</li>
  <li>Having its technology repurposed for the monumentally successful Blox in 2019</li>
  <li>Developing into a highly-optimised engine for Blox Survival in 2021</li>
  <li>Being spun off of Roblox into the standalone Project Stockholm, now called Daydream, where it is today!</li>
</ul>

<p>In many ways, Daydream is the one project I’ve wanted to work on for the longest. It’s also the most educational project I’ve ever worked on; originally used to learn about the basics of voxel engines, then teaching me so much more about high-performance Luau and game optimisation, and now serving as a vehicle for me to learn computer graphics, GPU programming, and low-level engine development.</p>

<p>I’m more motivated to work on Daydream now than I’ve ever been before. I love it so much.</p>

<p>So, I’m now focusing my efforts on Daydream first and foremost, to give it the time of day that it deserves. I’ll continue to work on the Daydream YouTube series so that I can educate more people about the myriad of interesting computer science problems that arise from it. Perhaps one day, it’ll even be something you can play! But I’m not thinking that far ahead yet.</p>

<h3 id="back-burner-fusion">Back burner: Fusion</h3>

<p>I still believe in Fusion as the ultimate companion for Luau development and remain committed to providing support for it. I also still intend to take Fusion further than where it is today, though I can’t say that I’ll do so on an expedited schedule.</p>

<p>I think Fusion is overall in a good place after the push to get 0.3 out the door. From here, things look to mostly be performance work and ergonomic tweaks, and some project restructuring.</p>

<p>As a little treat; I currently plan to use Fusion and Luau inside of Daydream! I want to build a complete non-Roblox UI using it. I’ve even been floating the idea of rebuilding the whole documentation website in Fusion one day, but that’s more of a wishful thought.</p>

<h3 id="and-thats-it">And that’s it</h3>

<p>All of my other existing projects aren’t going to be actively developed. That doesn’t mean they’re over, and it doesn’t mean I won’t start more of them - it just means I won’t touch any particular project unless I have a motivation to.</p>

<p>I’m making a point of paring down my active projects to exactly two, because that’s where I think I’m happiest right now. If I don’t make progress on other projects, I’m fine with that. Daydream and Fusion are where I want to push right now.</p>

<p>This doesn’t mean I’m leaving the Roblox community, by the way. I have a <em>whole lot</em> of Roblox knowledge and I’m incredibly interested in the future of the platform. I want to help build Roblox up to be the best place it can be, so you’ll still see my username around.</p>

<p>It’s just that the user in question will be a hobbyist, not a full time creator :)</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’m deciding to refocus my personal time on projects in the broader Luau ecosystem and my personal projects, rather than Roblox.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/towards-dedicated-luau-development/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/towards-dedicated-luau-development/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Framerate considered harmful</title><link href="https://fluff.blog/2025/01/21/framerate-considered-harmful.html" rel="alternate" type="text/html" title="Framerate considered harmful" /><published>2025-01-21T00:00:00+00:00</published><updated>2025-01-21T00:00:00+00:00</updated><id>https://fluff.blog/2025/01/21/framerate-considered-harmful</id><content type="html" xml:base="https://fluff.blog/2025/01/21/framerate-considered-harmful.html"><![CDATA[<p>I’ve spent the past little while moving Daydream away from framerate counters and towards “frame budgets”. Here’s why.</p>

<p><img src="/assets/posts/framerate-considered-harmful/frame-budget.png" alt="Daydream using frame and tick budgets." /></p>

<p>The primary reason is that it’s hard to properly read frame rates, because they are actually a reciprocal unit; to double your framerate, you must halve the amount of time taken to process a frame.</p>

<p>This can be fine in isolation - defining a “target framerate” is still a valid concept, because it’s reasonable to want to serve someone, say, 120 frames per second.</p>

<p>But when you try and compare frame rates with each other, or track frame rate through development, you’ll quickly find that framerate can be misleading. Targeting 240 FPS, but your prototype is hitting 2000 FPS? Plenty of headroom, surely - but actually, you’re already using 12% of the frame!</p>

<p>That’s why I’m moving away from the use of <em>framerate</em> as a debug metric. It just isn’t relevant during development. Instead, I’m measuring what percentage of the frame budget is being used, which I’ve set at 240 FPS for Daydream (since I have a good graphics card, and I feel like it should be possible).</p>

<p>Having a linear, easy-to-interpret percentage on-screen at all times makes performance usage visible and understandable in a way that - personally - I find framerate doesn’t offer. I derived the idea from a friend that used to work at Unity and Halfbrick; when they talk about performance, they always point to two things: set a budget early, and make expensive things visibly expensive.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I’ve spent the past little while moving Daydream away from framerate counters and towards “frame budgets”. Here’s why.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/framerate-considered-harmful/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/framerate-considered-harmful/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Garbage collected reference counting in Luau</title><link href="https://fluff.blog/2025/01/08/garbage-collected-reference-counting-in-luau.html" rel="alternate" type="text/html" title="Garbage collected reference counting in Luau" /><published>2025-01-08T00:00:00+00:00</published><updated>2025-01-08T00:00:00+00:00</updated><id>https://fluff.blog/2025/01/08/garbage-collected-reference-counting-in-luau</id><content type="html" xml:base="https://fluff.blog/2025/01/08/garbage-collected-reference-counting-in-luau.html"><![CDATA[<p>GCRC is a novel resource management technique for Luau that works around the lack of <code class="language-plaintext highlighter-rouge">__gc</code> metamethod using reference counted handles.</p>

<h2 id="a-primer-on-the-garbage-collection-problem">A primer on the garbage collection problem</h2>

<p>Imagine you have an object constructor like this:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="k">function</span> <span class="nf">make_person</span><span class="p">(</span>
	<span class="n">name</span><span class="p">:</span> <span class="n">string</span>
<span class="p">)</span>
	<span class="kd">local</span> <span class="n">self</span> <span class="o">=</span> <span class="p">{</span>
		<span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="p">,</span>
		<span class="n">age</span> <span class="o">=</span> <span class="nb">math.random</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="kd">local</span> <span class="k">function</span> <span class="nf">clean_up_person</span><span class="p">()</span>
		<span class="nb">print</span><span class="p">(</span><span class="err">`</span><span class="p">{</span><span class="n">self</span><span class="p">.</span><span class="n">name</span><span class="p">}</span> <span class="p">(</span><span class="n">aged</span> <span class="p">{</span><span class="n">self</span><span class="p">.</span><span class="n">age</span><span class="p">})</span> <span class="n">has</span> <span class="n">been</span> <span class="n">cleaned</span> <span class="n">up</span><span class="err">`</span><span class="p">)</span>
	<span class="k">end</span>

	<span class="k">return</span> <span class="n">self</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We would like to run <code class="language-plaintext highlighter-rouge">clean_up_person</code> when the user of <code class="language-plaintext highlighter-rouge">make_person</code> has stopped using the returned table in all of their code.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">do</span>
	<span class="kd">local</span> <span class="n">person</span> <span class="o">=</span> <span class="n">make_person</span><span class="p">(</span><span class="s2">"Allison"</span><span class="p">)</span>
	<span class="n">task</span><span class="p">.</span><span class="n">wait</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
	<span class="nb">print</span><span class="p">(</span><span class="s2">"I am"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="s2">"and I am"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">age</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1">-- call clean_up_person() here automatically....</span>
</code></pre></div></div>

<p>The idiomatic way to do this would be via the <code class="language-plaintext highlighter-rouge">__gc</code> metamethod, but that’s disabled in Luau for good reason. Instead, we can use a weak table to detect when the object has been garbage collected from the outside, and run our cleanup code then:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="k">function</span> <span class="nf">run_on_gc</span><span class="p">(</span>
	<span class="n">object</span><span class="p">:</span> <span class="n">unknown</span><span class="p">,</span> 
	<span class="n">callback</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="p">()</span>
<span class="p">)</span>
	<span class="kd">local</span> <span class="n">alive_test</span> <span class="o">=</span> <span class="nb">setmetatable</span><span class="p">({</span><span class="n">object</span><span class="p">},</span> <span class="p">{</span><span class="n">__mode</span> <span class="o">=</span> <span class="s2">"v"</span><span class="p">})</span>
	<span class="n">task</span><span class="p">.</span><span class="n">spawn</span><span class="p">(</span><span class="k">function</span><span class="p">()</span>
		<span class="k">repeat</span> <span class="n">task</span><span class="p">.</span><span class="n">wait</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="k">until</span> <span class="n">alive_test</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">~=</span> <span class="n">object</span>
		<span class="n">callback</span><span class="p">()</span>
	<span class="k">end</span><span class="p">)</span>
<span class="k">end</span>

<span class="kd">local</span> <span class="k">function</span> <span class="nf">make_person</span><span class="p">(</span>
	<span class="n">name</span><span class="p">:</span> <span class="n">string</span>
<span class="p">)</span>
	<span class="kd">local</span> <span class="n">self</span> <span class="o">=</span> <span class="p">{</span>
		<span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="p">,</span>
		<span class="n">age</span> <span class="o">=</span> <span class="nb">math.random</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="kd">local</span> <span class="k">function</span> <span class="nf">clean_up_person</span><span class="p">()</span>
		<span class="nb">print</span><span class="p">(</span><span class="err">`</span><span class="p">{</span><span class="n">self</span><span class="p">.</span><span class="n">name</span><span class="p">}</span> <span class="p">(</span><span class="n">aged</span> <span class="p">{</span><span class="n">self</span><span class="p">.</span><span class="n">age</span><span class="p">})</span> <span class="n">has</span> <span class="n">been</span> <span class="n">cleaned</span> <span class="n">up</span><span class="err">`</span><span class="p">)</span>
	<span class="k">end</span>

	<span class="n">run_on_gc</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">clean_up_person</span><span class="p">)</span>

	<span class="k">return</span> <span class="n">self</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The only problem is that this doesn’t work, because <code class="language-plaintext highlighter-rouge">make_person</code> still contains a strong reference to <code class="language-plaintext highlighter-rouge">self</code>, which we either must change to a weak reference or set to <code class="language-plaintext highlighter-rouge">nil</code>. However, both of those options means we can’t access <code class="language-plaintext highlighter-rouge">self.name</code> and <code class="language-plaintext highlighter-rouge">self.age</code> in <code class="language-plaintext highlighter-rouge">clean_up_person</code>, because the object would no longer be accessible to our code.</p>

<p>This has been a fundamental thorn in the side of Luau library developers for a very long time, requiring the use of manual memory management methods like <code class="language-plaintext highlighter-rouge">:destroy()</code> methods, or structured memory management frameworks like <a href="https://fluff.blog/2023/08/30/the-next-ten-years-beyond-maids.html">scopes</a> or maids.</p>

<p>Enter: garbage collected reference counting.</p>

<p>The core idea of GCRC is simple; strongly store the object, and give out weak handles that reference back to the object. The handles can be destroyed freely without destroying any of the object’s information. When no handles remain, run the cleanup code manually, just like any other reference counting technique.</p>

<p>Here’s what that looks like for a simplified example with only one handle:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="k">function</span> <span class="nf">run_on_gc</span><span class="p">(</span>
	<span class="n">object</span><span class="p">:</span> <span class="n">unknown</span><span class="p">,</span> 
	<span class="n">callback</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="p">()</span>
<span class="p">)</span>
	<span class="kd">local</span> <span class="n">alive_test</span> <span class="o">=</span> <span class="nb">setmetatable</span><span class="p">({</span><span class="n">object</span><span class="p">},</span> <span class="p">{</span><span class="n">__mode</span> <span class="o">=</span> <span class="s2">"v"</span><span class="p">})</span>
	<span class="n">task</span><span class="p">.</span><span class="n">spawn</span><span class="p">(</span><span class="k">function</span><span class="p">()</span>
		<span class="k">repeat</span> <span class="n">task</span><span class="p">.</span><span class="n">wait</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="k">until</span> <span class="n">alive_test</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">~=</span> <span class="n">object</span>
		<span class="n">callback</span><span class="p">()</span>
	<span class="k">end</span><span class="p">)</span>
<span class="k">end</span>

<span class="kd">local</span> <span class="k">function</span> <span class="nf">ref_counter</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span>
	<span class="n">callback</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="p">()</span>
<span class="p">):</span> <span class="p">{}</span>
	<span class="kd">local</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">table</span><span class="p">.</span><span class="n">freeze</span> <span class="p">{}</span>
	<span class="n">run_on_gc</span><span class="p">(</span><span class="n">handle</span><span class="p">,</span> <span class="n">callback</span><span class="p">)</span>
	<span class="k">return</span> <span class="n">handle</span>
<span class="k">end</span>

<span class="kd">local</span> <span class="k">function</span> <span class="nf">make_person</span><span class="p">(</span>
	<span class="n">name</span><span class="p">:</span> <span class="n">string</span>
<span class="p">)</span>
	<span class="kd">local</span> <span class="n">self</span> <span class="o">=</span> <span class="p">{</span>
		<span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="p">,</span>
		<span class="n">age</span> <span class="o">=</span> <span class="nb">math.random</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="kd">local</span> <span class="k">function</span> <span class="nf">clean_up_person</span><span class="p">()</span>
		<span class="nb">print</span><span class="p">(</span><span class="err">`</span><span class="p">{</span><span class="n">self</span><span class="p">.</span><span class="n">name</span><span class="p">}</span> <span class="p">(</span><span class="n">aged</span> <span class="p">{</span><span class="n">self</span><span class="p">.</span><span class="n">age</span><span class="p">})</span> <span class="n">has</span> <span class="n">been</span> <span class="n">cleaned</span> <span class="n">up</span><span class="err">`</span><span class="p">)</span>
	<span class="k">end</span>

	<span class="kd">local</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">ref_counter</span><span class="p">(</span><span class="n">clean_up_person</span><span class="p">)</span>
	<span class="n">handle</span><span class="p">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">self</span>
	
	<span class="k">return</span> <span class="n">handle</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The user of the code will have to access the data through the handle:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">do</span>
	<span class="kd">local</span> <span class="n">person</span> <span class="o">=</span> <span class="n">make_person</span><span class="p">(</span><span class="s2">"Allison"</span><span class="p">)</span>
	<span class="n">task</span><span class="p">.</span><span class="n">wait</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
	<span class="nb">print</span><span class="p">(</span><span class="s2">"I am"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="s2">"and I am"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">value</span><span class="p">.</span><span class="n">age</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1">-- clean_up_person() runs here!</span>
</code></pre></div></div>

<p>However, they should not hold onto the data strongly themselves, because the handle would be dropped immediately:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">do</span>
	<span class="kd">local</span> <span class="n">person</span> <span class="o">=</span> <span class="n">make_person</span><span class="p">(</span><span class="s2">"Allison"</span><span class="p">).</span><span class="n">value</span>
	<span class="n">task</span><span class="p">.</span><span class="n">wait</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="c1">-- clean_up_person() could run here!</span>
	<span class="nb">print</span><span class="p">(</span><span class="s2">"I am"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="s2">"and I am"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">age</span><span class="p">)</span> <span class="c1">-- kaboom</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This can be prevented by changing <code class="language-plaintext highlighter-rouge">make_person</code> to provide indirect methods rather than direct data access:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="k">function</span> <span class="nf">make_person</span><span class="p">(</span>
	<span class="n">name</span><span class="p">:</span> <span class="n">string</span>
<span class="p">)</span>
	<span class="kd">local</span> <span class="n">self</span> <span class="o">=</span> <span class="p">{</span>
		<span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="p">,</span>
		<span class="n">age</span> <span class="o">=</span> <span class="nb">math.random</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="kd">local</span> <span class="k">function</span> <span class="nf">clean_up_person</span><span class="p">()</span>
		<span class="nb">print</span><span class="p">(</span><span class="err">`</span><span class="p">{</span><span class="n">self</span><span class="p">.</span><span class="n">name</span><span class="p">}</span> <span class="p">(</span><span class="n">aged</span> <span class="p">{</span><span class="n">self</span><span class="p">.</span><span class="n">age</span><span class="p">})</span> <span class="n">has</span> <span class="n">been</span> <span class="n">cleaned</span> <span class="n">up</span><span class="err">`</span><span class="p">)</span>
	<span class="k">end</span>

	<span class="kd">local</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">ref_counter</span><span class="p">(</span><span class="n">clean_up_person</span><span class="p">)</span>
	<span class="k">function</span> <span class="nc">handle</span><span class="p">.</span><span class="nf">name</span><span class="p">()</span>
		<span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">name</span>
	<span class="k">end</span>
	<span class="k">function</span> <span class="nc">handle</span><span class="p">.</span><span class="nf">age</span><span class="p">()</span>
		<span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">age</span>
	<span class="k">end</span>
	
	<span class="k">return</span> <span class="n">handle</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, users are forced to intermediate all data access with the handle, so it can’t be accidentally dropped without also dropping access to the data:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">do</span>
	<span class="kd">local</span> <span class="n">person</span> <span class="o">=</span> <span class="n">make_person</span><span class="p">(</span><span class="s2">"Allison"</span><span class="p">)</span>
	<span class="n">task</span><span class="p">.</span><span class="n">wait</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span>
	<span class="nb">print</span><span class="p">(</span><span class="s2">"I am"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">name</span><span class="p">(),</span> <span class="s2">"and I am"</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">age</span><span class="p">())</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This “private data” concept maps well to local variables in closure-based OOP:</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">local</span> <span class="k">function</span> <span class="nf">make_person</span><span class="p">(</span>
	<span class="n">name</span><span class="p">:</span> <span class="n">string</span>
<span class="p">)</span>
	<span class="kd">local</span> <span class="n">age</span> <span class="o">=</span> <span class="nb">math.random</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>

	<span class="kd">local</span> <span class="k">function</span> <span class="nf">clean_up_person</span><span class="p">()</span>
		<span class="nb">print</span><span class="p">(</span><span class="err">`</span><span class="p">{</span><span class="n">name</span><span class="p">}</span> <span class="p">(</span><span class="n">aged</span> <span class="p">{</span><span class="n">age</span><span class="p">})</span> <span class="n">has</span> <span class="n">been</span> <span class="n">cleaned</span> <span class="n">up</span><span class="err">`</span><span class="p">)</span>
	<span class="k">end</span>

	<span class="kd">local</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">ref_counter</span><span class="p">(</span><span class="n">clean_up_person</span><span class="p">)</span>
	<span class="k">function</span> <span class="nc">handle</span><span class="p">.</span><span class="nf">name</span><span class="p">()</span>
		<span class="k">return</span> <span class="n">name</span>
	<span class="k">end</span>
	<span class="k">function</span> <span class="nc">handle</span><span class="p">.</span><span class="nf">age</span><span class="p">()</span>
		<span class="k">return</span> <span class="n">age</span>
	<span class="k">end</span>
	
	<span class="k">return</span> <span class="n">handle</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The technique can be generalised to support multiple handles being created at once, but that isn’t done in these examples for brevity.</p>

<p>While GCRC makes the resource management concerns invisible to the end user, they do necessitate a polling loop, which could be undesirable and may introduce extra latency between the time an object is dropped by user code, and the time the cleanup code actually runs, potentially making it unsuitable for external IO like file locks or network request closing.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[GCRC is a novel resource management technique for Luau that works around the lack of __gc metamethod using reference counted handles.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/garbage-collected-reference-counting-in-luau/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/garbage-collected-reference-counting-in-luau/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Please, no more “UI frameworks”</title><link href="https://fluff.blog/2024/12/26/please-no-more-ui-frameworks.html" rel="alternate" type="text/html" title="Please, no more “UI frameworks”" /><published>2024-12-26T00:00:00+00:00</published><updated>2024-12-26T00:00:00+00:00</updated><id>https://fluff.blog/2024/12/26/please-no-more-ui-frameworks</id><content type="html" xml:base="https://fluff.blog/2024/12/26/please-no-more-ui-frameworks.html"><![CDATA[<p>Roblox UI is an island far removed from the rest of the engine. This is my manifesto: focusing so heavily on user-space client-centric UI-first frameworks has damaged how we talk about the future of Roblox and the Luau ecosystem.</p>

<p>I’ll be tackling this post from a technical perspective, but <a href="https://fluff.blog/2022/11/10/ui-frameworks-dont-serve-designers.html">I’ve written a few years ago about even more problems with these frameworks from a low-code/designer perspective too.</a></p>

<h2 id="ui-is-not-a-unique-problem">UI is not a unique problem</h2>

<p>We developed UI frameworks to allow us to define a simple source of truth, and derive a more complex set of consequences from it. Fundamentally, this is all that React, Fusion, and other frameworks try to do. Yet, it’s hardly the first time we’ve been confronted with this problem in the engine.</p>

<p>Consider the example of reusable model packages. Say I want to distribute a car on the Creator Store as a 3D model, built of multiple meshes. I want to give users an artist-friendly “paint colour” attribute so that they can configure the colour of the car’s meshes without reaching inside and making invasive modifications to each mesh.</p>

<p>Other benefits include (non-exhaustively):</p>
<ul>
  <li>Efficient replication - a minimal amount of state can be driven from the server, and clients can infer the rest, reducing bandwidth usage</li>
  <li>Frictionless updates - the attributes can be configured externally without touching any internal instances, so there are no conflicting changes when upgrading to a new package version</li>
  <li>Fluent controls - a low-code user of this package can easily discover all of the axes it can be customised along, and can immediately tweak and see the effect of those controls, improving iteration times and encouraging asset variation</li>
</ul>

<p>I’d feel pretty confident in saying that packages would benefit from being able to derive at least some state from a simpler set of inputs. The car would obviously still have other internal state, such as the velocity of the different assemblies, but as the user, we don’t really need to concern ourselves with that.</p>

<p>If this sounds familiar, it’s because I believe that packages with attributes are isomorphic to modern-day UI components - it’s almost exactly the same state derivation problem, and is amenable to equivalent solutions.</p>

<p>UI is not special - it’s merely the same problem at scale. Any well-done solution that solves one of these problems should solve the other, but no attention has been given to this at all.</p>

<h2 id="luau-is-fundamentally-compromised">Luau is fundamentally compromised</h2>

<p>One of the most difficult problems in Fusion’s history has been the memory management problem; how can we make sure that resources allocated in state objects get destroyed properly when they’re recomputed, without destroying resources that are still in use? In particular, this is relevant for resources like instances which require explicit manual cleanup (e.g. unparenting or destruction).</p>

<p>Since the <code class="language-plaintext highlighter-rouge">__gc</code> metamethod is absent, the only thing Luau can do is respond to the absence of a resource handle (e.g. by polling a weak table), at which point it’s too late to perform logic involving that resource. You could log a generic message, perhaps, but the data stored inside that handle is gone, often making it impossible to take a new reference to whatever was on the other side (with the notable exception of instances, where the data model allows fresh handles to be obtained if you’re <em>really</em> persistent).</p>

<p>This always-early-or-late problem haunted Fusion for a very long time. No matter how hard I looked, I couldn’t find a way to square this circle. It’s a fundamental compromise of Luau, and I have left many notes on this blog over the years to warn you that you won’t solve it either.</p>

<p>In the end, I worked around it by introducing structured object lifetimes to Fusion, in the form of scopes. Famously, scopes are very easy to teach and nobody asks questions about them.</p>

<p>But all I can think of when I reflect on this problem, is just how entirely unnecessary it is. It’s an invented problem caused by a limited API surface. I have good reason to believe that we could offer a simpler and more understandable solution to this kind of problem if only the implementation lived lower down the tech stack, where the resources actually live.</p>

<p>This is just one of the downsides of building complex UI frameworks in user space, but it’s especially salient whenever people talk about post-React framework design. I don’t think it’s possible to combine granular reactivity with managed resources like instances without running headfirst into exactly this problem, and we will only be left with unsatisfying solutions.</p>

<h2 id="manual-replication-is-un-robloxian">Manual replication is un-Robloxian</h2>

<p>We appropriated today’s attitude towards building Roblox UI directly from web conventions around single-page app (SPA) architecture. As the web world has learned over time though, there are tangible benefits to leveraging the server side more fully, especially when dealing with state that needs to be protected by server authority, and there is a lot of room to make the client/server divide more transparent and easier to work with.</p>

<p>UI shouldn’t require a special kind of script to be properly constructed. I think Roblox had something good going with server-side UI control, because it naturally allows the game state to live on the server, where there is one authoritative world view, making it easy to integrate with other game state and easy to control securely. It avoids the mental overhead of having to manage a client/server divide, and it’s my belief that we missed out on a better API design by heavily restricting the server side from being involved in user input and rendering.</p>

<p>The only reason we need client-side UI in 90% of use cases is to improve responsiveness in certain cases; for example low-latency responses to user input or smooth animation sequences. My view is that those are solvable design problems; even without adopting some advanced server authority system, you could <em>at least</em> replicate animations more smartly than how <code class="language-plaintext highlighter-rouge">TweenService</code> does things today, and you’d see meaningful payoff even if more complex actions would still have high latency.</p>

<p>Ultimately, I just think the engine needs to get smart about scheduling work to be done on clients at a fundamental level. Remotes and local scripts were a mistake in my eyes - sure, they’re a good power user tool for those who really need them, but we shouldn’t have ever shuttled novice developers through these mechanisms by force.</p>

<p>Would this require abandoning some purity around consistent worldviews? Would it mean that clients could get out of sync because they played animations at slightly different times, or processed inputs slightly differently from the server? Sure! But let’s not pretend like we ever had that - the speed of light already guarantees that clients have inconsistent worldviews due to networking. The sooner we stop trying to be precious about something we don’t have, and just implement proper rollback and server authority mechanisms, the better for the user.</p>

<p>Roblox UI should be following the example of the web and better learn how to leverage Roblox’s always-available server authority and cloud compute. We have a great replication and networking model with the potential to make server authority easy to reason about. Let’s lean into that rather than building yet another framework for SPAs with a complex manual networking layer.</p>

<h2 id="it-is-time-for-a-better-way">It is time for a better way</h2>

<p>It’s now my full belief that we should be spending less time ogling at Luau frameworks and more time investing into the fundamental design of the Roblox engine. That’s not to say that Luau frameworks have no place in a future Roblox, but they should not be a prerequisite tool to achieve reasonable UI goals.</p>

<p>We should take what we’ve learned about the way we want to build UI, and apply it by introducing key engine features to make the fundamental experience of building UI simpler. We are long overdue proper tools for componentisation and for managing the client/server boundary.</p>

<p>It’s also my belief that many of the modern paradigms we want to adopt, such as granular reactivity, would benefit from having lower-level implementations - not merely for some mythical language speedup (runtime complexity matters more), but because they would benefit from proper engine coordination in a way that would meaningfully reduce user-space mental overhead.</p>

<p>I’m thinking about following up this post with some thoughts about a mental model for UI that extends the skeuomorphic idea of bindings into a proper engine-level primitive, in a way that might even be compatible with actor-style parallelism. I’ll probably stew on it some more though, because I’m torn on a few design problems at the moment.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Roblox UI is an island far removed from the rest of the engine. This is my manifesto: focusing so heavily on user-space client-centric UI-first frameworks has damaged how we talk about the future of Roblox and the Luau ecosystem.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/please-no-more-ui-frameworks/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/please-no-more-ui-frameworks/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Passthrough isn’t dead</title><link href="https://fluff.blog/2024/11/08/passthrough-isnt-dead.html" rel="alternate" type="text/html" title="Passthrough isn’t dead" /><published>2024-11-08T00:00:00+00:00</published><updated>2024-11-08T00:00:00+00:00</updated><id>https://fluff.blog/2024/11/08/passthrough-isnt-dead</id><content type="html" xml:base="https://fluff.blog/2024/11/08/passthrough-isnt-dead.html"><![CDATA[<p>Meta’s AR tech demo made the Vision Pro look ancient - but that doesn’t mean Apple bet on the wrong horse.</p>

<p>I believe that passthrough VR might not be so far off what we end up doing with AR glasses in the future, to address one of their fundamental weaknesses.</p>

<p>Let me explain.</p>

<h2 id="normal-optics">Normal optics</h2>

<p>I won’t spend too long on this part, because it’s well-trodden territory. Let’s start with a normal VR device, and look at how it gets light into your eyes.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/quest-pro.jpg" alt="Wearing the Quest Pro." /></p>

<p>In front of each of your eyes, you have a tiny display, which emits light to form an image, just like any other display you’ve ever used on a computer.</p>

<p>However, the display will be much too close to your eyes for you to get a clear image on your retina, because your eyes expect light to be coming in from much further away. So, a lens is used to shape the light rays so that the image is clear when it hits your retina.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/vr-diagram.png" alt="A simplified diagram of how VR optics work." /></p>

<p>That’s it… minus all the tiny details required to land you a nice job as a hardware designer.</p>

<p>Fundamentally, every headset on the market - including the Vision Pro - work this way. There’s no way for you to look “through” the headset at all; you are looking at a display.</p>

<p>So how come you can see the world outside of your Vision Pro?</p>

<h2 id="how-passthrough-works">How passthrough works</h2>

<p>The answer is a technology called “passthrough”. The idea is simple; use cameras to show what the outside world looks like on the displays. If you do it right, then the light going to your eyes should look the same as the normal light that’d have come from the room.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/vr-passthrough-diagram.png" alt="A simplified diagram of how VR passthrough works." />
This is how the Vision Pro works. It uses a bunch of camera feeds to figure out what light would normally be reaching your eyes. Then, it uses some reverse logic to figure out what part of the display should emit that light. When the display shines that light, it hits your eye from the correct angle and makes it look like you’re staring at your room, rather than a headset.</p>

<p>This approach has many upsides - mainly, you get full control over what light goes to your eyes. Because the light is blocked by the headset, it’s up to the display to recreate all the light that should be coming in. But of course, that also means you can modify the light, painting on your own objects and scenes to make them look real.</p>

<p>The Vision Pro decides to paint on a bunch of glassy apps and 3D objects to make it look like you have multiple large displays floating in your room.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/vision-os.jpg" alt="A photo of visionOS." /></p>

<p>Still with me so far?</p>

<h2 id="how-ar-glasses-work">How AR glasses work</h2>

<p>Let’s now talk about how Meta’s AR glasses work. Meta would like you to think that nothing else like them exists, but there’s a few companies making AR glasses today.</p>

<p>Here’s my favourite pair, the XReal Air 2 Pro glasses.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/xreal-air.jpg" alt="Wearing the XReal Air 2 Pro." /></p>

<figure class="aside">
    <img src="/assets/posts/passthrough-isnt-dead/xreal-underside.jpg" alt="The underside of the XReal glasses." />
</figure>
<p>These are actually based on a similar concept to VR headsets. The goal is to shine light from a display into your eyes, with the light coming in at just the right angle to look like it’s coming from inside of the room.</p>

<p>If you look at what’s behind the front glass, you’ll see a pair of transparent glass prisms. These perform a similar task to the VR headset lenses from before; they shape the light coming into your eyes from a display.</p>

<p>A display? What display?</p>

<p>With enough effort, you can find it. Look up under the glasses while you’re sending an image to them, and you’ll see a pair of tiny OLED displays embedded in the top of the frame, seen through the prism.</p>

<p>These displays shine downwards, reflecting off of the slanted glass prism and shooting off towards your eyes, with the same carefully-crafted shape you’d get from a VR headset.</p>

<p>If you get up close with a camera, you can see just how small these displays really are - yet they’re 1080p!</p>

<p><img src="/assets/posts/passthrough-isnt-dead/xreal-display.jpg" alt="Wearing the XReal Air 2 Pro." /></p>

<p>This approach has many downsides, which I won’t elaborate on for now. However, it has one very large upside - there is nothing in front of you except for a transparent glass prism, so you can let in <em>real</em> light. You can see the room you’re in <em>as well as</em> the light coming from the displays!</p>

<p><img src="/assets/posts/passthrough-isnt-dead/ar-diagram.png" alt="A simplified diagram of how AR optics work." /></p>

<h2 id="the-occlusion-problem">The occlusion problem</h2>

<p>This setup works marvellously, except for one slight issue.</p>

<p>Because we are letting light in from the room, that light is mixing with the light from the displays. More specifically, the display can only add light on top - there’s no way to block any of the light that’s coming in from the room. This means that everything you show on the display will look “ghostly”.</p>

<p>As an example, here’s what I see when I open up XReal’s app.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/ghosting.jpg" alt="Ghostly UI elements in XReal's app." /></p>

<p>Look behind the large tiles, and you’ll notice that you can see my lights and wall decorations through them. Everything is slightly semi-transparent, because the light from the display is mixing with the light from the room, a bit like when you look through a window and see your reflection mix with the room on the other side of the window.</p>

<p>This is what I call the “occlusion problem” - you can’t block out any of the light from the room. In particular, this means that optical AR tends to struggle outdoors in sunlight or in bright rooms.</p>

<p>Currently, XReal’s solution to this is to use electrochromic dimming on the front glass, blocking the light to create a dark backdrop so you can isolate the digital content. This kills the utility of these things as AR glasses, but it means you can see your content clearly. It’s basically a VR mode.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/xreal-dimming.jpg" alt="XReal glasses at minimum and maximum dimming." /></p>

<p>Would it be possible to dim only parts of the glass, perhaps like an LCD display? Yes, but there’s another problem at play here - the glass is so close to you that it will be out of focus.</p>

<p>Your eyes will be focused on the contents of the room (and the light coming from the display as if it was in the room), so the glasses themselves will appear blurry. That includes the front glass that’ll be doing the dimming.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/blurry.jpg" alt="The glasses look blurry when you look through them." /></p>

<p>This means, at best, you’ll only be able to do very approximate local dimming. This might be enough for improving legibility, but it won’t give you any crisp edges on objects. Everything would either look like it has an aura of darkness, or it’d have a soft, ghostly faded edge.</p>

<p>Solving this problem likely requires somehow blocking or interfering with the incoming light in some really precise, tightly-calibrated way. Nobody really knows how to do this yet.</p>

<p>And yet, If we don’t solve this problem, then the quality of optical AR will never match the experience of a headset using passthrough.</p>

<p>So, what to do?</p>

<h2 id="solving-the-occlusion-problem">Solving the occlusion problem</h2>

<p>High-end AR glasses, like the more modern XReal Air 2 Ultra glasses, now come with integrated cameras for position and hand tracking. It’s not hard to imagine that these cameras could one day capture the appearance of your surroundings; after all, we have great cameras in our phones right now, and we’re surprisingly good at miniaturising technology.</p>

<p><img src="/assets/posts/passthrough-isnt-dead/new-ar-glasses.jpg" alt="The XReal Air 2 Ultra and Meta Orion smart glasses." /></p>

<p>So, my proposal is this: when we have the technology to provide a large-FOV pair of glasses with a dimmable front surface, and when we have the technology to capture what the environment looks like, what’s stopping us from doing passthrough like VR headsets do?</p>

<p><img src="/assets/posts/passthrough-isnt-dead/ar-passthrough.png" alt="A simplified diagram of how AR passthrough could work." />
To me, blending “natural light” and “display light” is a pragmatic way to solve the problem. While you would need well-calibrated and decent-looking cameras, and you’d need to make sure that things align decently well between the two sources of light, this could end up being an interesting solution to the occlusion problem.</p>

<p>AR passthrough is strong because software is strong. With passthrough VR, we have experience stitching together images on-the-fly. The Vision Pro already shows good competency with image processing here.</p>

<p>Passthrough also avoids the trap of special hardware. While it may be possible to solve this problem with novel methods, those solutions will tend to be expensive and bespoke. Meanwhile, camera systems and dimmable glass are both transferable across product categories, and likely will be included with the product anyway. You don’t need specialised support beyond perhaps having a better array of cameras.</p>

<p>The best part is that the problems with approximate local dimming can be compensated for. Instead of having a shadowy aura around virtual objects, input from the cameras can be used to compensate for the decrease in brightness around objects. The end result is a crisp, sharp edge.</p>

<p>Admittedly, I’m biased as a computer graphics guy, but having thought about this for a long time, I don’t see any simpler solution without inventing wild new hardware.</p>

<h2 id="conclusion">Conclusion</h2>

<p>So, was Apple wrong to invest so much into the Vision Pro? Surprisingly, I think it was a good idea.</p>

<p>By going with a passthrough system from the beginning, Apple can now invest in their passthrough processing pipeline and refine the quality of the output. Simultaneously, they can move towards smaller and smaller camera hardware. And when they do release glasses, they will have a head start on solving the occlusion problem.</p>

<p>There are some caveats to this idea, of course. For one, it would require that you can display a high-FOV image, which seems to be within the realm of possibility at this point given the recent advances by Meta.</p>

<p>It’d also require good quality camera feeds, which is both a hardware and image processing challenge. It’s possible that dedicated image processing silicon could make the leap to AR glasses, but it’s not a certainty given the space restraints, even if/when a computing puck is introduced.</p>

<p>And of course, there’s always the possibility that something genuinely new comes along and makes this all possible. Optics isn’t a solved field, after all.</p>

<p>But overall? I would not be surprised if passthrough AR was the direction things went, even for optical AR glasses. At least, that’s my take on it as a nerd.</p>

<p>I’m eager to hear other ideas.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Meta’s AR tech demo made the Vision Pro look ancient - but that doesn’t mean Apple bet on the wrong horse.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://fluff.blog/assets/posts/passthrough-isnt-dead/thumb.jpg" /><media:content medium="image" url="https://fluff.blog/assets/posts/passthrough-isnt-dead/thumb.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>