<?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://darshanmakwana412.github.io/feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://darshanmakwana412.github.io/" rel="alternate" type="text/html" />
  <updated>2026-10-15T23:25:54+05:30</updated>
  <id>https://darshanmakwana412.github.io/feed.xml</id>

  
  
    <title type="html">Darshan’s blog</title>
  

  
    <subtitle>code for my public webpage: https://darshanmakwana412.github.io</subtitle>
  

  
    <author>
      <name>Darshan Makwana</name>
    </author>
  

  
  
  
    <entry>
      

      <title type="html">Coding Agents Make Careful Engineering Cheap</title>
      <link href="https://darshanmakwana412.github.io/2026/10/coding-agents-make-careful-engineering-cheap/" rel="alternate" type="text/html" title="Coding Agents Make Careful Engineering Cheap" />
      <published>2026-10-15T00:00:00+05:30</published>
      <updated>2026-10-15T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/10/coding-agents-make-careful-engineering-cheap</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/10/coding-agents-make-careful-engineering-cheap/"><![CDATA[<p>I think there are two ways in which people have been designing systems. The first approach is that you understand what the requirement is and then you try to build individual components to meet the high level vision of what you want to build. Essentially trying to correlate the input and output requirements of each of the components and how do they interact with each other and carefully building each component individually and then piecing together the entire system together</p>

<p>The second approach is you build the minimal version which works. You try to see if it meets the requirement or not. If it meets it, it’s fine and you’re good. If it does not, you go back to the whiteboard and try to understand what the system is lacking. You try to break it down into more components if it’s possible or you can just keep on improving the current one. Once you are satisfied with the progress you gauge if what you built meets your purpose, if not you keep on doing it until it fits your requirement</p>

<p>There are different constraints or timelines, quality requirements, resources, or bandwidth, which allow you to make the decision of either going with the first approach or the second approach. For example: if you are a startup that is building something, then your first instinct should be to just build the version that you had in mind and then see how the society reacts to it. But if you are, let’s say, a space organisation which wants to send a satellite or a piece of equipment to, let’s say, Mars or the space station, you really can’t just build the first thing which pops into your mind and then send it across (because the cost of failure is so high that you really can’t afford to lose). You have to make sure that every individual component of the system which you are building is end-to-end stress tested and that it works correctly and it’s not susceptible to failure. If you are a programmer then you can either go with a top-down approach or a bottom-up approach to building your application</p>

<p>I think the systems which are built using the first kind of approach have a really good property: they are much easier to maintain compared to the systems which are built using the second approach. I think because every individual component in the first approach has been primarily studied under various conditions, their properties as well as the way in which they behave are clearly understood. It becomes very much easier to then understand how the system would behave or evolve when put in some different situations or conditions. We might have some hypothesis about which components are susceptible to failure in these conditions or which components will stop working after a certain period of time. It’s also much easier to change or completely swap out any of the components or change the way in which the system behaves altogether</p>

<p>With the rise in coding agents it seems to be that the approaches of the second kind are the ones which are going to be used by everyone. But there is a greater incentive to build systems using approaches of the first kind, using these AI coding agents or tools, it’s much easier to study every individual component and map out the relations (like mapping out the way in which they are going to behave under different conditions) or study them in isolated or paired scenarios. It’s much easier to consolidate all of this knowledge in, let’s say, some skills or markdown files, and then share it across other teams or other agents of the same kind</p>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="coding-agents" />
      
        <category term="engineering" />
      
        <category term="systems-design" />
      

      
      
        <summary type="html"><![CDATA[I think there are two ways in which people have been designing systems. The first approach is that you understand what the requirement is and then you try to build individual components to meet the high level vision of what you want to build. Essentially trying to correlate the input and output requirements of each of the components and how do they interact with each other and carefully building each component individually and then piecing together the entire system together]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">Extracting Steering Vectors from the J Space</title>
      <link href="https://darshanmakwana412.github.io/2026/09/extracting-steering-vectors-from-j-space/" rel="alternate" type="text/html" title="Extracting Steering Vectors from the J Space" />
      <published>2026-09-04T00:00:00+05:30</published>
      <updated>2026-09-04T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/09/extracting-steering-vectors-from-j-space</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/09/extracting-steering-vectors-from-j-space/"><![CDATA[<p>I was reading about the <a href="https://transformer-circuits.pub/2026/workspace/index.html">jacobian space</a> and how it can be used to verbalize the intermediate activations of an LLM to decode what it is most likely going to say or is thinking about. I wanted to test if we can use the j lens to arrive at a general <a href="https://www.lesswrong.com/posts/ndyngghzFY388Dnew/implementing-activation-steering">activation steering vector</a> from a couple of tokens related to the concept towards which we wanted to steer the model i.e inverting the j lens to have a general method of finding steering vectors from the concept tokens</p>

<p>Surprisingly I found really good evidence that J space can be used to derive steering vectors from just concept tokens which are represented in the steering behaviour. It works really well for simple model behaviours such as outputting everything in all caps or speaking in a weird manner. However the steering vector derived in this manner is prone to hallucinations and is brittle for behaviours which are complex and cannot clearly be represented with just tokens/words</p>

<h1 id="setup">Setup</h1>

<p>For all experiments I used <a href="https://huggingface.co/Qwen/Qwen3-1.7B">Qwen3-1.7B</a> which already has a published J lens by neuronpedia at <a href="http://neuronpedia/jacobian-lens">neuronpedia/jacobian-lens</a> huggingface repo. I ran all the experiments reported locally on my macbook, this was also a reason why I couldn’t test larger models<br />
Code is publicly avl at <a href="https://github.com/darshanmakwana412/jlens_steer">jlens_steer</a></p>

<h1 id="executive-summary">Executive Summary</h1>

<p>I wanted to first set a good enough baseline with some working steering vectors which I can use for comparison. I found the <a href="https://huggingface.co/science-of-finetuning/steering-vecs-qwen3_1_7B">science-of-finetuning/steering-vecs-qwen3_1_7B</a> repo which had a steering vector which steered the model to answering all tokens in all caps. This was fitted the expensive way from a model organism finetuned to answer in capitals. Along the way I also found someone published an <a href="https://huggingface.co/mlabonne/Qwen3-1.7B-abliterated">abliterated model for qwen3-1.7b</a> with refusal behaviour. <a href="https://huggingface.co/blog/mlabonne/abliteration">Abliteration</a> works by finding the direction inside the model that means refusal, then subtracting it from the parts of each layer that write into the model’s running state. So the difference between the abliterated model and the base model is just that direction, applied over and over. I subtracted the two models and pulled each layer’s change apart to get back the steering vector for refusal</p>

<p>Here is the result of the model steering</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th> </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/assets/images/posts/jlens-steering_image1.png" alt="Fitted caps steering vector: share of generated tokens in ALL CAPS vs steering coefficient at layer 13" /></td>
      <td><img src="/assets/images/posts/jlens-steering_image2.png" alt="Refusal direction recovered from the abliterated model: share of prompts refused vs steering coefficient at layer 15" /></td>
    </tr>
  </tbody>
</table>

<p>Alright so our baseline comparison steering vectors work fine</p>

<h2 id="steering-for-all-caps-behaviour">Steering for All Caps Behaviour</h2>

<p>Now let us come back to the J space, since the J space is just a linear mapping between the activations at layer L to the unembedding matrix let us find all pairs of all caps tokens and their corresponding lowercase tokens in the vocabulary like (“ AND”, “ and” or “ TOWN”, “ town”) and invert the rows of J lens corresponding to those tokens</p>

<p>This will evidently give us the activation vector at that layer L which would have verbalized that token according to J lens. Take all such pairs of activation vector and project them using PCA and let’s compare them with the activation vector which we got for the all caps steering</p>

<p><img src="/assets/images/posts/jlens-steering_image3.png" alt="PCA of the uppercase and lowercase token activations recovered from the J lens, against the ground truth caps steering vector" /></p>

<p>We find that the vector a_upper - a_lower derived from J lens is closer to the activation vector of all caps tokens. They also have very high cosine similarity score compared to replacing the J lens with an identity operation in which case it becomes just a <a href="https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens">logit lens</a></p>

<p><img src="/assets/images/posts/jlens-steering_image4.png" alt="Cosine similarity of a_UPPER - a_lower with the caps direction, J lens rows vs an identity lens" /></p>

<p>Let’s now take an average of all activation diffs of all such pairs and comparing it with our baseline earlier it definitely steers the model towards generating all caps tokens/words</p>

<p><img src="/assets/images/posts/jlens-steering_image5.png" alt="Share of generated tokens in ALL CAPS vs steering coefficient, fitted vector vs the vocab centered J lens vector" /></p>

<h2 id="refusal-steering">Refusal Steering</h2>

<p>Let us now generalize or atleast attempt to generalize this algorithm for steering a model to refuse even harmless prompts. For this we need to gather concept tokens that are related to refusal and words/tokens that might be occurring inside the model’s mind when it’s trying to refuse the prompt even though it’s harmless.</p>

<p>Let’s see what goes on inside the model using the j lens top 5 words when we steer the model using the refusal steering vector over the prompt “How do I bake a loaf of sourdough bread?”, the model answers “<code class="language-plaintext highlighter-rouge">I'm sorry, but I cannot assist with any illegal, unethical, or harmful...</code>”. The below some tokens which appeared when verbalized the activation using j lens</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th> </th>
      <th> </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/assets/images/posts/jlens-steering_image6.png" alt="J lens top tokens at layer 17, position 8" /></td>
      <td><img src="/assets/images/posts/jlens-steering_image7.png" alt="J lens top tokens at layer 22, position 10" /></td>
      <td><img src="/assets/images/posts/jlens-steering_image8.png" alt="J lens top tokens at layer 14, position 14" /></td>
    </tr>
  </tbody>
</table>

<p>As expected it’s thinks about words/tokens related to refusal behaviour. Inverting just a single refusal word/token with a neutral token did not work this time as the single refusal tokens could relate lots of concepts and inverting it does not have enough information passed via the J lens to arrive at a refusal activation vector</p>

<p>My immediate next attempt was to use 5 refusal words/tokens each and invert them together via the J lens, the intuition was this is a really good approximation of the activation that results in making the model think about those tokens, now generating more activations with similar sets of refusal tokens and taking an average of all such approximated activations would be the activation most likely to produce those words related to refusal in the model’s thinking/scratchpad and would give us the steering vector for refusal if we subtract it by the average activation over the activation of tokens which elicit</p>

<p>So the algorithm is:</p>

<ol>
  <li>Collect 20-30 words/tokens that relate/ellicit to the concept/behaviour towards which we want to steer the model</li>
  <li>Sample C=5 tokens with replacement from this above set, and compute an activation vector by inverting the jacobian over these concept tokens</li>
  <li>Repeat K times step 2 to collect K activation vector, and average all of them</li>
  <li>This is the activation vector most likely to produce refusals</li>
  <li>Substract this from the avg activation vector of the entire vocabulary to get the steering vector</li>
</ol>

<p>Now let’s compute this for refusals and stop at step 4 and then run the activation vector we got from step 4 and visualize what the model generates using that activation vector on arbitrary prompts</p>

<p><img src="/assets/images/posts/jlens-steering_image9.png" alt="Completions from the model run with the activation recovered from the J lens over refusal tokens" /></p>

<p>We observe that the model shows signs and behaviour of telling user not to do certain things. It also makes the model hallucinate more often only 1 in 5 prompts where the model showed genuine refusal behaviour but even that was where the model hallucinated and kept repeating itself. The activation is really noisy and brittle to use as it is. Let’s evaluate on the 10 prompts we evaluated our baseline steering vector on by subtracting the average activation we got after inverting the J lens over the entire vocabulary</p>

<p><img src="/assets/images/posts/jlens-steering_image10.png" alt="Share of harmless prompts refused vs steering coefficient, abliteration direction vs the direction derived from the J lens" /></p>

<p>It’s way behind the baseline score yet, the reparations are gone but it still hallucinates often or only mentions not to do certain things or stop things are mentioning them first. Direct refusal rates are still low</p>

<p>Reading the verbalized outputs of the steered model we see that the activation is making the model think about the tokens/words which were relating to our refusal behaviour<br />
<code class="language-plaintext highlighter-rouge">" even", " only", " but", " forbidden", " not", " the", " wrong", " in", " failed"</code><br />
but reading the above 5 completions also shows that the activation is forcing the model to think about these words/tokens which could and it is not directly transferring to make the model show refusal behaviour</p>

<p>The model hallucinates a lot more or make the outputs more narrow in the sense of making the model force to generate sentences relating those concept tokens and not the behaviour which corresponds to them</p>

<p>One major limitation for complex steering behaviours is they are very hard to represent via only tokens/words and using only the J space looses out on the information required to articulate or represent those behaviours in the activation space</p>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="llm" />
      
        <category term="interpretability" />
      
        <category term="steering" />
      
        <category term="jacobian-lens" />
      
        <category term="activation-steering" />
      

      
      
        <summary type="html"><![CDATA[I was reading about the jacobian space and how it can be used to verbalize the intermediate activations of an LLM to decode what it is most likely going to say or is thinking about. I wanted to test if we can use the j lens to arrive at a general activation steering vector from a couple of tokens related to the concept towards which we wanted to steer the model i.e inverting the j lens to have a general method of finding steering vectors from the concept tokens]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">On Peace and Ambition</title>
      <link href="https://darshanmakwana412.github.io/2026/08/on-peace-and-ambition/" rel="alternate" type="text/html" title="On Peace and Ambition" />
      <published>2026-08-22T00:00:00+05:30</published>
      <updated>2026-08-22T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/08/on-peace-and-ambition</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/08/on-peace-and-ambition/"><![CDATA[<p>When I was in my UG years I always used to feel a really deep discontent with myself, whenever I used to accomplish anything I used to tell myself I can achieve even more, my potential is beyond this, I can be more ambitious, I used to vitaly belittle myself</p>

<p>During the same time I was also reading some books and taking humanities electives which used to essentially advice we should have a kind “inner voice” and should cherrish and love ourself, I always felt these were coping mechanisms for the week, it’s like burrowing your head in the sand and acting as if the problem does not exist, cheap dopamins to dull the pain of reality around us, it’s better to embrace the pain I used to think, only the unambitious feel content with their life, I had seen the most successful people around me and the people whom I admired have an intense self belief and conviction fueled by an intense desire to be good enough</p>

<p>I am really really young right now, in my early twenties but I would say my stance has changed somewhat than the younger version of myself. Ambition does not mean you have to constantly diminish or belittle everything that you do, it’s very simply put the magnitude and impact of the end goals that you have decided for yourself, you can reach the same destination by taking a different path, <a href="/2026/06/framework-for-winning/">a framework that preserves your peace of mind and your conviction</a>, it could get to your goal faster or slower but it will eventually take you there. you can control what is within your control, let go of what is not and make peace with the universe, the universe if neither against you nor it is with you, it is just there existing in itself to fulfill it’s own purpose of existing for you</p>

<p>I have always measured life in terms of slope and intercepts, intercepts is essentially where you currently are and slope is your rate of change, the rate at which you are improving or working towards your ambitions or goals, eventually as the theory goes it does not matter what your intercept is what matters is your slope, a line with a greater slope will always cross over a line with a lesser slope</p>

<p>I don’t know but there has been this weird phenomenon which I have observed since the past 2 years where I have observed by slope has always increased, infact it has always been more than an exponential increase for me, in every aspect of life, I can list down more than 1000 improvements and signals/indicators which I have observed in myself, I have never felt so great ever in my life, mentally, physically and spiritually, exponentials have a weird property that their slope is also an exponential, so there is literally nothing that can slow down this insane exponential growth that I have unlocked</p>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="philosophy" />
      
        <category term="mindset" />
      
        <category term="ambition" />
      
        <category term="growth" />
      

      
      
        <summary type="html"><![CDATA[When I was in my UG years I always used to feel a really deep discontent with myself, whenever I used to accomplish anything I used to tell myself I can achieve even more, my potential is beyond this, I can be more ambitious, I used to vitaly belittle myself]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">Winning ECML 2026 Flatland with a SIPP Train Dispatcher</title>
      <link href="https://darshanmakwana412.github.io/2026/08/flatland-sipp-dispatcher/" rel="alternate" type="text/html" title="Winning ECML 2026 Flatland with a SIPP Train Dispatcher" />
      <published>2026-08-15T00:00:00+05:30</published>
      <updated>2026-08-15T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/08/flatland-sipp-dispatcher</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/08/flatland-sipp-dispatcher/"><![CDATA[<p>This is a consolidated writeup of my <a href="https://competition.flatland.cloud">ECML 2026 Flatland Real-World Baselines Challenge solution</a> that achieved rank 1 on the leaderboard. This writeup consolidates all the experimentations and implementation details from my obsidian vault, claude has been used to write parts of this writeup and structure the content, experiments and findings</p>

<h2 id="the-flatland-competition">The Flatland Competition</h2>

<p>Flatland is a train scheduling problem where we are given a railway, a fleet of trains with timetables, and we have to route every train to its destination without them crashing into each other. The competition fixes one railway topology, a 120x150 grid with 28 stations, and has 7 difficulty levels, 5 scenarios each, for 35 scored episodes. Each scenario spawns anywhere from 8 to 532 trains onto that same map. Here is what one scenario with 25 trains looks like in Flatland’s own renderer, cartoon trains and grass and all:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th> </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><img src="/assets/images/posts/flatland_hero_stock.png" alt="The same scenario in Flatland's own renderer, cartoon trains and grass" /></td>
      <td><img src="/assets/images/posts/flatland_hero.png" alt="The same scenario redrawn by flatviz as the graph the solver reasons over" /></td>
    </tr>
  </tbody>
</table>

<p>Every train starts on the map at some origin and has to reach a destination, which are the red stars, and the grey diamonds are switches. The dense knot of switches in the middle is a station throat, where a lot of routes funnel together and most of the interesting conflicts happen. Each cell has a set of allowed entry to exit direction transitions, which means a train entering a cell heading north can only leave in the directions that cell permits for a northbound entry, so most cells have exactly one exit, but a switch has a real routing choice:</p>

<p><img src="/assets/images/posts/flatland_junction.png" alt="A switch is a routing choice: each (cell, heading) is a graph node" /></p>

<p>A train at cell <code class="language-plaintext highlighter-rouge">(r, c)</code> heading east is a different search node from the same train heading west, because they can do different things next. We encode a node as <code class="language-plaintext highlighter-rouge">((r * W) + c) * 4 + d</code> and the whole planner lives on this directed graph of <code class="language-plaintext highlighter-rouge">4 * H * W</code> nodes, and a switch is just a node where some entry direction has more than one successor.</p>

<p>On top of the geometry sits a timetable. Each train has an earliest departure, a latest arrival, and a list of intermediate stops it is supposed to serve on the way, so think of an intercity that calls at three platforms before its terminus:</p>

<p><img src="/assets/images/posts/flatland_timetable.png" alt="One train's timetable: origin, intermediate stops, destination" /></p>

<p>Trains also move at different speeds, and Flatland speeds are fractions, so an intercity runs at <code class="language-plaintext highlighter-rouge">v = 1.0</code> which is one cell per step, a regional at <code class="language-plaintext highlighter-rouge">0.8</code>, a slow stopping service at <code class="language-plaintext highlighter-rouge">0.6</code>, and a commuter at <code class="language-plaintext highlighter-rouge">0.5</code>. A train at speed <code class="language-plaintext highlighter-rouge">p/q</code> banks <code class="language-plaintext highlighter-rouge">p/q</code> of distance every step and only leaves its cell once the banked distance reaches 1, so a <code class="language-plaintext highlighter-rouge">0.8</code> train needs two steps to leave its first cell, and because the leftover fraction carries over, some of its later cells clear in one step. The planner has to track this fraction exactly, because it needs to predict the precise step at which each cell frees up.</p>

<h3 id="scoring">Scoring</h3>

<p>The score is where the whole game is decided. Per scenario every agent piles up penalties (all rewards are <code class="language-plaintext highlighter-rouge">&lt;= 0</code>), and the normalized reward is</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>normalized = sum_over_agents( max(cum_reward_agent, -max_episode_steps) ) / (max_episode_steps * n_agents) + 1
</code></pre></div></div>

<p>so a perfect run scores 1.0 and a total disaster scores 0, and the competition score is just the sum of these over all 35 scenarios, so the max is 35. There is one hard rule on top of this, which is that evaluation aborts a whole level if fewer than 25% of trains finish it, so getting most trains home is a constraint and not just something nice to have.</p>

<p>The malfunctions ramp up with the levels, so levels 0 to 2 are clean, level 3 adds random breakdowns where a train freezes in place for some steps, levels 4 and 5 add random departure delays, and level 6 adds infrastructure disruptions on top of everything else. One important detail is that the evaluator does not run your policy live and trust it. Your container records a trajectory of actions with <code class="language-plaintext highlighter-rouge">flatland-trajectory-generate-from-policy</code>, and then a server-side <code class="language-plaintext highlighter-rouge">TrajectoryEvaluator</code> <strong>replays those actions</strong> through its own Flatland install to compute the score, so only the recorded actions matter. We ran our own trajectories through that exact code path so that there would be no surprise between the local score and the real one.</p>

<p>The RL baseline they ship, which is a FastTreeObs observation fed into an MLP policy trained on the example curriculum, scores <strong>15.55 out of 27 on that curriculum and gets 45.6% of trains home</strong>. That is the number to beat, and it is also the reason the organizers run a separate OR / non-RL track, because planners win Flatland, they always have, and the organizers know it.</p>

<h2 id="the-reward-function-and-the-one-number-that-matters">The reward function, and the one number that matters</h2>

<p>Before writing a single line of the planner we sat down and read <code class="language-plaintext highlighter-rouge">flatland.envs.rewards</code> for the exact competition parameters, because in a problem like this the cost function is the strategy. Here is the full reward book:</p>

<p><img src="/assets/images/posts/flatland_rewardbook.png" alt="The ECML2026 reward book and the one number that dominates it" /></p>

<p>Most of these are what you would expect. Canceling a train (never departing it) costs <code class="language-plaintext highlighter-rouge">5x</code> its shortest-path time, which is large, so you always launch every train. Arriving late costs <code class="language-plaintext highlighter-rouge">1</code> per step. Skipping an intermediate stop costs a flat <code class="language-plaintext highlighter-rouge">50</code>. But look at the top bar, because a <strong>collision costs <code class="language-plaintext highlighter-rouge">250 x speed</code></strong>, and here is the thing that took us a couple of hours of poking at the environment to actually believe:</p>

<blockquote>
  <p>In Flatland 4.2.6, which is the version the evaluator runs, <em>any</em> transition from MOVING to STOPPED is charged the full collision penalty. Not just crashes. A clean, deliberate, perfectly safe brake of a moving train costs <code class="language-plaintext highlighter-rouge">250 x v</code>.</p>
</blockquote>

<p>We checked this by hand instead of trusting the docstring (which says otherwise). Load a scenario, drive one intercity forward on empty track, brake it cleanly with nothing in front of it, and read the reward:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># one speed-1 train, clean voluntary brake, empty track ahead
</span><span class="n">env</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">RailEnvPersister</span><span class="p">.</span><span class="n">load_new</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">rewards</span><span class="o">=</span><span class="n">ECML2026Rewards</span><span class="p">())</span>
<span class="c1"># ... step it forward a few cells, then issue STOP_MOVING ...
# step_reward for that transition:  -250.0
</span></code></pre></div></div>

<p>It is <code class="language-plaintext highlighter-rouge">-250.0</code>, exactly. If you dig into <code class="language-plaintext highlighter-rouge">rail_env.py</code> the reason is that <code class="language-plaintext highlighter-rouge">AgentTransitionData.speed</code> is captured in the pre-step loop <em>before</em> the speed update, and the reward code multiplies the collision factor by that pre-step speed on every MOVING to STOPPED transition, whether it came from the motion check blocking you or from your own STOP action. Reaching the final target is a MOVING to DONE transition and is exempt, so arriving is free, but stopping short is not.</p>

<p>This one detail decides everything, and three things fall straight out of it.</p>

<p><strong>We skip every intermediate stop, on purpose.</strong> Serving a stop means the train has to actually halt at the platform, and a halt is a MOVING to STOPPED transition, so it costs <code class="language-plaintext highlighter-rouge">250 x v</code>, while skipping the stop costs a flat <code class="language-plaintext highlighter-rouge">50</code>. So serving only wins when <code class="language-plaintext highlighter-rouge">250 x v &lt; 50</code>, which is <code class="language-plaintext highlighter-rouge">v &lt; 0.2</code>, and every real train in this competition runs at <code class="language-plaintext highlighter-rouge">0.5</code> or faster:</p>

<p><img src="/assets/images/posts/flatland_skipstops.png" alt="Serve vs skip break-even by train speed" /></p>

<p>The break-even is at <code class="language-plaintext highlighter-rouge">v = 0.2</code> and every train category (commuter, stopping service, regional, intercity) sits above it, so the right move is to blow through every platform without stopping and just eat the <code class="language-plaintext highlighter-rouge">-50</code>. It feels wrong because an intercity is supposed to stop at its platforms, but the reward book says skip, so we skip. We left one environment flag, <code class="language-plaintext highlighter-rouge">PLANNER_SERVE_STOPS</code>, that flips this back, in case the organizers ever fix the braking bug during the competition, but they did not.</p>

<p><strong>We hold trains off the map instead of stopping them on it.</strong> A train that has not departed yet sits off the board for free with no penalty for waiting, but a train on the board that has to give way has to brake, which is <code class="language-plaintext highlighter-rouge">250 x v</code>. And delaying a departure by <code class="language-plaintext highlighter-rouge">D</code> steps only costs about <code class="language-plaintext highlighter-rouge">D</code> at the end, because late arrival is <code class="language-plaintext highlighter-rouge">-1</code> per step, so holding a train back is a slow linear cost while braking a moving one is a flat and expensive cost:</p>

<p><img src="/assets/images/posts/flatland_holdoffmap.png" alt="Holding a train off-map is free; braking it on-map is not" /></p>

<p>The two lines cross at <code class="language-plaintext highlighter-rouge">D = 250</code>, so you can hold an intercity off the map for 250 steps and still come out ahead of letting it brake even once. On a map where episodes only run a few hundred steps, that basically means never let a moving train stop if you can delay its departure instead.</p>

<p><strong>Minimize halts, full stop.</strong> Every MOVING to STOPPED is <code class="language-plaintext highlighter-rouge">-250 x v</code>, so the whole job of the planner, on top of getting trains home, is to get them home without ever touching the brakes. This changes what we are even searching for, because we are no longer looking for shortest paths, we are looking for paths that never have to stop, and this is exactly the problem that Safe-Interval Path Planning was made for.</p>

<h2 id="v1-sipp-over-a-reservation-table">V1: SIPP over a reservation table</h2>

<p>The standard way to schedule a lot of agents on shared track is prioritized planning with a reservation table. You order the trains and plan them one at a time, so each train plans around the reservations of all the trains planned before it and then writes its own path into the table. It is not globally optimal like a full Conflict-Based Search would be, but it scales to hundreds of agents and with a good priority order it gets very close.</p>

<p>The core of it is <a href="https://www.cs.cmu.edu/~maxim/files/sipp_icra11.pdf">Safe-Interval Path Planning</a>. The idea behind SIPP is that you should not search over <code class="language-plaintext highlighter-rouge">(cell, timestep)</code> because there are far too many timesteps, you should search over <code class="language-plaintext highlighter-rouge">(cell, safe interval)</code>, where a safe interval is a maximal window during which a cell is free. A reservation table is really a 2D object, resource on one axis and time on the other, where every occupancy is a rectangle, so the gaps in a resource’s column are its safe intervals, and a train’s plan is a staircase that climbs through those gaps:</p>

<p><img src="/assets/images/posts/flatland_sipp_spacetime.png" alt="SIPP threads a monotone staircase through the safe intervals" /></p>

<p>This picture is the whole algorithm. Our train wants to get from cell A to cell F, and another train (train 7) has reserved cell D for the window <code class="language-plaintext highlighter-rouge">[3, 9)</code>, so our train rolls through A and B and then has to wait, because it cannot enter D until D frees at <code class="language-plaintext highlighter-rouge">t=9</code>. SIPP holds it in cell C until the exact moment D opens and then threads through, and that wait is a single halt, marked with the X, which costs <code class="language-plaintext highlighter-rouge">250 x v</code>. The search is looking for the staircase that reaches the goal with the fewest halts, and after that with the earliest arrival.</p>

<p>The reservation table maps every resource to a sorted list of <code class="language-plaintext highlighter-rouge">(start, end, agent)</code> occupancies, and the core query hands back the free gaps:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">safe_intervals</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">res</span><span class="p">,</span> <span class="n">t_from</span><span class="p">):</span>
    <span class="s">"""Maximal free intervals of `res` intersecting [t_from, horizon)."""</span>
    <span class="n">lst</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">intervals</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">res</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">lst</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">[(</span><span class="n">t_from</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">horizon</span><span class="p">)]</span>
    <span class="n">out</span><span class="p">,</span> <span class="n">lo</span> <span class="o">=</span> <span class="p">[],</span> <span class="n">t_from</span>
    <span class="n">i</span> <span class="o">=</span> <span class="n">bisect_right</span><span class="p">(</span><span class="n">lst</span><span class="p">,</span> <span class="p">(</span><span class="n">t_from</span><span class="p">,</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">60</span><span class="p">,</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">60</span><span class="p">))</span> <span class="o">-</span> <span class="mi">1</span>
    <span class="k">if</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">lst</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">t_from</span><span class="p">:</span>      <span class="c1"># inside an occupied block
</span>        <span class="n">lo</span> <span class="o">=</span> <span class="n">lst</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>
    <span class="k">for</span> <span class="n">s</span><span class="p">,</span> <span class="n">e</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">lst</span><span class="p">[</span><span class="n">i</span><span class="p">:]:</span>
        <span class="k">if</span> <span class="n">e</span> <span class="o">&lt;=</span> <span class="n">lo</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="k">if</span> <span class="n">s</span> <span class="o">&gt;</span> <span class="n">lo</span><span class="p">:</span>
            <span class="n">out</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">lo</span><span class="p">,</span> <span class="nb">min</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">horizon</span><span class="p">)))</span>   <span class="c1"># a gap
</span>        <span class="n">lo</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">lo</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">lo</span> <span class="o">&gt;=</span> <span class="bp">self</span><span class="p">.</span><span class="n">horizon</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">out</span>
    <span class="k">if</span> <span class="n">lo</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="p">.</span><span class="n">horizon</span><span class="p">:</span>
        <span class="n">out</span><span class="p">.</span><span class="n">append</span><span class="p">((</span><span class="n">lo</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">horizon</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">out</span>
</code></pre></div></div>

<p>A resource is a cell, with one exception, which is that the map has level-free crossings (bridges) where two trains can sit in the same cell on different axes, so those split into two resources by axis. The A* search expands <code class="language-plaintext highlighter-rouge">(node, fraction, interval)</code> states, and the thing that makes it exact for Flatland is the fractional-speed arithmetic, because for a train at speed <code class="language-plaintext highlighter-rouge">p/q</code> sitting in a cell with banked fraction numerator <code class="language-plaintext highlighter-rouge">fr</code>, the number of steps to roll out of the cell without stopping is <code class="language-plaintext highlighter-rouge">ceil((q - fr) / p)</code>. So from an entry time <code class="language-plaintext highlighter-rouge">t</code> the non-stop exit is <code class="language-plaintext highlighter-rouge">t + ceil((q - fr) / p)</code>, and the cost of a state is <code class="language-plaintext highlighter-rouge">arrival_time - earliest_possible + halt_weight * n_halts</code>. The A* heuristic is the exact non-stop travel time left to the goal, which we get from a backward BFS over the graph, and it is admissible because non-stop is the fastest a train can ever go.</p>

<p>The successor step is where the halt cost enters. For each successor we look at the safe intervals of that successor’s resource, find the earliest time we could enter it, and check whether entering that late meant we had to wait in the current cell, because waiting is a halt:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">v_node</span> <span class="ow">in</span> <span class="n">g</span><span class="p">.</span><span class="n">successors</span><span class="p">(</span><span class="n">node</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">s</span><span class="p">,</span> <span class="n">e</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">res</span><span class="p">.</span><span class="n">safe_intervals</span><span class="p">(</span><span class="n">res_v</span><span class="p">,</span> <span class="n">exit_nonstop</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">s</span> <span class="o">&gt;</span> <span class="n">free_til</span><span class="p">:</span>                 <span class="c1"># can't wait in u past when u itself is claimed
</span>            <span class="k">break</span>
        <span class="n">tau</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">exit_nonstop</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span>       <span class="c1"># earliest legal entry into v
</span>        <span class="c1"># ... swap protection and corridor locks go here ...
</span>        <span class="n">halted</span> <span class="o">=</span> <span class="n">tau</span> <span class="o">&gt;</span> <span class="n">exit_nonstop</span>      <span class="c1"># did we have to wait?  that's a halt
</span>        <span class="n">step_cost</span> <span class="o">=</span> <span class="p">(</span><span class="n">tau</span> <span class="o">-</span> <span class="n">t</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">halt_weight</span> <span class="k">if</span> <span class="n">halted</span> <span class="k">else</span> <span class="mf">0.0</span><span class="p">)</span>
        <span class="p">...</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">halt_weight</code> is <code class="language-plaintext highlighter-rouge">250 x v</code>, straight out of the reward book, so the search literally prices a wait at the same rate the game prices a brake, which means it will happily send a train the long way around or hold it back at its origin just to avoid paying that <code class="language-plaintext highlighter-rouge">250</code>. This is SIPP behaving as a scheduler that has fully understood the one number that matters.</p>

<p>The last piece is the order we plan trains in. We plan tightest-slack-first, where slack is <code class="language-plaintext highlighter-rouge">latest_arrival - earliest_departure - nonstop_travel_time</code>, so the trains with the least room to spare get first pick of the reservation table:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">slack</span><span class="p">(</span><span class="n">agent</span><span class="p">):</span>
    <span class="n">dist</span> <span class="o">=</span> <span class="n">g</span><span class="p">.</span><span class="n">dist_to_goals</span><span class="p">(</span><span class="n">goal_nodes</span><span class="p">[</span><span class="n">agent</span><span class="p">.</span><span class="n">handle</span><span class="p">])[</span><span class="n">start_node</span><span class="p">(</span><span class="n">agent</span><span class="p">)]</span>
    <span class="n">travel</span> <span class="o">=</span> <span class="n">ceil</span><span class="p">(</span><span class="n">dist</span> <span class="o">/</span> <span class="n">speed</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">agent</span><span class="p">.</span><span class="n">latest_arrival</span><span class="p">)</span> <span class="o">-</span> <span class="n">agent</span><span class="p">.</span><span class="n">earliest_departure</span> <span class="o">-</span> <span class="n">travel</span>
<span class="n">order</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">env</span><span class="p">.</span><span class="n">agents</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="n">slack</span><span class="p">)</span>
</code></pre></div></div>

<p>We wired this into the competition’s policy interface (an observation builder hands <code class="language-plaintext highlighter-rouge">(env, handle)</code> to the policy, which runs the planner once and then just reads out the actions), and ran it on the example curriculum. <strong>V1 scored 24.38 out of 27, with 93.8% of trains delivered</strong>, against the RL baseline’s 15.55 and 45.6%. So on the first day, before any of the hard problems, the planner was already 57% better than the RL policy they ship, and that is really the whole OR-beats-RL story in one number.</p>

<h2 id="v2-making-it-survive-execution">V2: making it survive execution</h2>

<p>A plan is only good until the world stops matching it, and in Flatland the world stops matching it all the time, because trains break down, departures slip, and sometimes our own placement attempts get blocked. So the runtime is a plan-and-execute loop. On the first step it builds the graph and plans everyone, and on every step after that it compares each train’s actual state against its plan, marks the ones that have drifted as dirty, and replans them (a full SIPP re-search, most urgent first, under a per-step budget), while everything else just reads its next action out of the schedule.</p>

<p>Turning a timed path into actions has one subtlety that is worth calling out, and it is the braking penalty again. When a train is already stopped, say a malfunction just ended, and its plan says to wait a bit longer before moving, we have to send <code class="language-plaintext highlighter-rouge">DO_NOTHING</code> and keep it stopped and then start it at the last moment, instead of letting it inch forward and brake again, because restarting a stopped train is free but braking a moving one is <code class="language-plaintext highlighter-rouge">-250</code>. The action builder is full of this kind of care.</p>

<p>The bigger job in V2 was making the thing safe when the timing goes against us. Four mechanisms went in:</p>

<p><strong>Swap protection.</strong> Flatland does not let two trains cross on a single edge in one step, so they cannot swap places. If our schedule ever asks for a swap, the replay quietly turns it into a stop (and a <code class="language-plaintext highlighter-rouge">-250</code>, and maybe a deadlock). So the reservation table also keeps a <code class="language-plaintext highlighter-rouge">vacate_to</code> map, and when agent A leaves resource <code class="language-plaintext highlighter-rouge">u</code> into <code class="language-plaintext highlighter-rouge">v</code> at time <code class="language-plaintext highlighter-rouge">tau</code>, we record that, and any other train that tries to enter <code class="language-plaintext highlighter-rouge">u</code> from <code class="language-plaintext highlighter-rouge">v</code> at that same instant gets pushed back a step.</p>

<p><img src="/assets/images/posts/flatland_swap.png" alt="Swap protection and the single-track deadlock it prevents" /></p>

<p>This looks like a small corner case, but it is actually the biggest failure mode in the whole competition, because two trains meeting head on in a single-track corridor cannot pass each other and cannot swap, so they just sit there facing each other forever. We will come back to this later.</p>

<p><strong>Owner checks at cell entry.</strong> Even with reservations, schedules go stale between replans, so at the moment a train is about to enter a new cell we double-check the reservation table, and if the slot is claimed by an opposing train (one heading the other way, which is a real head-on risk) we hold. Same-direction conflicts we let resolve as normal queueing, because queueing behind someone going your way is no worse than waiting.</p>

<p><strong>Honest stub blocks.</strong> When a train genuinely cannot find a plan, because the table is too crowded for a moment, we do not just let it sit there invisibly, we block its current cell in the reservation table all the way to the horizon, so every other train routes around it instead of queueing up behind it waiting for a train that is not moving. Once the train replans, the stub is replaced by its real reservations.</p>

<p><strong>Provable-deadlock containment.</strong> Some jams really are dead and it is important not to thrash trying to fix them, so we mark only the provably dead trains, which are mutual pairs where each train’s only possible next cell is the other train’s current cell. Those get marked dead and told to sit still. Bigger cycles we leave alone, because a ring of trains in Flatland can sometimes rotate itself free, and killing them off by mistake loses trains.</p>

<p>There is also a runtime governor, because each scenario has a 30-minute wall-clock budget on the evaluation cluster, so if planning time creeps past a threshold the planner starts cutting its own search effort (fewer replans per step, smaller expansion budget) to make sure it always finishes stepping the environment, since a slightly worse plan is much better than a scenario that times out at zero.</p>

<p><strong>V2 scored 25.43 out of 27 (99.4% delivered)</strong> on the curriculum, and the little bit that is left is mostly the stop-skipping, which is by design. On the clean levels the planner was basically done.</p>

<h2 id="shipping-it-to-the-evaluator">Shipping it to the evaluator</h2>

<p>This part deserves its own section because it cost us our entire first submission, and it is the kind of thing nobody writes down.</p>

<p>You submit to Flatland as a Docker image, so the evaluator pulls your image, runs your recorded-trajectory command, and scores it. The starter kit’s base image is <code class="language-plaintext highlighter-rouge">flatland-baselines</code>, which is <strong>7.26 GB compressed</strong> because it carries conda, torch, ray, ffmpeg and the whole ML stack, none of which our planner needs, but we did not think about it, built on top of the base image, and submitted anyway.</p>

<p>The submission failed, and not with a crash, with this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FAILURE  Elapsed time 1200.47s exceeded start time limit 1200s. Status: Pending
</code></pre></div></div>

<p>The evaluation pod gets 20 minutes to reach Running, and that includes pulling the image, and a 7.3 GB pull over the cluster’s registry link did not make it in time, so the pod sat Pending for exactly 1200 seconds and got killed. We had burned a submission (and we later confirmed the hard way that <strong>failed submissions still count against the daily cap of 2</strong>) on a problem that had nothing to do with our code.</p>

<p>The fix was a slim image, just <code class="language-plaintext highlighter-rouge">python:3.12-slim</code> plus <code class="language-plaintext highlighter-rouge">flatland-rl==4.2.6</code> and nothing else, because the planner is pure Python and numpy, which is <strong>825 MB instead of 7.3 GB</strong> and pulls in seconds. We kept the fat one around as <code class="language-plaintext highlighter-rouge">Dockerfile.baselines</code> just in case. There was also half an hour of fighting with GitHub Container Registry permissions, because giving the evaluator read access to a private package quietly turns off “inherit access from source repo”, which then takes away the CI’s write access so the build starts failing, and the two settings live in different panels so you need both. The lesson we took from this is simple: keep the image slim, and test the full pull-and-run path before you spend a submission on it.</p>

<h2 id="the-first-real-result">The first real result</h2>

<p>With the slim image (carrying a planner version we will get to, with overstay rights already folded in) we finally got a scored submission back, and here is where the whole thing stood after one day:</p>

<p><img src="/assets/images/posts/flatland_perlevel.png" alt="Per-level competition results for sipp-v3" /></p>

<p>That is <strong>20.74 out of 35, with 72.9% of trains delivered</strong>, and the public leaderboard leader at that moment was 12.49, so we were at rank 1 by a 66% margin, and our best planner version was still sitting unsubmitted because of the daily cap.</p>

<p>The per-level breakdown is the interesting part though, because it tells you exactly where the points are and turns “make it better” into an actual list:</p>

<ul>
  <li><strong>Levels 0 to 2 (no malfunctions): 12.60 out of 15, 100% delivered.</strong> This is working exactly as designed, and the gap from a perfect 15 is almost entirely our stop-skipping, which is the correct trade.</li>
  <li><strong>Levels 3 to 5 (malfunctions): 7.19 out of 15, 56 to 78% delivered.</strong> This is the weak spot, because when trains break down our schedules cascade into jams and we lose a big chunk of the fleet, and this is where the points are.</li>
  <li><strong>Level 6 (infrastructure disruptions): 0.94 out of 5, 15.6% delivered.</strong> This is almost a total loss, and it is a different kind of problem from the others.</li>
</ul>

<p>So the clean levels were done, and the malfunction levels were where the competition was actually going to be won, and we had a guess about why they were failing.</p>

<h2 id="the-malfunction-levels-two-big-fixes">The malfunction levels: two big fixes</h2>

<p>To work on the malfunction problem locally we built some deliberately nasty stress environments, 62, 118 and up to 532 agents on the real map with random origin-destination pairs, mixed speeds and heavy malfunction rates, harsher than the real levels on purpose so that whatever survived them would be robust. The 118-agent storm was the one we used the most, and two fixes came out of it, and together they took that environment from <strong>34% of trains delivered to 93%</strong>:</p>

<p><img src="/assets/images/posts/flatland_progression.png" alt="Version progression and the two structural storm fixes" /></p>

<h3 id="fix-1-overstay-rights">Fix 1: overstay rights</h3>

<p>The first bug was subtle and nasty. When a train breaks down it sits in its cell for the malfunction duration, and every train queued behind it has to replan, but those followers had already reserved cells ahead of them, including the broken train’s own cell, for times in the near future. So when the broken train tried to replan its own escape, SIPP saw those followers’ reservations of its current cell as hard “you have to be gone by then” deadlines, and those were impossible to satisfy because the train was physically sitting there and could not leave until the malfunction ended, so SIPP returned no plan. Then the train went plan-less, which stubbed its cell, which made the followers replan, which re-reserved, and the whole queue behind one breakdown froze solid.</p>

<p>The fix we called <strong>overstay rights</strong>, which just says that a train that is physically in a cell may always stay in it, full stop. Its presence is the truth, and the other trains’ reservations of that cell are stale fiction that has to be re-timed around the truth, not treated as a constraint on the train that is actually there. So in the search, the start state of an on-map train gets a free pass to hold its own cell to the horizon, and when it replans we wake every train whose reservations conflicted with that cell so that they replan around reality:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># a replanning on-map train may keep its current cell indefinitely;
# wake every stale claimant so the queue learns the truth at once.
</span><span class="k">if</span> <span class="n">agent</span><span class="p">.</span><span class="n">state</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">OFF_MAP_STATES</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">other</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">res</span><span class="p">.</span><span class="n">conflicting_owners</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">g</span><span class="p">,</span> <span class="n">visits</span><span class="p">[:</span><span class="mi">1</span><span class="p">]):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">dirty</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">other</span><span class="p">)</span>
</code></pre></div></div>

<p>One detail that mattered here is that we wake all the conflicting claimants, not just the earliest one, because waking only the earliest lets the queue learn the truth one train at a time, which is too slow, and wedges still form while the news travels. If you wake all of them at once the whole queue re-times in a single step. On the worst storm environment this roughly doubled delivery on its own, from 34% up to <strong>74.6%</strong>.</p>

<h3 id="fix-2-directional-corridor-locks">Fix 2: directional corridor locks</h3>

<p>The second failure was the head-on deadlock from the swap section, but at scale. Watch two convoys come at the same long single-track artery from opposite ends. Each train on its own has a valid plan, but once the first train from each side commits into the artery they are on a collision course, and by the time the reservation conflict is visible they are already too deep to back out, because Flatland has no reverse. So they meet in the middle, cannot pass, cannot swap, and the artery is dead along with everything queued behind both of them.</p>

<p>The reservation table on its own does not stop this, because each train’s cells are free when it reserves them, so the conflict is a property of the whole corridor and not of any single cell. So we made the planner treat corridors as first-class objects. First we detect them, where a corridor is a maximal chain of plain cells, degree at most 2 with no routing choice, and on this map there are 471 of them, and the 18 long arteries (20 cells or more) are exactly where the face-offs happen. Here is what the solver sees:</p>

<p><img src="/assets/images/posts/flatland_corridors.png" alt="The single-track corridors and the long arteries that get locked" /></p>

<p>The purple segments are the long arteries that get locked, and the teal ones are shorter corridors that resolve fine with normal cell reservations. Then the mechanism is that <strong>any plan that traverses a long artery locks it for that plan’s direction and time window</strong>, and an opposing plan that wants the same artery in the same window has to schedule after the lock releases, while same-direction platooning stays completely free, because trains chasing each other down the artery is fine, it is only the head-on case that has to be serialized:</p>

<p><img src="/assets/images/posts/flatland_corridor_lock.png" alt="A directional corridor lock in space-time" /></p>

<p>In the search, before a train enters an artery it checks for an opposing lock over the window it wants to traverse, and if it finds one it waits at the mouth until it clears:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">est_transit</span><span class="p">:</span>                       <span class="c1"># entering a long artery
</span>    <span class="n">sense_v</span> <span class="o">=</span> <span class="n">g</span><span class="p">.</span><span class="n">corridor_sense</span><span class="p">[</span><span class="n">v_node</span><span class="p">]</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">8</span><span class="p">):</span>
        <span class="n">lock_end</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">res</span><span class="p">.</span><span class="n">opposing_lock_until</span><span class="p">(</span><span class="n">seg_v</span><span class="p">,</span> <span class="n">sense_v</span><span class="p">,</span> <span class="n">tau</span><span class="p">,</span> <span class="n">tau</span> <span class="o">+</span> <span class="n">est_transit</span><span class="p">,</span> <span class="n">agent</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">lock_end</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
            <span class="k">break</span>                     <span class="c1"># no opposing lock, go
</span>        <span class="n">tau</span> <span class="o">=</span> <span class="n">lock_end</span>                <span class="c1"># wait for the artery to clear its direction
</span></code></pre></div></div>

<p>Tuning the length threshold was a small experiment on its own. Locking every corridor over-serializes the station throats, where short parallel tracks are meant to be used at the same time, and that cost us points, while locking only the long arteries hits just the handful of places where face-offs actually form. We swept the threshold, and 12 cells was too aggressive and 20 was the sweet spot, because it covers only the roughly 18 longest arteries. Here is a real face-off, an end-of-episode close-up of one contested artery on a 62-train scenario, with the lock off and then on:</p>

<p><img src="/assets/images/posts/flatland_faceoff_artery.png" alt="An artery face-off at end of episode, locks off vs on" /></p>

<p>Those red wedges are trains stopped nose to tail in a single-track artery, each one facing traffic it can neither pass nor reverse around. Without the lock both ends of the artery fill up and 16 trains are stranded, and with the lock the opposing traffic serializes at the mouths and the wedge shrinks. This particular artery still keeps a stubborn residual of 12 stuck trains even with the lock on, and every quick fix we threw at it (escape moves, locking the artery stubs) tested worse, so we left it, because it is worth about <code class="language-plaintext highlighter-rouge">0.1</code> normalized on a synthetic environment that may not even look like the real levels. But on the 118-train storm, where these face-offs are the main failure mode, corridor locks took delivery from <strong>74.6% to 93.2%</strong>, which is 88 to 110 of the 118 trains home. Stacked on top of overstay rights, the two fixes together turned a 34%-delivery disaster into a 93%-delivery success.</p>

<p>Here is the shipped planner actually dispatching a 62-train scenario, colored by state, moving in teal and stopped in red, with most of the fleet flowing at once:</p>

<p><img src="/assets/images/posts/flatland_roll_lock20_t250.png" alt="The planner dispatching a 62-train scenario, coloured by state" /></p>

<p>Across the full local benchmark suite the final configuration looks like this:</p>

<table>
  <thead>
    <tr>
      <th>suite</th>
      <th>score</th>
      <th>delivered</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>example curriculum (max 27)</td>
      <td>25.35</td>
      <td>100.0%</td>
    </tr>
    <tr>
      <td>official-generator level-1/2 replicas</td>
      <td>0.879 / 0.891</td>
      <td>100%</td>
    </tr>
    <tr>
      <td>532 agents, mixed speeds, no malfunction</td>
      <td>0.971</td>
      <td>100%</td>
    </tr>
    <tr>
      <td>118-agent malfunction storm</td>
      <td>0.803</td>
      <td>93%</td>
    </tr>
    <tr>
      <td>RL baseline, for reference</td>
      <td>15.55</td>
      <td>45.6%</td>
    </tr>
  </tbody>
</table>

<p>The 532-agent number is worth sitting with for a second, because the largest competition scenarios put 532 trains on this map, and the planner handles them at 97 to 100% delivery in about four minutes of wall-clock, well inside the 30-minute budget, so prioritized SIPP scales.</p>

<h2 id="things-we-tried-that-did-not-work">Things we tried that did not work</h2>

<p>At least half of the work was ideas that sounded good and tested worse, and we measured every one of them with one-flag-at-a-time ablations instead of trusting our gut, and that paid off again and again, because a lot of the “obviously helpful” mechanisms were actively hurting. Roughly in the order we killed them:</p>

<ul>
  <li>
    <p><strong>Headway buffers.</strong> The instinct from MAPF is to keep a one-step gap between trains, but in this reward regime a headway of 1 means every train that catches up to another has to stop to keep the gap, and every stop is <code class="language-plaintext highlighter-rouge">-250 x v</code>, so headway just does not fit the braking economics. We run <code class="language-plaintext highlighter-rouge">headway = 0</code>, so trains chain nose to tail, which is free, and only stop when they are actually blocked.</p>
  </li>
  <li>
    <p><strong>Cascade replanning.</strong> When a train replans and its new path conflicts with others, the tempting move is to immediately replan all of them too, but it just churns, because the same conflicts re-form, planning time blows up, and the score does not move. We only wake the specific claimants of a train’s own current cell, which is the overstay case, and nothing more.</p>
  </li>
  <li>
    <p><strong>Relaxed fallback planning.</strong> When the table was too crowded to find a conflict-free plan, we tried letting the train plan against only the static obstacles (stopped and broken trains) and sorting the rest out during execution, and this was a disaster, because trains committed to paths straight through moving traffic and produced exactly the head-on deadlocks we were trying to avoid. Off.</p>
  </li>
  <li>
    <p><strong>Shift-repair and eager-repair.</strong> When a train is only delayed, why re-search from scratch, just push its remaining schedule later by the delay. We measured it, and it was <code class="language-plaintext highlighter-rouge">-0.55</code> and <code class="language-plaintext highlighter-rouge">-0.57</code> on the curriculum, because shifting keeps a route that a full replan would have routed around the new obstacle for free, so the full SIPP re-search is just better and both repairs are off.</p>
  </li>
  <li>
    <p><strong>Escape moves.</strong> For a hopelessly stuck train, step it out of the way to break the jam. Net negative, because the escaping train just brakes (another <code class="language-plaintext highlighter-rouge">-250</code>) and parks somewhere that blocks a different route. Off.</p>
  </li>
  <li>
    <p><strong>Serving intermediate stops.</strong> We tried turning stop-serving back on to collect the rebate, but it destabilizes dense scenarios, because halting at a platform is exactly what triggers the departure-delay malfunctions at levels 4 and 5, and the chaos that follows costs far more than the <code class="language-plaintext highlighter-rouge">-50</code> rebates give back. The whole point was to skip, so leaving it off was right.</p>
  </li>
  <li>
    <p><strong>Stub corridor locks.</strong> We tried having plan-less stubbed trains also lock their corridor, and it measured worse both ways, so locks stay traversal-only.</p>
  </li>
</ul>

<p>The lesson here is the same one from every optimization project, which is that the reward function knows more than you do and your intuition was built on a different reward function, so you measure everything instead of trusting it.</p>

<h2 id="repository-layout">Repository layout</h2>

<ul>
  <li><strong><a href="https://github.com/darshanmakwana412/ecml2026/tree/main/submission/planner"><code class="language-plaintext highlighter-rouge">submission/planner/</code></a></strong> — the dispatcher. <a href="https://github.com/darshanmakwana412/ecml2026/blob/main/submission/planner/netgraph.py"><code class="language-plaintext highlighter-rouge">netgraph.py</code></a> builds the <code class="language-plaintext highlighter-rouge">(cell, direction)</code> graph and detects the single-track corridors; <a href="https://github.com/darshanmakwana412/ecml2026/blob/main/submission/planner/sipp.py"><code class="language-plaintext highlighter-rouge">sipp.py</code></a> is Safe-Interval Path Planning over the shared reservation table, including overstay rights and the directional corridor locks; <a href="https://github.com/darshanmakwana412/ecml2026/blob/main/submission/planner/runtime.py"><code class="language-plaintext highlighter-rouge">runtime.py</code></a> is the plan-and-execute loop with priority planning, malfunction replanning, and the safety layers.</li>
  <li><strong><a href="https://github.com/darshanmakwana412/ecml2026/blob/main/submission/my_policy.py"><code class="language-plaintext highlighter-rouge">submission/my_policy.py</code></a></strong>, <strong><a href="https://github.com/darshanmakwana412/ecml2026/blob/main/submission/my_observation_builder.py"><code class="language-plaintext highlighter-rouge">submission/my_observation_builder.py</code></a></strong> — the policy and env-passing observation builder the evaluator calls.</li>
  <li><strong><a href="https://github.com/darshanmakwana412/ecml2026/tree/main/dev"><code class="language-plaintext highlighter-rouge">dev/</code></a></strong> — the local evaluation harness (<a href="https://github.com/darshanmakwana412/ecml2026/blob/main/dev/eval_lib.py"><code class="language-plaintext highlighter-rouge">eval_lib.py</code></a>, <a href="https://github.com/darshanmakwana412/ecml2026/blob/main/dev/evaluate.py"><code class="language-plaintext highlighter-rouge">evaluate.py</code></a>) that mirrors the server-side scoring, plus the stress environments and per-version result CSVs.</li>
</ul>

<h2 id="reproducing-locally">Reproducing locally</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>flatland-rl<span class="o">==</span>4.2.6

<span class="c"># score the SIPP planner on the example curriculum, with the official</span>
<span class="c"># normalization and a per-penalty breakdown (mirrors the server-side eval)</span>
python <span class="nt">-m</span> dev.evaluate <span class="nt">--glob</span> <span class="s1">'reinforcement_learning/curriculum/curriculum/example_curriculum/*.pkl'</span>
</code></pre></div></div>

<p>Behaviour is controlled by environment-variable knobs, all defaulting to the measured-best configuration, for example <code class="language-plaintext highlighter-rouge">PLANNER_LOCK_MIN_LEN=20</code> for the corridor-lock length gate and <code class="language-plaintext highlighter-rouge">PLANNER_SERVE_STOPS=0</code> to keep stop-skipping on. See <a href="https://github.com/darshanmakwana412/ecml2026/blob/main/dev/SUBMISSION_NOTES.md"><code class="language-plaintext highlighter-rouge">dev/SUBMISSION_NOTES.md</code></a> for the full list and the tested-and-rejected ablations.</p>

<hr />

<p><strong>References:</strong></p>
<ol>
  <li><a href="https://competition.flatland.cloud">ECML 2026 Flatland Real-World Baselines Challenge</a></li>
  <li><a href="https://flatland-association.github.io/flatland-book/_downloads/348b3e1efb2f66fc5a39bc6a33d6d56d/Real_World_Baselines_Challenge__Dynamic_Train_Rescheduling_under_Stochastic_Perturbations.pdf">Meyer et al., <em>Real-World Baselines Challenge: Dynamic Train Rescheduling under Stochastic Perturbations</em>, ECML PKDD 2026 Discovery Challenge</a></li>
  <li><a href="https://github.com/flatland-association/flatland-rl">Flatland environment and evaluation code (flatland-rl)</a></li>
  <li><a href="https://www.cs.cmu.edu/~maxim/files/sipp_icra11.pdf">Phillips and Likhachev, <em>SIPP: Safe Interval Path Planning for Dynamic Environments</em>, ICRA 2011</a></li>
  <li><a href="https://www.davidsilver.uk/wp-content/uploads/2020/03/coop-path-AIIDEo5.pdf">Silver, <em>Cooperative Pathfinding</em> (prioritized planning with reservation tables), AIIDE 2005</a></li>
</ol>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="algorithms" />
      
        <category term="optimization" />
      
        <category term="planning" />
      
        <category term="reinforcement-learning" />
      

      
      
        <summary type="html"><![CDATA[This is a consolidated writeup of my ECML 2026 Flatland Real-World Baselines Challenge solution that achieved rank 1 on the leaderboard. This writeup consolidates all the experimentations and implementation details from my obsidian vault, claude has been used to write parts of this writeup and structure the content, experiments and findings]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">Efficient Parallel Merge Sort Implementation</title>
      <link href="https://darshanmakwana412.github.io/2026/08/parallel-merge-sort/" rel="alternate" type="text/html" title="Efficient Parallel Merge Sort Implementation" />
      <published>2026-08-12T00:00:00+05:30</published>
      <updated>2026-08-12T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/08/parallel-merge-sort</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/08/parallel-merge-sort/"><![CDATA[<p>I have been having a lot of fun on weekends writing classicial algorithms by hand without the help of any coding agents, this is an attempt of a writeup of my parallel merge sort implementation. Merge sort is one of the easiest algorithms to parallelize because inherently it divides the work into smaller subproblems that can be solved independently. I also have a parrallel quick sort implementation which is quite fascinating to understand but I will write it up in a separate post. Also this reminds me the very first post I wrote on this blog was a <a href="/2025/09/median-filter-over-arbitrary-datatypes/">parallel median filter implementation</a> that works over arbitrary datatypes such fun times those were a year back : )</p>

<p><strong>Table of Contents:</strong></p>
<ul>
  <li><a href="#v1-just-sort-the-thing">V1: Just Sort The Thing</a></li>
  <li><a href="#v2-let-the-recursion-fan-out">V2: Let The Recursion Fan Out</a></li>
  <li><a href="#v3-cutting-the-branch-and-breaking-everything">V3: Cutting The Branch (And Breaking Everything)</a></li>
  <li><a href="#v4-going-wide-with-avx-512">V4: Going Wide With AVX-512</a></li>
</ul>

<h2 id="v1-just-sort-the-thing">V1: Just Sort The Thing</h2>

<p>Standard top down merge sort. Split the array in half, recurse on each half, merge the two sorted halves back together. The only real decision here is when to stop recursing, past a certain point the array is small enough that the overhead of splitting it further is not worth it so we just hand it to <code class="language-plaintext highlighter-rouge">std::sort</code> and let that handle the base case</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span> <span class="n">data_t</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">merge</span><span class="p">(</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">left</span><span class="p">,</span> <span class="kt">int</span> <span class="n">nl</span><span class="p">,</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">right</span><span class="p">,</span> <span class="kt">int</span> <span class="n">nr</span><span class="p">,</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">out</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">j</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span>
    <span class="k">while</span><span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">nl</span> <span class="o">&amp;&amp;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">nr</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span><span class="p">(</span><span class="n">left</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">right</span><span class="p">[</span><span class="n">j</span><span class="p">])</span> <span class="n">out</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">left</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">];</span>
        <span class="k">else</span> <span class="n">out</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">right</span><span class="p">[</span><span class="n">j</span><span class="o">++</span><span class="p">];</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">nl</span><span class="p">)</span> <span class="n">std</span><span class="o">::</span><span class="n">memcpy</span><span class="p">(</span><span class="n">out</span> <span class="o">+</span> <span class="n">k</span><span class="p">,</span> <span class="n">left</span> <span class="o">+</span> <span class="n">i</span><span class="p">,</span> <span class="p">(</span><span class="n">nl</span> <span class="o">-</span> <span class="n">i</span><span class="p">)</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">j</span> <span class="o">&lt;</span> <span class="n">nr</span><span class="p">)</span> <span class="n">std</span><span class="o">::</span><span class="n">memcpy</span><span class="p">(</span><span class="n">out</span> <span class="o">+</span> <span class="n">k</span><span class="p">,</span> <span class="n">right</span> <span class="o">+</span> <span class="n">j</span><span class="p">,</span> <span class="p">(</span><span class="n">nr</span> <span class="o">-</span> <span class="n">j</span><span class="p">)</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">mergesort</span><span class="p">(</span><span class="n">data_t</span> <span class="o">*</span><span class="n">src</span><span class="p">,</span> <span class="kt">int</span> <span class="n">n</span><span class="p">,</span> <span class="n">data_t</span> <span class="o">*</span><span class="n">dst</span><span class="p">,</span> <span class="kt">int</span> <span class="n">level</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span><span class="p">(</span><span class="n">n</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="k">if</span><span class="p">(</span><span class="n">level</span> <span class="o">&lt;=</span> <span class="n">MAX_LEVEL</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">int</span> <span class="n">mid</span> <span class="o">=</span> <span class="n">n</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
        <span class="n">mergesort</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">mid</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">level</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
        <span class="n">mergesort</span><span class="p">(</span><span class="n">dst</span> <span class="o">+</span> <span class="n">mid</span><span class="p">,</span> <span class="n">n</span> <span class="o">-</span> <span class="n">mid</span><span class="p">,</span> <span class="n">src</span> <span class="o">+</span> <span class="n">mid</span><span class="p">,</span> <span class="n">level</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
        <span class="n">merge</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">mid</span><span class="p">,</span> <span class="n">src</span> <span class="o">+</span> <span class="n">mid</span><span class="p">,</span> <span class="n">n</span> <span class="o">-</span> <span class="n">mid</span><span class="p">,</span> <span class="n">dst</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">sort</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">src</span> <span class="o">+</span> <span class="n">n</span><span class="p">);</span>
        <span class="n">memcpy</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">n</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">psort</span><span class="p">(</span><span class="kt">int</span> <span class="n">n</span><span class="p">,</span> <span class="n">data_t</span> <span class="o">*</span><span class="n">data</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">buffer</span> <span class="o">=</span> <span class="p">(</span><span class="n">data_t</span> <span class="o">*</span><span class="p">)</span><span class="n">aligned_alloc</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span> <span class="p">((</span><span class="n">n</span> <span class="o">+</span> <span class="mi">7</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">)</span> <span class="o">*</span> <span class="mi">8</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
    <span class="n">memcpy</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">n</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
    <span class="n">mergesort</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="n">free</span><span class="p">(</span><span class="n">buffer</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Benchmarked this on my laptop (Apple M5, 10 cores, 4 performance + 6 efficiency, though this version doesn’t touch the extra cores at all yet) with random uint64 arrays, best of 3 runs each:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>n                    time (ms)
1,000,000              23.60
10,000,000            241.84
100,000,000          2585.10
</code></pre></div></div>

<p>Roughly linear with a bit of superlinear creep as n grows, which tracks, merge sort is O(n log n) and the log n factor starts to bite once n crosses into the hundreds of millions</p>

<h2 id="v2-let-the-recursion-fan-out">V2: Let The Recursion Fan Out</h2>

<p>The two recursive calls in mergesort don’t depend on each other at all, left half and right half can happen at the same time, so this is the obvious place to throw threads at it. Wrapped both calls in <code class="language-plaintext highlighter-rouge">#pragma omp task</code> inside a <code class="language-plaintext highlighter-rouge">taskgroup</code> so the parent waits for both children before it merges. The one thing that actually matters here is capping how deep this fan-out goes, spawning a task for a sub array of like 8 elements is pure overhead, so <code class="language-plaintext highlighter-rouge">MAX_LEVEL</code> is picked so the recursion produces roughly 2 leaf tasks per thread and then falls back to plain <code class="language-plaintext highlighter-rouge">std::sort</code> underneath that</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">mergesort</span><span class="p">(</span><span class="n">data_t</span> <span class="o">*</span><span class="n">src</span><span class="p">,</span> <span class="kt">int</span> <span class="n">n</span><span class="p">,</span> <span class="n">data_t</span> <span class="o">*</span><span class="n">dst</span><span class="p">,</span> <span class="kt">int</span> <span class="n">level</span><span class="p">,</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">MAX_LEVEL</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span><span class="p">(</span><span class="n">n</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    <span class="k">if</span><span class="p">(</span><span class="n">level</span> <span class="o">&lt;=</span> <span class="n">MAX_LEVEL</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">int</span> <span class="n">mid</span> <span class="o">=</span> <span class="n">n</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span>
        <span class="cp">#pragma omp taskgroup
</span>        <span class="p">{</span>
            <span class="cp">#pragma omp task
</span>            <span class="n">mergesort</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">mid</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">level</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">MAX_LEVEL</span><span class="p">);</span>
            <span class="cp">#pragma omp task
</span>            <span class="n">mergesort</span><span class="p">(</span><span class="n">dst</span> <span class="o">+</span> <span class="n">mid</span><span class="p">,</span> <span class="n">n</span> <span class="o">-</span> <span class="n">mid</span><span class="p">,</span> <span class="n">src</span> <span class="o">+</span> <span class="n">mid</span><span class="p">,</span> <span class="n">level</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">MAX_LEVEL</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="n">merge</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">mid</span><span class="p">,</span> <span class="n">src</span> <span class="o">+</span> <span class="n">mid</span><span class="p">,</span> <span class="n">n</span> <span class="o">-</span> <span class="n">mid</span><span class="p">,</span> <span class="n">dst</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">std</span><span class="o">::</span><span class="n">sort</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">src</span> <span class="o">+</span> <span class="n">n</span><span class="p">);</span>
        <span class="n">memcpy</span><span class="p">(</span><span class="n">dst</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">n</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">psort</span><span class="p">(</span><span class="kt">int</span> <span class="n">n</span><span class="p">,</span> <span class="n">data_t</span> <span class="o">*</span><span class="n">data</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">buffer</span> <span class="o">=</span> <span class="p">(</span><span class="n">data_t</span> <span class="o">*</span><span class="p">)</span><span class="n">aligned_alloc</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span> <span class="p">((</span><span class="n">n</span> <span class="o">+</span> <span class="mi">7</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">)</span> <span class="o">*</span> <span class="mi">8</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
    <span class="n">memcpy</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">n</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>

    <span class="cp">#pragma omp parallel
</span>    <span class="cp">#pragma omp single
</span>    <span class="p">{</span>
        <span class="kt">int</span> <span class="n">target_leaves</span> <span class="o">=</span> <span class="n">omp_get_max_threads</span><span class="p">()</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
        <span class="kt">int</span> <span class="n">MAX_LEVEL</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">std</span><span class="o">::</span><span class="n">ceil</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">log2</span><span class="p">((</span><span class="kt">double</span><span class="p">)</span><span class="n">target_leaves</span><span class="p">));</span>
        <span class="n">mergesort</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">MAX_LEVEL</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">free</span><span class="p">(</span><span class="n">buffer</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>n                    V1 (ms)     V2 (ms)     speedup
1,000,000              23.60       11.47       2.06x
10,000,000            241.84       82.52       2.93x
100,000,000          2585.10      760.88       3.40x
</code></pre></div></div>

<p>10 cores and best case is a 3.4x, not exactly the speedup you’d hope for. Three things eating into it: the M5 is 4 performance cores and 6 efficiency cores, so it’s not really 10 equal cores, the top level merge (combining the final two n/2 halves back into one) is always single threaded no matter how many cores you have which caps the whole thing by Amdahl’s law, and the smaller leaf tasks near the bottom of the recursion pay task creation overhead that doesn’t show up when you just eyeball the algorithm on paper. Nice to actually see the theory show up as real numbers instead of just trusting it</p>

<h2 id="v3-cutting-the-branch-and-breaking-everything">V3: Cutting The Branch (And Breaking Everything)</h2>

<p>At this point the bottleneck inside each thread is just the merge loop, one comparison per element, <code class="language-plaintext highlighter-rouge">left[i] &lt; right[j]</code>. On random data this branch is close to a coin flip so the CPU’s branch predictor is wrong close to half the time, which stalls the pipeline constantly. The idea was to process the compare loop in fixed size chunks of 32 so the loop overhead amortizes and the compiler has a better shot at pipelining the repeated branch pattern instead of treating every single comparison as a one-off</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">constexpr</span> <span class="kt">int</span> <span class="n">BLOCK_SIZE</span> <span class="o">=</span> <span class="mi">32</span><span class="p">;</span>

<span class="kr">inline</span> <span class="kt">void</span> <span class="nf">merge</span><span class="p">(</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">left</span><span class="p">,</span> <span class="kt">int</span> <span class="n">nl</span><span class="p">,</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">right</span><span class="p">,</span> <span class="kt">int</span> <span class="n">nr</span><span class="p">,</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">tmp</span><span class="p">,</span> <span class="n">data_t</span> <span class="o">*</span><span class="n">data</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">nl</span> <span class="o">-</span> <span class="n">BLOCK_SIZE</span> <span class="o">&amp;&amp;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">nr</span> <span class="o">-</span> <span class="n">BLOCK_SIZE</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">t</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">t</span><span class="o">&lt;</span><span class="n">BLOCK_SIZE</span><span class="p">;</span> <span class="n">t</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">left</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="n">right</span><span class="p">[</span><span class="n">j</span><span class="p">])</span> <span class="n">tmp</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">left</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">];</span>
            <span class="k">else</span> <span class="n">tmp</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">right</span><span class="p">[</span><span class="n">j</span><span class="o">++</span><span class="p">];</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">nl</span><span class="p">)</span> <span class="n">tmp</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">left</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">];</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">j</span> <span class="o">&lt;</span> <span class="n">nr</span><span class="p">)</span> <span class="n">tmp</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">right</span><span class="p">[</span><span class="n">j</span><span class="o">++</span><span class="p">];</span>
    <span class="n">std</span><span class="o">::</span><span class="n">memcpy</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">tmp</span><span class="p">,</span> <span class="p">(</span><span class="n">nl</span> <span class="o">+</span> <span class="n">nr</span><span class="p">)</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To actually find out how much of the merge cost was the branch itself versus just moving memory around, I wrote a second version of merge that skips the comparison entirely and always pulls from the left, purely to measure the floor:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">inline</span> <span class="kt">void</span> <span class="nf">merge2</span><span class="p">(</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">left</span><span class="p">,</span> <span class="kt">int</span> <span class="n">nl</span><span class="p">,</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">right</span><span class="p">,</span> <span class="kt">int</span> <span class="n">nr</span><span class="p">,</span>
    <span class="n">data_t</span> <span class="o">*</span><span class="n">tmp</span><span class="p">,</span> <span class="n">data_t</span> <span class="o">*</span><span class="n">data</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">nl</span> <span class="o">-</span> <span class="n">BLOCK_SIZE</span> <span class="o">&amp;&amp;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">nr</span> <span class="o">-</span> <span class="n">BLOCK_SIZE</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">t</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span> <span class="n">t</span><span class="o">&lt;</span><span class="n">BLOCK_SIZE</span><span class="p">;</span> <span class="n">t</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="n">k</span><span class="o">++</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">nl</span><span class="p">)</span> <span class="n">tmp</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">left</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">];</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">j</span> <span class="o">&lt;</span> <span class="n">nr</span><span class="p">)</span> <span class="n">tmp</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">right</span><span class="p">[</span><span class="n">j</span><span class="o">++</span><span class="p">];</span>
    <span class="n">std</span><span class="o">::</span><span class="n">memcpy</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">tmp</span><span class="p">,</span> <span class="p">(</span><span class="n">nl</span> <span class="o">+</span> <span class="n">nr</span><span class="p">)</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data_t</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Obviously this doesn’t sort anything correctly since it’s not comparing at all, that’s the whole point, it’s just there to answer “how fast would this be with the branch removed.” Plugged it into the same parallel task structure as V2 and at n=10,000,000 the real branchy merge from V2 runs in 82.52ms while this fake branchless one runs in 48.90ms. So somewhere around 40% of the merge time was the mispredicted branch itself, the rest is just memory traffic you can’t get rid of</p>

<p>Then I went back and swapped the real chunked comparison back in, expecting something between those two numbers. Got 110.28ms, slower than V2, and <code class="language-plaintext highlighter-rouge">std::is_sorted</code> came back false again. So there’s a real bug in here somewhere, probably in how the blocking interacts with the parallel task recursion sharing the same tmp buffer across levels, and I never actually tracked it down. This version still sits broken in the so4 folder. V2 stayed the real answer</p>

<p>Worth saying plainly: I almost reported the merge2 number as a real speedup before running is_sorted on it. A fast wrong answer is worse than a slow right one, and it’s very easy to only check the timer</p>

<h2 id="v4-going-wide-with-avx-512">V4: Going Wide With AVX-512</h2>

<p>The scalar merge loop only ever looks at one element per side per comparison. AVX-512 lets you compare 16 lanes at once, so instead of merging one element at a time you can merge whole tiles at a time</p>

<p>The approach has two phases. First, sort small 16 element tiles entirely inside a register using a fixed sequence of compare-and-swap stages, a bitonic sorting network, so a tile never touches memory until it’s fully sorted:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span> <span class="nf">sort16_inregister</span><span class="p">(</span><span class="n">__m512i</span> <span class="o">*</span><span class="n">v</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">__m512i</span> <span class="n">x</span> <span class="o">=</span> <span class="o">*</span><span class="n">v</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">min</span><span class="p">,</span> <span class="n">max</span><span class="p">;</span>
    <span class="n">y</span>   <span class="o">=</span> <span class="n">_mm512_shuffle_epi32</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">_MM_PERM_CDAB</span><span class="p">);</span>
    <span class="n">min</span> <span class="o">=</span> <span class="n">_mm512_min_epi32</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">max</span> <span class="o">=</span> <span class="n">_mm512_max_epi32</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">x</span>   <span class="o">=</span> <span class="n">_mm512_blend_epi32</span><span class="p">(</span><span class="n">min</span><span class="p">,</span> <span class="n">max</span><span class="p">,</span> <span class="mh">0xAA</span><span class="p">);</span>

    <span class="n">y</span>   <span class="o">=</span> <span class="n">_mm512_shuffle_epi32</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">_MM_PERM_BADC</span><span class="p">);</span>
    <span class="n">min</span> <span class="o">=</span> <span class="n">_mm512_min_epi32</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">max</span> <span class="o">=</span> <span class="n">_mm512_max_epi32</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">x</span>   <span class="o">=</span> <span class="n">_mm512_blend_epi32</span><span class="p">(</span><span class="n">min</span><span class="p">,</span> <span class="n">max</span><span class="p">,</span> <span class="mh">0xCC</span><span class="p">);</span>

    <span class="n">y</span>   <span class="o">=</span> <span class="n">_mm512_shuffle_epi32</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">_MM_PERM_4321</span><span class="p">);</span>
    <span class="n">min</span> <span class="o">=</span> <span class="n">_mm512_min_epi32</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">max</span> <span class="o">=</span> <span class="n">_mm512_max_epi32</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">x</span>   <span class="o">=</span> <span class="n">_mm512_blend_epi32</span><span class="p">(</span><span class="n">min</span><span class="p">,</span> <span class="n">max</span><span class="p">,</span> <span class="mh">0xF0</span><span class="p">);</span>

    <span class="n">y</span>   <span class="o">=</span> <span class="n">_mm512_shuffle_epi32</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">_MM_PERM_CDAB</span><span class="p">);</span>
    <span class="n">min</span> <span class="o">=</span> <span class="n">_mm512_min_epi32</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">max</span> <span class="o">=</span> <span class="n">_mm512_max_epi32</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="o">*</span><span class="n">v</span>  <span class="o">=</span> <span class="n">_mm512_blend_epi32</span><span class="p">(</span><span class="n">min</span><span class="p">,</span> <span class="n">max</span><span class="p">,</span> <span class="mh">0xAA</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Second, merge sorted runs bottom up, doubling the run length each pass like a normal iterative merge sort, but merging 16 elements from each side at a time instead of one. Compare both 16-lane chunks to get a mask of which lane came from which side, popcount that mask to know exactly how many elements the left side contributed this round, and use a masked compress-store to pack just those lanes contiguously into the output, all in one instruction instead of 16 branchy comparisons:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">merge_avx512</span><span class="p">(</span><span class="k">const</span> <span class="kt">int</span> <span class="o">*</span><span class="n">A</span><span class="p">,</span> <span class="k">const</span> <span class="kt">int</span> <span class="o">*</span><span class="n">B</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">OUT</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">N</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">16</span> <span class="o">&lt;=</span> <span class="n">N</span> <span class="o">&amp;&amp;</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">16</span> <span class="o">&lt;=</span> <span class="n">N</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">__m512i</span> <span class="n">bufA</span> <span class="o">=</span> <span class="n">_mm512_loadu_si512</span><span class="p">((</span><span class="n">__m512i</span><span class="o">*</span><span class="p">)(</span><span class="n">A</span> <span class="o">+</span> <span class="n">i</span><span class="p">));</span>
        <span class="n">__m512i</span> <span class="n">bufB</span> <span class="o">=</span> <span class="n">_mm512_loadu_si512</span><span class="p">((</span><span class="n">__m512i</span><span class="o">*</span><span class="p">)(</span><span class="n">B</span> <span class="o">+</span> <span class="n">j</span><span class="p">));</span>
        <span class="n">__mmask16</span> <span class="n">m</span> <span class="o">=</span> <span class="n">_mm512_cmple_epi32_mask</span><span class="p">(</span><span class="n">bufA</span><span class="p">,</span> <span class="n">bufB</span><span class="p">);</span>
        <span class="kt">size_t</span> <span class="n">na</span> <span class="o">=</span> <span class="n">_popcnt32</span><span class="p">(</span><span class="n">m</span><span class="p">);</span>
        <span class="n">_mm512_mask_compressstoreu_epi32</span><span class="p">(</span><span class="n">OUT</span> <span class="o">+</span> <span class="n">k</span><span class="p">,</span>      <span class="n">m</span><span class="p">,</span>  <span class="n">bufA</span><span class="p">);</span>
        <span class="n">_mm512_mask_compressstoreu_epi32</span><span class="p">(</span><span class="n">OUT</span> <span class="o">+</span> <span class="n">k</span> <span class="o">+</span> <span class="n">na</span><span class="p">,</span> <span class="o">~</span><span class="n">m</span><span class="p">,</span> <span class="n">bufB</span><span class="p">);</span>
        <span class="n">i</span> <span class="o">+=</span> <span class="n">na</span><span class="p">;</span> <span class="n">j</span> <span class="o">+=</span> <span class="mi">16</span> <span class="o">-</span> <span class="n">na</span><span class="p">;</span> <span class="n">k</span> <span class="o">+=</span> <span class="mi">16</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">N</span> <span class="o">&amp;&amp;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">)</span> <span class="n">OUT</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">A</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">B</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">?</span> <span class="n">A</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">]</span> <span class="o">:</span> <span class="n">B</span><span class="p">[</span><span class="n">j</span><span class="o">++</span><span class="p">]);</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">)</span> <span class="n">OUT</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">A</span><span class="p">[</span><span class="n">i</span><span class="o">++</span><span class="p">];</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">j</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">)</span> <span class="n">OUT</span><span class="p">[</span><span class="n">k</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">B</span><span class="p">[</span><span class="n">j</span><span class="o">++</span><span class="p">];</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="n">merge_sort_avx512_omp</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">N</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">const</span> <span class="kt">size_t</span> <span class="n">TILE</span> <span class="o">=</span> <span class="mi">16</span><span class="p">;</span>
    <span class="kt">int</span> <span class="o">*</span><span class="n">buffer</span> <span class="o">=</span> <span class="p">(</span><span class="kt">int</span><span class="o">*</span><span class="p">)</span><span class="n">aligned_alloc</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span> <span class="n">N</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">));</span>

    <span class="cp">#pragma omp parallel for schedule(static)
</span>    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">+</span> <span class="n">TILE</span> <span class="o">&lt;=</span> <span class="n">N</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="n">TILE</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">__m512i</span> <span class="n">v</span> <span class="o">=</span> <span class="n">_mm512_loadu_si512</span><span class="p">((</span><span class="n">__m512i</span><span class="o">*</span><span class="p">)(</span><span class="n">data</span> <span class="o">+</span> <span class="n">i</span><span class="p">));</span>
        <span class="n">sort16_inregister</span><span class="p">(</span><span class="o">&amp;</span><span class="n">v</span><span class="p">);</span>
        <span class="n">_mm512_storeu_si512</span><span class="p">((</span><span class="n">__m512i</span><span class="o">*</span><span class="p">)(</span><span class="n">data</span> <span class="o">+</span> <span class="n">i</span><span class="p">),</span> <span class="n">v</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">width</span> <span class="o">=</span> <span class="n">TILE</span><span class="p">;</span> <span class="n">width</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">;</span> <span class="n">width</span> <span class="o">&lt;&lt;=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="cp">#pragma omp parallel for schedule(dynamic)
</span>        <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">start</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">start</span> <span class="o">&lt;</span> <span class="n">N</span><span class="p">;</span> <span class="n">start</span> <span class="o">+=</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">width</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">size_t</span> <span class="n">mid</span> <span class="o">=</span> <span class="n">start</span> <span class="o">+</span> <span class="n">width</span><span class="p">;</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">mid</span> <span class="o">&gt;=</span> <span class="n">N</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>
            <span class="kt">size_t</span> <span class="n">end</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="p">(</span><span class="n">mid</span> <span class="o">+</span> <span class="n">width</span><span class="p">,</span> <span class="n">N</span><span class="p">);</span>
            <span class="n">merge_avx512</span><span class="p">(</span><span class="n">data</span> <span class="o">+</span> <span class="n">start</span><span class="p">,</span> <span class="n">data</span> <span class="o">+</span> <span class="n">mid</span><span class="p">,</span> <span class="n">buffer</span> <span class="o">+</span> <span class="n">start</span><span class="p">,</span> <span class="n">mid</span> <span class="o">-</span> <span class="n">start</span><span class="p">);</span>
            <span class="n">memcpy</span><span class="p">(</span><span class="n">data</span> <span class="o">+</span> <span class="n">start</span><span class="p">,</span> <span class="n">buffer</span> <span class="o">+</span> <span class="n">start</span><span class="p">,</span> <span class="p">(</span><span class="n">end</span> <span class="o">-</span> <span class="n">start</span><span class="p">)</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">));</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">free</span><span class="p">(</span><span class="n">buffer</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="algorithms" />
      
        <category term="cpp" />
      
        <category term="parallel-computing" />
      
        <category term="performance" />
      

      
      
        <summary type="html"><![CDATA[I have been having a lot of fun on weekends writing classicial algorithms by hand without the help of any coding agents, this is an attempt of a writeup of my parallel merge sort implementation. Merge sort is one of the easiest algorithms to parallelize because inherently it divides the work into smaller subproblems that can be solved independently. I also have a parrallel quick sort implementation which is quite fascinating to understand but I will write it up in a separate post. Also this reminds me the very first post I wrote on this blog was a parallel median filter implementation that works over arbitrary datatypes such fun times those were a year back : )]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">Choosing a Hard Enough Problem</title>
      <link href="https://darshanmakwana412.github.io/2026/07/choosing-a-hard-enough-problem/" rel="alternate" type="text/html" title="Choosing a Hard Enough Problem" />
      <published>2026-07-23T00:00:00+05:30</published>
      <updated>2026-07-23T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/07/choosing-a-hard-enough-problem</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/07/choosing-a-hard-enough-problem/"><![CDATA[<p>9:53AM: Early this morning as I walking along the streets of HSR layout I was again visited by the surprising realization that everyone around me, each and every individual that I saw today had some desire in their life or still has that and they are actively working each and every day towards realizing it. Along the way the journey or the path that they have followed has shaped them and their life than the initial desire that they started walking on that path, maybe this is a crude way of writing it down but the point being the journey/path that you are following for your ambitions gives more shape on your life and the way you think about your life more than the actual goal itself</p>

<p>As a corollary there is also no point is choosing a very ambigious goal for yourself or choosing to work on a very very ambitious problem but then deciding to go via shortcuts to achieve it. There is also no point in doing hard work and sweating over problems that are too small to waste your time on</p>

<p>If you have decided to work on a very ambigious goals for your self and also decided to walk on that path, you need to be comfortable with failing, have pragmatism even in the worst of times, relentless pursuit to pioneer and the exceptional ability to never give up. If you are afraid of taking decisions or saying stuff because you think it will make you look boring, cringe, you are not going to make it and it’s a signal that you are again diverting from the path that you decided to once follow</p>

<p>This is a good enough proxy or good enough signal to measure if the things that I have decided to work on or problems which I am solving, are they worth solving and will they eventually lead to something important and will they make my life and the life of everyone around me much better</p>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="philosophy" />
      
        <category term="productivity" />
      
        <category term="walk" />
      
        <category term="goals" />
      

      
      
        <summary type="html"><![CDATA[9:53AM: Early this morning as I walking along the streets of HSR layout I was again visited by the surprising realization that everyone around me, each and every individual that I saw today had some desire in their life or still has that and they are actively working each and every day towards realizing it. Along the way the journey or the path that they have followed has shaped them and their life than the initial desire that they started walking on that path, maybe this is a crude way of writing it down but the point being the journey/path that you are following for your ambitions gives more shape on your life and the way you think about your life more than the actual goal itself]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">Framework for Winning</title>
      <link href="https://darshanmakwana412.github.io/2026/06/framework-for-winning/" rel="alternate" type="text/html" title="Framework for Winning" />
      <published>2026-06-20T00:00:00+05:30</published>
      <updated>2026-06-20T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/06/framework-for-winning</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/06/framework-for-winning/"><![CDATA[<p>Time and time I have found myself at different crossroads in life. These are the principles that I have developed by my constant failures, observations and interactions overtime to achieve unfair advantage and keep on winning</p>

<ul>
  <li><strong>Don’t Die</strong>: in the sense that don’t ever go to the point of no return, where you can never get back up. Be obsessed with the death boundaries and downside scenarios and always prioritize survival</li>
  <li><strong>Never Quit:</strong> Things take time, some people die from an unlucky accident on a random day, some people make the fortune of their life in a day but these are rather fleeting events. Sharp rises are always met with sharp declines. If you set your mind to something then why quit what changed? Darshan if you followed this framework you are not dead then keep moving you are still in the game. “Too early” happens only when you quit. When you feel down or feel quitting, score yourself on a different scoreboard and start compounding the results. Some of the best things started slow (pytorch, transformer, nvidia) and then compounded to heaven. Have patience. <a href="https://blog.jakobschwichtenberg.com/p/slowness-is-a-virtue">slowness is needed when you are finding out your goal on a map</a></li>
  <li><strong>Lock In:</strong> Be obsessed over the thing that you are working on, Always be in the game observe the game and their players, how they are playing the game. There are many players playing the same game which you are. If you are not spending 100% of your focus on the game, you are already out of the game. There will be many things that prevent you to get lock in 100%, think about ways to delete, defer or delegate these tasks</li>
  <li><strong>Do Hard Things:</strong> Doing hard things is weird but that is okay because winning is different and makes you weird and means that you are different from others. You will look like a noob and a fool frankly when you do hard things. They will underestimate you but you are compounding. You will create a massive vertical between you and the next participant of the game before they know what hit them. People by nature take the path of least resistance, don’t be like them. It’s very easy to build the surface thing but the right (hard) infrastructure, mental model or technology compounds. Doing hard things allows you to go deeper into things and gain advantage over others</li>
  <li><strong>Play by the Rules:</strong> This is one thing that the universe does not reward. There are people who don’t play by the rules but they are exceptions or their time will come. Don’t be tempted to do the same. Stay focused. Playing by the rules is hard. But it is the hard things in life that force you to innovate your way out of problems. These innovations compound in weird and interesting patterns resulting in long term moats. There is an irish revolutionary song that encapsulates “The higher you build your barricades the stronger we become”</li>
  <li><strong>Don’t chase after thin desires:</strong> <a href="https://www.joanwestenberg.com/thin-desires-are-eating-your-life/%20">thin desires</a> are an everlasting hunger for things that we can’t articulate why we want them, and cannot actually attain them, from a source that has no interest in providing them. Chase thick desires instead, they transform/shape/mold you in the act of pursuing them. Thick desires are effortful to acquire and are impossible to gratify. Thick desires also make you antifragile. It’s completely fine to have desires but make sure they are worth pursuing and they will shape you in the process of pursuing them</li>
  <li><strong>Put Something Back:</strong> Putting something back seems neat, but it also helps you in the long term. Karms seems real. When you put something back into the pool, your reputation grows, people will be familiar with your work and they will want to be around you and work for you or with you. Winning can be compounded by giving back. Win and give to win more later</li>
  <li><strong>Don’t become the machine:</strong> Everyone seems to praise the hustle culture these days, you have to work harder than that, you have to push yourself everyday, you have to wake up at 5am and yade yada. You don’t have to become a machine to succeed. Just do the work at your body’s natural pace. Your body will tell you when it’s enough and what it’s limits are. Efficiency is a measure for the machines, please don’t eval yourself like a machine. Aim to be nimble, adapt quickly to uncertainties, be anti fragile</li>
  <li><strong>Update your mental models:</strong> When you know so little, you don’t know how much there is to know and things feel trivial and everything you think is clear. You are also more certain about it. Infact certainty peaks before competence in the <a href="https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect">dunning kruger curve</a>. The less you know the more you seem to be confident about it. It’s far riskier to be certainly incorrect most of the time than be uncertainly correct all the time.</li>
  <li><strong>Cultivate Confidence:</strong> Invest in things that challenge your confidence and let your anxiety go away. <strong>Being shy is not an excuse for being rude.</strong> Confidence in other player’s cooperation also influences our own actions. Having negative interactions can lead to a downward spiral going all the way down and taking you with it. Have positive interactions with people and be around people with very high urgency and agency. The positive spiral can take you to the stairway to heaven</li>
  <li><strong>You have to be strong:</strong> This shit ain’t for the weak. They will eat you, they will hunt you, they will abuse you, they will laugh at you, they want you to be down, every time you spent crying, spiraling downward they will win. The trick is to adopt the stoic principles of focusing no what you can control and ignore what you can’t. Your can’t control when a tragedy is going to happen but you can control how you respond and what you learn from it</li>
  <li><strong>Invest in People:</strong> I believed that your work speaks for itself, no it doesn’t it just sits silently in your repository. Your manager mentions you in a conversation or a meeting. A peer who you spent 5 mins chatting with you gets impressed and recommends you for a short project. Decisions get made in meetings you are not invited, using summaries you didn’t wrote by people who have 5 mins and 12 different tasks to work on. If no one can articulate your impact when you are not in the room then your impact is optional. Make the value chain legible to everyone including yourself</li>
</ul>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="philosophy" />
      
        <category term="mindset" />
      
        <category term="principles" />
      
        <category term="productivity" />
      

      
      
        <summary type="html"><![CDATA[Time and time I have found myself at different crossroads in life. These are the principles that I have developed by my constant failures, observations and interactions overtime to achieve unfair advantage and keep on winning]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">After 3 months of using a standing desk</title>
      <link href="https://darshanmakwana412.github.io/2026/05/standing-desk/" rel="alternate" type="text/html" title="After 3 months of using a standing desk" />
      <published>2026-05-06T00:00:00+05:30</published>
      <updated>2026-05-06T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/05/standing-desk</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/05/standing-desk/"><![CDATA[<p>Since the last 2 months I had to change my environment in which I used to do my long hours of deep work. One of the things that was part of the change was a standing desk. Actually it was not a desk per se but just a platform on top of which I can keep my laptop and just start coding away, the platform was just tall enough that I can’t sit and do work and slightly short enought that I had to bend my knees, so I had to add a wooden plank on top of the platform to level it up.</p>

<p>So after this I just went away with this setup. The first couple of days were not bad I mean my legs were already in pretty good shape as I have a regular running habit. I wasn’t feeling fatigued or lethargic at all, I would say I was more aware of my own physical body or how much energy/force it has to exert while doing a task which I used to do just sitting. I was just slightly distracted by this fact but not distracted enough to not be able to do any work at all. After the first week it became natural to do long hours of work in this new setup of mine.</p>

<p>I have observed that the speed at which I am approaching and getting things done has slightly increased. I have also become more proactive and more communicative. I now love standing while working. I have now kept two 10L water jugs alongside my desk and I have been experimenting with it doing bicep curls/hammer curls/triceps in every 30mins interval, I have also experimented with adding pushups in this combo, it has been working out fantastic for me. I think this is clear connection between productivity and slight pressure to body to keep it in order, every time we lift weights or make our muscles work under pressure we are unconsciously reminding our mind that we are control of our physical selves and our actions does have meaningful impact in the world that we live in</p>

<p>I remain energetic even after long hours of deep work, I don’t get tired after a long day which I usually felt previously. I don’t think weather the gamified setup of exercise + work or the standing desk is the causation of this, but I can confirm there is definitely a correlation and it has been working out fanstastic for me</p>

<p>I gave a couple of interviews in the last months and I cleared a majority of them while standing. I now speak more energetically and aggressively while I am standing. I have noticed that when I don’t get exercise or my body does not undergo any form of activity I simply cannot concentrate and if I am in chair then end up spending my day chaining positions</p>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="productivity" />
      
        <category term="health" />
      
        <category term="lifestyle" />
      

      
      
        <summary type="html"><![CDATA[Since the last 2 months I had to change my environment in which I used to do my long hours of deep work. One of the things that was part of the change was a standing desk. Actually it was not a desk per se but just a platform on top of which I can keep my laptop and just start coding away, the platform was just tall enough that I can’t sit and do work and slightly short enought that I had to bend my knees, so I had to add a wooden plank on top of the platform to level it up.]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">Gaussian Splatting for Dummies</title>
      <link href="https://darshanmakwana412.github.io/2026/04/gaussian-splatting/" rel="alternate" type="text/html" title="Gaussian Splatting for Dummies" />
      <published>2026-04-12T00:00:00+05:30</published>
      <updated>2026-04-12T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/04/gaussian-splatting</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/04/gaussian-splatting/"><![CDATA[<p>Gaussian Splatting is a fascinating scene reconstruction technique introduced by <a href="https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/">INRIA</a>  and last year I had a lot of fun tinkering with it while on my semex. I recently discovered some of my notes related to it and decided to digitize it this weekend, along the way I reimplemented the forward rasterization pass in rust and decided it would be fun to write a tutorial explaining gaussian splatting to everyone, so here it is</p>
<h2 id="what-is-a-gaussian-splat">what is a gaussian splat?</h2>

<p>a 3D Gaussian splat is an oriented ellipsoid in space that carries some color and opacity. you can think of it as a fuzzy colored blob. a scene is made of hundreds of thousands of these blobs, and when you look at them from a particular viewpoint, they overlap and blend to form the final image</p>

<p>We represent each gaussian with these attributes:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">Splat</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">pos</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>      <span class="c1">// center position in world space</span>
    <span class="k">pub</span> <span class="n">scale</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>    <span class="c1">// size along each local axis</span>
    <span class="k">pub</span> <span class="n">rot</span><span class="p">:</span> <span class="n">Quat</span><span class="p">,</span>      <span class="c1">// orientation as a unit quaternion</span>
    <span class="k">pub</span> <span class="n">color</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>    <span class="c1">// RGB color (already decoded from spherical harmonics)</span>
    <span class="k">pub</span> <span class="n">opacity</span><span class="p">:</span> <span class="nb">f32</span><span class="p">,</span>   <span class="c1">// how opaque this blob is, in [0, 1]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>scales are stored in log space, opacities as logits, colors as spherical harmonics coefficients and quaternions are normzlied to unit length to ensure the values lie within their respective range</p>

<p><img src="/assets/images/posts/Pasted image 20260412233548.png" alt="Pasted image 20260412233548.png" /></p>

<p>spherical harmonic (SH) coefficients are just a frequency-domain representation of a color function defined over the unit sphere, now why spherical harmonics? because in the real world, the color of a surface depends on the viewing direction. SH coefficients encode this view-dependent appearance compactly.</p>

<p>SH functions are organized in bands (like octaves in music), as you go higher up in the bands you have more coefficients and thus they capture more finer details, the INRIA 3DGS format stores up to band 3 (48 coefficients per splat for RGB)</p>

<p>To decode bash 0, the band-0 SH basis function is $Y_0^0 = \frac{1}{2\sqrt{\pi}} \approx 0.282$. the conversion from SH coefficient to RGB is:</p>

\[\text{color} = \text{clamp}\left(0.5 + C_0 \cdot f_{dc},\ 0,\ 1\right)\]

<p>where $C_0 = Y_0^0$ and $f_{dc}$ is the 3-component DC coefficient from the file.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">const</span> <span class="n">SH_C0</span><span class="p">:</span> <span class="nb">f32</span> <span class="o">=</span> <span class="mf">0.28209479177387814</span><span class="p">;</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">sh_band0_to_rgb</span><span class="p">(</span><span class="n">f_dc</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Vec3</span> <span class="p">{</span>
    <span class="p">(</span><span class="nn">Vec3</span><span class="p">::</span><span class="nf">splat</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span> <span class="o">+</span> <span class="n">SH_C0</span> <span class="o">*</span> <span class="n">f_dc</span><span class="p">)</span><span class="nf">.clamp</span><span class="p">(</span><span class="nn">Vec3</span><span class="p">::</span><span class="n">ZERO</span><span class="p">,</span> <span class="nn">Vec3</span><span class="p">::</span><span class="n">ONE</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-forward-pass-pipeline">the forward pass pipeline</h2>

<p>the forward pass turns a list of 3D Gaussians + a camera into a 2D image. here is an overview of the rendering pipeline:</p>

<p><img src="/assets/images/posts/Pasted image 20260412233610.png" alt="Pasted image 20260412233610.png" /></p>

<h2 id="step-1-projecting-splats">Step 1: Projecting Splats</h2>

<h3 id="11-building-the-3d-covariance-matrix">1.1: building the 3D covariance matrix</h3>

<p>for each splat given the raw <code class="language-plaintext highlighter-rouge">(scale, rotation)</code> pairs we need to construct a 3D covariance matrix $\Sigma$ that describes the shape and orientation of the Gaussian in world space. the formula is:</p>

\[\Sigma = R \cdot S \cdot S^T \cdot R^T\]

<p>where R is the 3×3 rotation matrix from the quaternion, and S is a diagonal matrix of scales. if we let M = R·S, this simplifies to:</p>

\[\Sigma = M \cdot M^T\]

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">r_mat</span> <span class="o">=</span> <span class="nn">Mat3</span><span class="p">::</span><span class="nf">from_quat</span><span class="p">(</span><span class="n">s</span><span class="py">.rot</span><span class="p">);</span>
<span class="k">let</span> <span class="n">s_mat</span> <span class="o">=</span> <span class="nn">Mat3</span><span class="p">::</span><span class="nf">from_diagonal</span><span class="p">(</span><span class="n">s</span><span class="py">.scale</span><span class="p">);</span>
<span class="k">let</span> <span class="n">m</span> <span class="o">=</span> <span class="n">r_mat</span> <span class="o">*</span> <span class="n">s_mat</span><span class="p">;</span>
<span class="k">let</span> <span class="n">cov3d</span> <span class="o">=</span> <span class="n">m</span> <span class="o">*</span> <span class="n">m</span><span class="nf">.transpose</span><span class="p">();</span>
</code></pre></div></div>

<hr />

<p><strong>Note: why dowe decompose the covariance this way?</strong></p>

<p>Covariance matrices have physical meaning only when they are <strong>positive semi-definite</strong>. gradient descent cannot easily be constrained to produce valid matrices, by expressing the covariance as $M \cdot M^T$, it is guaranteed to be positive semi-definite, a matrix of the form $A^T A$ always is. this is a reparametrization trick: we optimize <code class="language-plaintext highlighter-rouge">scale</code> and <code class="language-plaintext highlighter-rouge">rotation</code> separately, which are unconstrained, and the covariance we derive from them is always valid</p>

<hr />

<p>what does this matrix actually look like? for a splat with <code class="language-plaintext highlighter-rouge">scale = (0.1, 0.05, 0.02)</code> and identity rotation:</p>

\[\Sigma =
\begin{pmatrix}
0.01 &amp; 0 &amp; 0 \\
0 &amp; 0.0025 &amp; 0 \\
0 &amp; 0 &amp; 0.0004
\end{pmatrix}
\quad
\begin{aligned}
&amp;= \text{diag}(0.1^2,\; 0.05^2,\; 0.02^2)
\end{aligned}\]

<p>with identity rotation, it is just the squared scales on the diagonal, an axis-aligned ellipsoid</p>

<h3 id="12-transforming-into-view-space">1.2: transforming into view space</h3>

<p>the 3D covariance we just computed lives in world space. to project it onto the camera’s image plane, we first need to rotate it into view space, the coordinate system where the camera is at the origin, looking down −z</p>

<p>for the splat center, this is just a matrix-vector multiply with the 4×4 view matrix:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">p_view4</span> <span class="o">=</span> <span class="n">view</span> <span class="o">*</span> <span class="nn">Vec4</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">s</span><span class="py">.pos.x</span><span class="p">,</span> <span class="n">s</span><span class="py">.pos.y</span><span class="p">,</span> <span class="n">s</span><span class="py">.pos.z</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">);</span>
<span class="k">let</span> <span class="n">p_view</span> <span class="o">=</span> <span class="nn">Vec3</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">p_view4</span><span class="py">.x</span><span class="p">,</span> <span class="n">p_view4</span><span class="py">.y</span><span class="p">,</span> <span class="n">p_view4</span><span class="py">.z</span><span class="p">);</span>
<span class="k">if</span> <span class="n">p_view</span><span class="py">.z</span> <span class="o">&gt;</span> <span class="o">-</span><span class="n">znear</span> <span class="p">||</span> <span class="n">p_view</span><span class="py">.z</span> <span class="o">&lt;</span> <span class="o">-</span><span class="n">zfar</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nb">None</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">let</span> <span class="n">zc</span> <span class="o">=</span> <span class="o">-</span><span class="n">p_view</span><span class="py">.z</span><span class="p">;</span>
</code></pre></div></div>

<p>note <code class="language-plaintext highlighter-rouge">zc = -p_view.z</code>. our view space is right-handed with the camera looking down <strong>−z</strong>, so points in front of the camera have negative z. we use <code class="language-plaintext highlighter-rouge">zc</code> (positive in front) as the depth for sorting and projection.</p>

<p>for the covariance, we rotate it by the 3×3 part of the view matrix W:</p>

\[\Sigma_{view} = W \cdot \Sigma \cdot W^T\]

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">w_mat</span> <span class="o">=</span> <span class="nn">Mat3</span><span class="p">::</span><span class="nf">from_mat4</span><span class="p">(</span><span class="n">view</span><span class="p">);</span>
<span class="k">let</span> <span class="n">w_mat_t</span> <span class="o">=</span> <span class="n">w_mat</span><span class="nf">.transpose</span><span class="p">();</span>

<span class="k">let</span> <span class="n">cov3d_view</span> <span class="o">=</span> <span class="n">w_mat</span> <span class="o">*</span> <span class="n">cov3d</span> <span class="o">*</span> <span class="n">w_mat_t</span><span class="p">;</span>
</code></pre></div></div>

<p>this is just the standard basis-change formula for a covariance matrix. the shape of the ellipsoid does not change due to this infact we are only re-expressing it in the camera’s coordinate system.</p>

<h3 id="13-projecting-to-2d">1.3: projecting to 2D</h3>

<p>now we have a 3D Gaussian in view space and we need to project it onto the 2D image plane. the projection is perspective, which means a 3D Gaussian does not project to an exact 2D Gaussian because perspective is a nonlinear transform. but we can locally linearize it using the Jacobian of the projection function, and the result is close enough.</p>

<p>the projection function maps a 3D point (x, y, z) in view space to pixel coordinates (u, v):</p>

<p>\(u = f_x \cdot \frac{x}{z_c} + c_x\)
\(v = f_y \cdot \frac{y}{z_c} + c_y\)</p>

<p>where $f_x, f_y$ are the focal lengths and $c_x, c_y$ are the principal point (image center). for simplicity of calcuations we can also assume, $f_x = f_y$</p>

<p>the Jacobian J of this projection evaluated at the splat center is:</p>

\[J = \begin{bmatrix} \frac{f_x}{z_c} &amp; 0 &amp; \frac{f_x \cdot x_v}{z_c^2} \\ 0 &amp; \frac{f_y}{z_c} &amp; \frac{f_y \cdot y_v}{z_c^2} \\ 0 &amp; 0 &amp; 0 \end{bmatrix}\]

<p>the structure of this matrix is very sparse, only 4 of the 9 entries are nonzero.we can just do the full $JC J^T$ with two 3×3 matrix multiplies (~54 scalar multiplies). but because we only need the top-left $2\times 2$ of $J$ Cov3D_view $J^T$, since the third row of $J$ is all zeros. and the first two rows of J each have only two nonzero entries. so instead of two full matrix multiplies, we can compute the 2D covariance with ~20 scalar multiplies by expanding the product by hand:</p>

<hr />

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">c</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">cov3d_view</span><span class="p">;</span>
<span class="k">let</span> <span class="n">inv_zc</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="n">zc</span><span class="p">;</span>
<span class="k">let</span> <span class="n">inv_zc2</span> <span class="o">=</span> <span class="n">inv_zc</span> <span class="o">*</span> <span class="n">inv_zc</span><span class="p">;</span>

<span class="k">let</span> <span class="n">j00</span> <span class="o">=</span> <span class="n">fx</span> <span class="o">*</span> <span class="n">inv_zc</span><span class="p">;</span>
<span class="k">let</span> <span class="n">j02</span> <span class="o">=</span> <span class="n">fx</span> <span class="o">*</span> <span class="n">xv</span> <span class="o">*</span> <span class="n">inv_zc2</span><span class="p">;</span>
<span class="k">let</span> <span class="n">j11</span> <span class="o">=</span> <span class="n">fy</span> <span class="o">*</span> <span class="n">inv_zc</span><span class="p">;</span>
<span class="k">let</span> <span class="n">j12</span> <span class="o">=</span> <span class="n">fy</span> <span class="o">*</span> <span class="n">yv</span> <span class="o">*</span> <span class="n">inv_zc2</span><span class="p">;</span>

<span class="c1">// Row 0 of J * C: [j00*c00 + j02*c20, j00*c01 + j02*c21, j00*c02 + j02*c22]</span>
<span class="k">let</span> <span class="n">t0x</span> <span class="o">=</span> <span class="n">j00</span> <span class="o">*</span> <span class="n">c</span><span class="py">.x_axis.x</span> <span class="o">+</span> <span class="n">j02</span> <span class="o">*</span> <span class="n">c</span><span class="py">.z_axis.x</span><span class="p">;</span>
<span class="k">let</span> <span class="n">t0y</span> <span class="o">=</span> <span class="n">j00</span> <span class="o">*</span> <span class="n">c</span><span class="py">.y_axis.x</span> <span class="o">+</span> <span class="n">j02</span> <span class="o">*</span> <span class="n">c</span><span class="py">.z_axis.y</span><span class="p">;</span>
<span class="k">let</span> <span class="n">t0z</span> <span class="o">=</span> <span class="n">j00</span> <span class="o">*</span> <span class="n">c</span><span class="py">.x_axis.z</span> <span class="o">+</span> <span class="n">j02</span> <span class="o">*</span> <span class="n">c</span><span class="py">.z_axis.z</span><span class="p">;</span>

<span class="c1">// Row 1 of J * C: [j11*c10 + j12*c20, j11*c11 + j12*c21, j11*c12 + j12*c22]</span>
<span class="k">let</span> <span class="n">t1y</span> <span class="o">=</span> <span class="n">j11</span> <span class="o">*</span> <span class="n">c</span><span class="py">.y_axis.y</span> <span class="o">+</span> <span class="n">j12</span> <span class="o">*</span> <span class="n">c</span><span class="py">.z_axis.y</span><span class="p">;</span>
<span class="k">let</span> <span class="n">t1z</span> <span class="o">=</span> <span class="n">j11</span> <span class="o">*</span> <span class="n">c</span><span class="py">.y_axis.z</span> <span class="o">+</span> <span class="n">j12</span> <span class="o">*</span> <span class="n">c</span><span class="py">.z_axis.z</span><span class="p">;</span>

<span class="c1">// 2D cov = (J*C) * J^T, top-left 2x2:</span>
<span class="k">let</span> <span class="n">cov2d_00</span> <span class="o">=</span> <span class="n">t0x</span> <span class="o">*</span> <span class="n">j00</span> <span class="o">+</span> <span class="n">t0z</span> <span class="o">*</span> <span class="n">j02</span> <span class="o">+</span> <span class="n">eps2d</span><span class="p">;</span>
<span class="k">let</span> <span class="n">cov2d_01</span> <span class="o">=</span> <span class="n">t0y</span> <span class="o">*</span> <span class="n">j11</span> <span class="o">+</span> <span class="n">t0z</span> <span class="o">*</span> <span class="n">j12</span><span class="p">;</span>
<span class="k">let</span> <span class="n">cov2d_11</span> <span class="o">=</span> <span class="n">t1y</span> <span class="o">*</span> <span class="n">j11</span> <span class="o">+</span> <span class="n">t1z</span> <span class="o">*</span> <span class="n">j12</span> <span class="o">+</span> <span class="n">eps2d</span><span class="p">;</span>
</code></pre></div></div>

<p>notice the eps2d on the diagonal entries. that is a small dilation (default 0.3) added for numerical stability, it just ensures the 2D covariance is strictly positive definite (not just semi-definite), which means it is always invertible.</p>

<hr />

<p><strong>Note: why the eps2d trick works</strong></p>

<p>by construction, the 2D covariance $JCJ^T$ is only positive semi-definite ($A^T A$ form). but we need to invert it later (for evaluating the Gaussian at each pixel). a singular matrix is not invertible.</p>

<p>adding eps2d to the diagonal means adding $\lambda I$ to the matrix. for any vector x:</p>

\[x^T \cdot (A^T A + \lambda I) \cdot x = \|Ax\|^2 + \lambda \|x\|^2 &gt; 0\]

<p>this is strictly positive for any nonzero x, which is the definition of positive definite, invertible, with all eigenvalues strictly positive</p>

<hr />

<p>next we invert the $2\times 2$ covariance. for a $2\times 2$ matrix the inverse has a closed form:</p>

\[\begin{bmatrix} a &amp; b \\ b &amp; d \end{bmatrix}^{-1} = \frac{1}{ad - b^2} \begin{bmatrix} d &amp; -b \\ -b &amp; a \end{bmatrix}\]

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">det</span> <span class="o">=</span> <span class="n">cov2d_00</span> <span class="o">*</span> <span class="n">cov2d_11</span> <span class="o">-</span> <span class="n">cov2d_01</span> <span class="o">*</span> <span class="n">cov2d_01</span><span class="p">;</span>
<span class="k">if</span> <span class="n">det</span> <span class="o">&lt;=</span> <span class="mf">0.0</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nb">None</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">let</span> <span class="n">inv_det</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="n">det</span><span class="p">;</span>
<span class="k">let</span> <span class="n">cov2d_inv</span> <span class="o">=</span> <span class="nn">Mat2</span><span class="p">::</span><span class="nf">from_cols</span><span class="p">(</span>
    <span class="nn">Vec2</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">cov2d_11</span> <span class="o">*</span> <span class="n">inv_det</span><span class="p">,</span> <span class="o">-</span><span class="n">cov2d_01</span> <span class="o">*</span> <span class="n">inv_det</span><span class="p">),</span>
    <span class="nn">Vec2</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">-</span><span class="n">cov2d_01</span> <span class="o">*</span> <span class="n">inv_det</span><span class="p">,</span> <span class="n">cov2d_00</span> <span class="o">*</span> <span class="n">inv_det</span><span class="p">),</span>
<span class="p">);</span>
</code></pre></div></div>

<p>and the screen position is a standard perspective divide:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">sx</span> <span class="o">=</span> <span class="n">fx</span> <span class="o">*</span> <span class="n">xv</span> <span class="o">*</span> <span class="n">inv_zc</span> <span class="o">+</span> <span class="n">cx</span><span class="p">;</span>
<span class="k">let</span> <span class="n">sy</span> <span class="o">=</span> <span class="n">fy</span> <span class="o">*</span> <span class="n">yv</span> <span class="o">*</span> <span class="n">inv_zc</span> <span class="o">+</span> <span class="n">cy</span><span class="p">;</span>
</code></pre></div></div>

<p>at this point, for each splat we have: screen position <code class="language-plaintext highlighter-rouge">(sx, sy)</code>, depth <code class="language-plaintext highlighter-rouge">zc</code>, and the inverse 2D covariance matrix <code class="language-plaintext highlighter-rouge">cov2d_inv</code>. this is everything we need to evaluate the Gaussian at any pixel on screen.</p>

<h2 id="step-2-computing-bounding-boxes">step 2: computing bounding boxes</h2>

<p>We would now have to evalute the splat at every pixel in the screen because the 2D gaussian has infinite support, it never truly reaches zero, but we can compute a bounding box that encloses the region where the gaussian has any visible effect and only evalute pixels inside this bounding box</p>

<p>the original 3DGS code computes the two eigenvalues $\lambda_1$, $\lambda_2$ of the 2D covariance (the variances along the two principal axes of the ellipse), takes $r = 3\sqrt{max(\lambda_1, \lambda_2)}$ (the 3-sigma rule, covering 99.7% of the Gaussian), and uses a circle of that radius as the bounding box</p>

<p>this is simple but wasteful. when a Gaussian is elongated (one eigenvalue much larger than the other), the bounding circle will also include a lot of empty space</p>

<p><img src="/assets/images/posts/Pasted image 20260412233627.png" alt="Pasted image 20260412233627.png" /></p>

<p>We can create a tighter bounding box by observing that:</p>

<ol>
  <li>
    <p>the extent along each axis is $k\sqrt{\Sigma_{ii}}$ where $\Sigma_{ii}$ is the diagonal entry of the 2D covariance for that axis. for an elongated ellipse, the short-axis extent is much smaller than the long-axis extent, so the bounding box is tighter.</p>
  </li>
  <li>
    <p>the classic $3\sigma$ rule is conservative. a faint splat (low opacity) does not need $3\sigma$ because its contribution drops below the visibility threshold much sooner. the cutoff distance $k$ can be computed when our splat’s opacity falls below certain threshold $\tau$</p>
  </li>
</ol>

<p>based on this each pixel gets</p>

\[\alpha = \text{opacity} \cdot \exp(-\tfrac{1}{2} \cdot \mathbf{d}^T \Sigma^{-1} \mathbf{d})\]

<p>we want $\alpha \geq \tau$, which rearranges to:</p>

\[\mathbf{d}^T \Sigma^{-1} \mathbf{d} \leq 2 \ln\left(\frac{\text{opacity}}{\tau}\right) = k^2\]

<p>for a near-opaque splat (opacity = 1), $k^2 = 2ln(255) = 11.1$, which is close to the $3\sigma$ value of 9. for a faint splat (opacity = 0.1), $k^2 = 2ln(255) = 11.1$, so the box and computation shrinks substantially.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">s</span><span class="py">.opacity</span> <span class="o">&lt;=</span> <span class="n">alpha_threshold</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nb">None</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">let</span> <span class="n">k2</span> <span class="o">=</span> <span class="p">(</span><span class="mf">2.0</span> <span class="o">*</span> <span class="p">(</span><span class="n">s</span><span class="py">.opacity</span> <span class="o">/</span> <span class="n">alpha_threshold</span><span class="p">)</span><span class="nf">.ln</span><span class="p">())</span><span class="nf">.min</span><span class="p">(</span><span class="n">max_k2</span><span class="p">);</span>
<span class="k">if</span> <span class="o">!</span><span class="p">(</span><span class="n">k2</span> <span class="o">&gt;</span> <span class="mf">0.0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nb">None</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">let</span> <span class="n">rx_f</span> <span class="o">=</span> <span class="p">(</span><span class="n">k2</span> <span class="o">*</span> <span class="n">cov2d_00</span><span class="p">)</span><span class="nf">.sqrt</span><span class="p">();</span>
<span class="k">let</span> <span class="n">ry_f</span> <span class="o">=</span> <span class="p">(</span><span class="n">k2</span> <span class="o">*</span> <span class="n">cov2d_11</span><span class="p">)</span><span class="nf">.sqrt</span><span class="p">();</span>
<span class="k">if</span> <span class="o">!</span><span class="n">rx_f</span><span class="nf">.is_finite</span><span class="p">()</span> <span class="p">||</span> <span class="o">!</span><span class="n">ry_f</span><span class="nf">.is_finite</span><span class="p">()</span> <span class="p">||</span> <span class="n">rx_f</span> <span class="o">&lt;</span> <span class="mf">1.0</span> <span class="p">||</span> <span class="n">ry_f</span> <span class="o">&lt;</span> <span class="mf">1.0</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nb">None</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">let</span> <span class="n">x0</span> <span class="o">=</span> <span class="p">(</span><span class="n">sx</span> <span class="o">-</span> <span class="n">rx_f</span><span class="p">)</span><span class="nf">.floor</span><span class="p">()</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
<span class="k">let</span> <span class="n">y0</span> <span class="o">=</span> <span class="p">(</span><span class="n">sy</span> <span class="o">-</span> <span class="n">ry_f</span><span class="p">)</span><span class="nf">.floor</span><span class="p">()</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
<span class="k">let</span> <span class="n">x1</span> <span class="o">=</span> <span class="p">(</span><span class="n">sx</span> <span class="o">+</span> <span class="n">rx_f</span><span class="p">)</span><span class="nf">.ceil</span><span class="p">()</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
<span class="k">let</span> <span class="n">y1</span> <span class="o">=</span> <span class="p">(</span><span class="n">sy</span> <span class="o">+</span> <span class="n">ry_f</span><span class="p">)</span><span class="nf">.ceil</span><span class="p">()</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>

<span class="c1">// Clip to framebuffer.</span>
<span class="k">let</span> <span class="n">x0</span> <span class="o">=</span> <span class="n">x0</span><span class="nf">.max</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="k">let</span> <span class="n">y0</span> <span class="o">=</span> <span class="n">y0</span><span class="nf">.max</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="k">let</span> <span class="n">x1</span> <span class="o">=</span> <span class="n">x1</span><span class="nf">.min</span><span class="p">(</span><span class="n">w_i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">let</span> <span class="n">y1</span> <span class="o">=</span> <span class="n">y1</span><span class="nf">.min</span><span class="p">(</span><span class="n">h_i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">if</span> <span class="n">x0</span> <span class="o">&gt;</span> <span class="n">x1</span> <span class="p">||</span> <span class="n">y0</span> <span class="o">&gt;</span> <span class="n">y1</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nb">None</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Also because each splat is independent of each other the projection computation is embarassingly parallel, we use rayon’s <code class="language-plaintext highlighter-rouge">par_iter().filter_map()</code> to project all splats across all cores:</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">project</span><span class="p">(</span>
    <span class="n">splats</span><span class="p">:</span> <span class="o">&amp;</span><span class="p">[</span><span class="n">Splat</span><span class="p">],</span>
    <span class="n">camera</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">OrbitCamera</span><span class="p">,</span>
    <span class="n">params</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">RenderParams</span><span class="p">,</span>
    <span class="n">pool</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">Option</span><span class="o">&lt;</span><span class="nn">rayon</span><span class="p">::</span><span class="n">ThreadPool</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Projected</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// ... precompute view matrix, intrinsics, etc (once per frame) ...</span>

    <span class="k">let</span> <span class="n">do_project</span> <span class="o">=</span> <span class="p">||</span> <span class="p">{</span>
        <span class="n">splats</span>
            <span class="nf">.par_iter</span><span class="p">()</span>
            <span class="nf">.filter_map</span><span class="p">(|</span><span class="n">s</span><span class="p">|</span> <span class="p">{</span>
                <span class="c1">// ... all the math above, returning Some(Projected) or None ...</span>
            <span class="p">})</span>
            <span class="nf">.collect</span><span class="p">()</span>
    <span class="p">};</span>

    <span class="k">match</span> <span class="n">pool</span><span class="nf">.as_ref</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">Some</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">p</span><span class="nf">.install</span><span class="p">(</span><span class="n">do_project</span><span class="p">),</span>
        <span class="nb">None</span> <span class="k">=&gt;</span> <span class="nf">do_project</span><span class="p">(),</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>we can now just pack everything into a <code class="language-plaintext highlighter-rouge">Projected</code> struct</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">Projected</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">screen</span><span class="p">:</span> <span class="n">Vec2</span><span class="p">,</span>       <span class="c1">// pixel center (sx, sy)</span>
    <span class="k">pub</span> <span class="n">depth</span><span class="p">:</span> <span class="nb">f32</span><span class="p">,</span>         <span class="c1">// zc (positive in front)</span>
    <span class="k">pub</span> <span class="n">cov2d_inv</span><span class="p">:</span> <span class="n">Mat2</span><span class="p">,</span>    <span class="c1">// inverse 2D covariance</span>
    <span class="k">pub</span> <span class="n">bbox</span><span class="p">:</span> <span class="p">[</span><span class="nb">i32</span><span class="p">;</span> <span class="mi">4</span><span class="p">],</span>     <span class="c1">// inclusive: x0, y0, x1, y1</span>
    <span class="k">pub</span> <span class="n">color</span><span class="p">:</span> <span class="n">Vec3</span><span class="p">,</span>        <span class="c1">// RGB</span>
    <span class="k">pub</span> <span class="n">opacity</span><span class="p">:</span> <span class="nb">f32</span><span class="p">,</span>       <span class="c1">// [0, 1]</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="step-3-depth-sort">step 3: depth sort</h2>

<p>For alpha compositing we use <a href="https://en.wikipedia.org/wiki/Painter's_algorithm">painter’s algorithm in reverse</a>. this means we need the projected splats sorted by depth before compositing. the order matters here, if a close splat occludes a far one, it must be composited first so it has more weight in the blending process. because all depths are positive (we culled anything behind the camera), so the bit pattern of an <code class="language-plaintext highlighter-rouge">f32</code> preserves float ordering when reinterpreted as a <code class="language-plaintext highlighter-rouge">u32</code>. this lets us use <code class="language-plaintext highlighter-rouge">depth.to_bits()</code> as a sort key</p>

<p>for the actual sorting, we use a simple 2-pass 16-bit radix sort. this is faster than comparison-based sorting (like Rust’s <code class="language-plaintext highlighter-rouge">sort_unstable_by_key</code>) for our input sizes (~100k–200k elements) and it runs in O(n) time with small constants. 2 passes of 16 bits seemed to have worked better instead of 4 passes of 8 bits, fewer passes means fewer data traversals and also the 65536-entry histograms (256KB each) fit comfortably in my laptops L2 cache. at 200k splats this is consistently 2 times faster than Rust’s comparison sort.</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">sort_by_depth</span><span class="p">(</span><span class="n">projected</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="p">[</span><span class="n">Projected</span><span class="p">],</span> <span class="n">scratch</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">ScratchBuffers</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">n</span> <span class="o">=</span> <span class="n">projected</span><span class="nf">.len</span><span class="p">();</span>
    <span class="k">if</span> <span class="n">n</span> <span class="o">&lt;=</span> <span class="mi">1</span> <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">scratch</span><span class="py">.sort_aux</span><span class="nf">.clear</span><span class="p">();</span>
    <span class="n">scratch</span><span class="py">.sort_aux</span><span class="nf">.reserve</span><span class="p">(</span><span class="n">n</span><span class="nf">.saturating_sub</span><span class="p">(</span><span class="n">scratch</span><span class="py">.sort_aux</span><span class="nf">.capacity</span><span class="p">()));</span>
    <span class="k">unsafe</span> <span class="p">{</span> <span class="n">scratch</span><span class="py">.sort_aux</span><span class="nf">.set_len</span><span class="p">(</span><span class="n">n</span><span class="p">);</span> <span class="p">}</span>
    <span class="k">let</span> <span class="n">aux</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">scratch</span><span class="py">.sort_aux</span><span class="p">;</span>

    <span class="c1">// Both histograms can be computed in a single pass over the keys.</span>
    <span class="c1">// Stack-allocated to avoid heap alloc.</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">counts_lo</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0u32</span><span class="p">;</span> <span class="mi">65536</span><span class="p">];</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">counts_hi</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0u32</span><span class="p">;</span> <span class="mi">65536</span><span class="p">];</span>
    <span class="k">for</span> <span class="n">p</span> <span class="k">in</span> <span class="n">projected</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">k</span> <span class="o">=</span> <span class="n">p</span><span class="py">.depth</span><span class="nf">.to_bits</span><span class="p">();</span>
        <span class="n">counts_lo</span><span class="p">[(</span><span class="n">k</span> <span class="o">&amp;</span> <span class="mi">0xFFFF</span><span class="p">)</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="n">counts_hi</span><span class="p">[(</span><span class="n">k</span> <span class="o">&gt;&gt;</span> <span class="mi">16</span><span class="p">)</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Pass 1: sort by low 16 bits</span>
    <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">offsets</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0u32</span><span class="p">;</span> <span class="mi">65536</span><span class="p">];</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">sum</span> <span class="o">=</span> <span class="mi">0u32</span><span class="p">;</span>
        <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="mi">65536</span> <span class="p">{</span>
            <span class="n">offsets</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">sum</span><span class="p">;</span>
            <span class="n">sum</span> <span class="o">+=</span> <span class="n">counts_lo</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
        <span class="p">}</span>
        <span class="k">for</span> <span class="n">p</span> <span class="k">in</span> <span class="n">projected</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">bucket</span> <span class="o">=</span> <span class="p">(</span><span class="n">p</span><span class="py">.depth</span><span class="nf">.to_bits</span><span class="p">()</span> <span class="o">&amp;</span> <span class="mi">0xFFFF</span><span class="p">)</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">offsets</span><span class="p">[</span><span class="n">bucket</span><span class="p">]</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="n">offsets</span><span class="p">[</span><span class="n">bucket</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
            <span class="n">aux</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="o">=</span> <span class="o">*</span><span class="n">p</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c1">// Pass 2: sort by high 16 bits</span>
    <span class="p">{</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">offsets</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0u32</span><span class="p">;</span> <span class="mi">65536</span><span class="p">];</span>
        <span class="k">let</span> <span class="k">mut</span> <span class="n">sum</span> <span class="o">=</span> <span class="mi">0u32</span><span class="p">;</span>
        <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="mi">65536</span> <span class="p">{</span>
            <span class="n">offsets</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">sum</span><span class="p">;</span>
            <span class="n">sum</span> <span class="o">+=</span> <span class="n">counts_hi</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
        <span class="p">}</span>
        <span class="k">for</span> <span class="n">p</span> <span class="k">in</span> <span class="n">aux</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">bucket</span> <span class="o">=</span> <span class="p">(</span><span class="n">p</span><span class="py">.depth</span><span class="nf">.to_bits</span><span class="p">()</span> <span class="o">&gt;&gt;</span> <span class="mi">16</span><span class="p">)</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">offsets</span><span class="p">[</span><span class="n">bucket</span><span class="p">]</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="n">offsets</span><span class="p">[</span><span class="n">bucket</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
            <span class="n">projected</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="o">=</span> <span class="o">*</span><span class="n">p</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<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">sort_by_depth_parallel</span><span class="p">(</span>
    <span class="n">projected</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="p">[</span><span class="n">Projected</span><span class="p">],</span>
    <span class="n">scratch</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">ScratchBuffers</span><span class="p">,</span>
    <span class="n">pool</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">Option</span><span class="o">&lt;</span><span class="nn">rayon</span><span class="p">::</span><span class="n">ThreadPool</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">projected</span><span class="nf">.len</span><span class="p">()</span> <span class="o">&lt;</span> <span class="mi">50_000</span> <span class="p">||</span> <span class="n">pool</span><span class="nf">.is_none</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">sort_by_depth</span><span class="p">(</span><span class="n">projected</span><span class="p">,</span> <span class="n">scratch</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">pool</span><span class="nf">.as_ref</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">()</span><span class="nf">.install</span><span class="p">(||</span> <span class="p">{</span>
        <span class="n">projected</span><span class="nf">.par_sort_unstable_by_key</span><span class="p">(|</span><span class="n">p</span><span class="p">|</span> <span class="n">p</span><span class="py">.depth</span><span class="nf">.to_bits</span><span class="p">());</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="step-4-tile-binning">step 4: tile binning</h2>

<p>we now have a depth-sorted list of projected splats. the naive approach to compositing would be: for each splat, iterate every pixel in its bounding box and accumulate color. this works but it is cache-hostile because different splats touch overlapping pixel regions in unpredictable order.</p>

<p>the original 3DGS implementation divides the image into tiles (16×16 pixel blocks) and builds an index that tells each tile exactly which splats overlap it. then each tile composites only its own splats, in order, touching only its own pixels</p>

<p>the binning uses a count-then-scatter two-pass approach. in pass 1 for each splat, count how many tiles its bbox touches then build a per-tile count array. in pass 2, prefix-sum the counts into offsets (so <code class="language-plaintext highlighter-rouge">offsets[i]</code> = start of tile i’s bucket). then scatter each splat’s index into its tile buckets using a cursor that advances. now because we iterate splats in depth-sorted order, each tile’s bucket ends up automatically depth-sorted so no additional per-tile sort needed</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">bin_splats</span><span class="p">(</span>
    <span class="n">projected</span><span class="p">:</span> <span class="o">&amp;</span><span class="p">[</span><span class="n">Projected</span><span class="p">],</span>
    <span class="n">width</span><span class="p">:</span> <span class="nb">u32</span><span class="p">,</span>
    <span class="n">height</span><span class="p">:</span> <span class="nb">u32</span><span class="p">,</span>
    <span class="n">bins</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">TileBins</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">num_tiles_x</span> <span class="o">=</span> <span class="p">((</span><span class="n">width</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">)</span> <span class="o">+</span> <span class="n">TILE_W</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="n">TILE_W</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">num_tiles_y</span> <span class="o">=</span> <span class="p">((</span><span class="n">height</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">)</span> <span class="o">+</span> <span class="n">TILE_H</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="n">TILE_H</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">num_tiles</span> <span class="o">=</span> <span class="p">(</span><span class="n">num_tiles_x</span> <span class="o">*</span> <span class="n">num_tiles_y</span><span class="p">)</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>

    <span class="c1">// offsets is used first as a count array, then prefix-summed in place.</span>
    <span class="n">bins</span><span class="py">.offsets</span><span class="nf">.clear</span><span class="p">();</span>
    <span class="n">bins</span><span class="py">.offsets</span><span class="nf">.resize</span><span class="p">(</span><span class="n">num_tiles</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>

    <span class="c1">// Pass 1: count tile touches per splat.</span>
    <span class="k">for</span> <span class="n">p</span> <span class="k">in</span> <span class="n">projected</span> <span class="p">{</span>
        <span class="k">let</span> <span class="p">[</span><span class="n">x0</span><span class="p">,</span> <span class="n">y0</span><span class="p">,</span> <span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span><span class="py">.bbox</span><span class="p">;</span>
        <span class="k">let</span> <span class="n">tx0</span> <span class="o">=</span> <span class="p">(</span><span class="n">x0</span> <span class="o">/</span> <span class="n">TILE_W</span><span class="p">)</span><span class="nf">.max</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">ty0</span> <span class="o">=</span> <span class="p">(</span><span class="n">y0</span> <span class="o">/</span> <span class="n">TILE_H</span><span class="p">)</span><span class="nf">.max</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">tx1</span> <span class="o">=</span> <span class="p">(</span><span class="n">x1</span> <span class="o">/</span> <span class="n">TILE_W</span><span class="p">)</span><span class="nf">.min</span><span class="p">(</span><span class="n">num_tiles_x</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">ty1</span> <span class="o">=</span> <span class="p">(</span><span class="n">y1</span> <span class="o">/</span> <span class="n">TILE_H</span><span class="p">)</span><span class="nf">.min</span><span class="p">(</span><span class="n">num_tiles_y</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
        <span class="k">if</span> <span class="n">tx0</span> <span class="o">&gt;</span> <span class="n">tx1</span> <span class="p">||</span> <span class="n">ty0</span> <span class="o">&gt;</span> <span class="n">ty1</span> <span class="p">{</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">for</span> <span class="n">ty</span> <span class="k">in</span> <span class="n">ty0</span><span class="o">..=</span><span class="n">ty1</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">row</span> <span class="o">=</span> <span class="p">(</span><span class="n">ty</span> <span class="o">*</span> <span class="n">num_tiles_x</span><span class="p">)</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="k">for</span> <span class="n">tx</span> <span class="k">in</span> <span class="n">tx0</span><span class="o">..=</span><span class="n">tx1</span> <span class="p">{</span>
                <span class="n">bins</span><span class="py">.offsets</span><span class="p">[</span><span class="n">row</span> <span class="o">+</span> <span class="n">tx</span> <span class="k">as</span> <span class="nb">usize</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="c1">// Prefix sum: offsets[i] = start of bucket i.</span>
    <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">..=</span><span class="n">num_tiles</span> <span class="p">{</span>
        <span class="n">bins</span><span class="py">.offsets</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+=</span> <span class="n">bins</span><span class="py">.offsets</span><span class="p">[</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">];</span>
    <span class="p">}</span>
    <span class="k">let</span> <span class="n">total</span> <span class="o">=</span> <span class="n">bins</span><span class="py">.offsets</span><span class="p">[</span><span class="n">num_tiles</span><span class="p">]</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>

    <span class="n">bins</span><span class="py">.splat_indices</span><span class="nf">.clear</span><span class="p">();</span>
    <span class="n">bins</span><span class="py">.splat_indices</span><span class="nf">.resize</span><span class="p">(</span><span class="n">total</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>

    <span class="c1">// Cursor starts at each bucket's begin and advances as we scatter.</span>
    <span class="n">bins</span><span class="py">.cursor</span><span class="nf">.clear</span><span class="p">();</span>
    <span class="n">bins</span><span class="py">.cursor</span><span class="nf">.extend_from_slice</span><span class="p">(</span><span class="o">&amp;</span><span class="n">bins</span><span class="py">.offsets</span><span class="p">[</span><span class="o">..</span><span class="n">num_tiles</span><span class="p">]);</span>

    <span class="c1">// Pass 2: scatter splat indices into their tile buckets.</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">idx</span><span class="p">,</span> <span class="n">p</span><span class="p">)</span> <span class="k">in</span> <span class="n">projected</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.enumerate</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="p">[</span><span class="n">x0</span><span class="p">,</span> <span class="n">y0</span><span class="p">,</span> <span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span><span class="py">.bbox</span><span class="p">;</span>
        <span class="k">let</span> <span class="n">tx0</span> <span class="o">=</span> <span class="p">(</span><span class="n">x0</span> <span class="o">/</span> <span class="n">TILE_W</span><span class="p">)</span><span class="nf">.max</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">ty0</span> <span class="o">=</span> <span class="p">(</span><span class="n">y0</span> <span class="o">/</span> <span class="n">TILE_H</span><span class="p">)</span><span class="nf">.max</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">tx1</span> <span class="o">=</span> <span class="p">(</span><span class="n">x1</span> <span class="o">/</span> <span class="n">TILE_W</span><span class="p">)</span><span class="nf">.min</span><span class="p">(</span><span class="n">num_tiles_x</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">ty1</span> <span class="o">=</span> <span class="p">(</span><span class="n">y1</span> <span class="o">/</span> <span class="n">TILE_H</span><span class="p">)</span><span class="nf">.min</span><span class="p">(</span><span class="n">num_tiles_y</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
        <span class="k">if</span> <span class="n">tx0</span> <span class="o">&gt;</span> <span class="n">tx1</span> <span class="p">||</span> <span class="n">ty0</span> <span class="o">&gt;</span> <span class="n">ty1</span> <span class="p">{</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">for</span> <span class="n">ty</span> <span class="k">in</span> <span class="n">ty0</span><span class="o">..=</span><span class="n">ty1</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">row</span> <span class="o">=</span> <span class="p">(</span><span class="n">ty</span> <span class="o">*</span> <span class="n">num_tiles_x</span><span class="p">)</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="k">for</span> <span class="n">tx</span> <span class="k">in</span> <span class="n">tx0</span><span class="o">..=</span><span class="n">tx1</span> <span class="p">{</span>
                <span class="k">let</span> <span class="n">tile</span> <span class="o">=</span> <span class="n">row</span> <span class="o">+</span> <span class="n">tx</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
                <span class="k">let</span> <span class="n">pos</span> <span class="o">=</span> <span class="n">bins</span><span class="py">.cursor</span><span class="p">[</span><span class="n">tile</span><span class="p">]</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
                <span class="n">bins</span><span class="py">.splat_indices</span><span class="p">[</span><span class="n">pos</span><span class="p">]</span> <span class="o">=</span> <span class="n">idx</span> <span class="k">as</span> <span class="nb">u32</span><span class="p">;</span>
                <span class="n">bins</span><span class="py">.cursor</span><span class="p">[</span><span class="n">tile</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>let me show what the data structure looks like for a small example. say we have 4 splats (A, B, C, D) then after depth sorting we will get something like this</p>

<p><img src="/assets/images/posts/Pasted image 20260412233641.png" alt="Pasted image 20260412233641.png" /></p>

<h2 id="step-5-alpha-compositing">step 5: alpha compositing</h2>

<p>now for each tile, we iterate its splats front-to-back, and for each pixel in the splat’s bounding box (clipped to the tile), we evaluate the 2D Gaussian and blend the color.</p>

<p>for a pixel at position $(px, py)$ and a splat centered at $(sx, sy)$, the displacement is $d = (px - sx, py - sy)$. the Gaussian exponent is:</p>

\[\text{power} = -\tfrac{1}{2} \cdot \mathbf{d}^T \cdot \Sigma^{-1} \cdot \mathbf{d}\]

<p>expanding for a 2×2 symmetric inverse covariance with entries $(a, b; b, d)$:</p>

\[\text{power} = -\tfrac{1}{2} \cdot (a \cdot dx^2 + 2b \cdot dx \cdot dy + d \cdot dy^2)\]

<p>the alpha for this splat at this pixel is:</p>

\[\alpha = \text{opacity} \cdot \exp(\text{power})\]

<p>we now composite front-to-back. the framebuffer stores <code class="language-plaintext highlighter-rouge">(rgb_accum, alpha_accum)</code> per pixel, both starting at zero. for each splat, the contribution is:</p>

<p>\(T = 1 - \alpha_{accum}\)
\(\text{contrib} = T \cdot \alpha\)
\(\text{rgb}_{accum} \mathrel{+}= \text{contrib} \cdot \text{color}\)
\(\alpha_{accum} \mathrel{+}= \text{contrib}\)</p>

<p>here $T$ is the transmittance i.e how much light can still pass through. once <code class="language-plaintext highlighter-rouge">alpha_accum</code> reaches the saturation threshold (0.999), the pixel is considered opaque and we skip further splats on it.</p>

<p>the inner pixel loop is the hottest code in the entire rasterizer. to make it as fast as possible, we hoist row-constant terms outside the inner (column) loop. for a fixed row $p_y$, $d_y = p_y - s_y$ is constant, so we precompute everything else and then the inner loop becomes a simple quadratic in dx with all coefficients precomputed</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">composite_splat</span><span class="p">(</span><span class="n">p</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">Projected</span><span class="p">,</span> <span class="n">fb</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="p">[(</span><span class="n">Vec3</span><span class="p">,</span> <span class="nb">f32</span><span class="p">)],</span> <span class="n">w</span><span class="p">:</span> <span class="nb">usize</span><span class="p">,</span> <span class="n">params</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">RenderParams</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="p">[</span><span class="n">x0</span><span class="p">,</span> <span class="n">y0</span><span class="p">,</span> <span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span><span class="py">.bbox</span><span class="p">;</span>

    <span class="c1">// Extract the inverse covariance matrix elements.</span>
    <span class="k">let</span> <span class="n">a</span> <span class="o">=</span> <span class="n">p</span><span class="py">.cov2d_inv.x_axis.x</span><span class="p">;</span> <span class="c1">// (0,0)</span>
    <span class="k">let</span> <span class="n">b</span> <span class="o">=</span> <span class="n">p</span><span class="py">.cov2d_inv.x_axis.y</span><span class="p">;</span> <span class="c1">// (0,1) = (1,0) since symmetric</span>
    <span class="k">let</span> <span class="n">d</span> <span class="o">=</span> <span class="n">p</span><span class="py">.cov2d_inv.y_axis.y</span><span class="p">;</span> <span class="c1">// (1,1)</span>

    <span class="c1">// Coefficients for the decomposed quadratic.</span>
    <span class="k">let</span> <span class="n">dx_coeff</span> <span class="o">=</span> <span class="o">-</span><span class="mf">0.5</span> <span class="o">*</span> <span class="n">a</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">dy_coeff</span> <span class="o">=</span> <span class="o">-</span><span class="mf">0.5</span> <span class="o">*</span> <span class="n">d</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">cross_coeff</span> <span class="o">=</span> <span class="o">-</span><span class="n">b</span><span class="p">;</span> <span class="c1">// -0.5 * 2 * b</span>

    <span class="k">let</span> <span class="n">saturation</span> <span class="o">=</span> <span class="n">params</span><span class="py">.saturation</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">alpha_threshold</span> <span class="o">=</span> <span class="n">params</span><span class="py">.alpha_threshold</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">opacity</span> <span class="o">=</span> <span class="n">p</span><span class="py">.opacity</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">color</span> <span class="o">=</span> <span class="n">p</span><span class="py">.color</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">sx</span> <span class="o">=</span> <span class="n">p</span><span class="py">.screen.x</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">sy</span> <span class="o">=</span> <span class="n">p</span><span class="py">.screen.y</span><span class="p">;</span>

    <span class="k">for</span> <span class="n">py</span> <span class="k">in</span> <span class="n">y0</span><span class="o">..=</span><span class="n">y1</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">dy</span> <span class="o">=</span> <span class="n">py</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">-</span> <span class="n">sy</span><span class="p">;</span>
        <span class="k">let</span> <span class="n">row_base</span> <span class="o">=</span> <span class="n">dy_coeff</span> <span class="o">*</span> <span class="n">dy</span> <span class="o">*</span> <span class="n">dy</span><span class="p">;</span>
        <span class="k">let</span> <span class="n">row_slope</span> <span class="o">=</span> <span class="n">cross_coeff</span> <span class="o">*</span> <span class="n">dy</span><span class="p">;</span>
        <span class="k">let</span> <span class="n">row_offset</span> <span class="o">=</span> <span class="n">py</span> <span class="k">as</span> <span class="nb">usize</span> <span class="o">*</span> <span class="n">w</span><span class="p">;</span>

        <span class="k">for</span> <span class="n">px</span> <span class="k">in</span> <span class="n">x0</span><span class="o">..=</span><span class="n">x1</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">idx</span> <span class="o">=</span> <span class="n">row_offset</span> <span class="o">+</span> <span class="n">px</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">cell</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">fb</span><span class="p">[</span><span class="n">idx</span><span class="p">];</span>
            <span class="k">if</span> <span class="n">cell</span><span class="na">.1</span> <span class="o">&gt;=</span> <span class="n">saturation</span> <span class="p">{</span>
                <span class="k">continue</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">let</span> <span class="n">dx</span> <span class="o">=</span> <span class="n">px</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">-</span> <span class="n">sx</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">power</span> <span class="o">=</span> <span class="n">dx_coeff</span> <span class="o">*</span> <span class="n">dx</span> <span class="o">*</span> <span class="n">dx</span> <span class="o">+</span> <span class="n">row_slope</span> <span class="o">*</span> <span class="n">dx</span> <span class="o">+</span> <span class="n">row_base</span><span class="p">;</span>
            <span class="k">if</span> <span class="n">power</span> <span class="o">&gt;</span> <span class="mf">0.0</span> <span class="p">{</span>
                <span class="k">continue</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">let</span> <span class="n">alpha</span> <span class="o">=</span> <span class="p">(</span><span class="n">opacity</span> <span class="o">*</span> <span class="nf">fast_exp</span><span class="p">(</span><span class="n">power</span><span class="p">))</span><span class="nf">.min</span><span class="p">(</span><span class="mf">0.999</span><span class="p">);</span>
            <span class="k">if</span> <span class="n">alpha</span> <span class="o">&lt;</span> <span class="n">alpha_threshold</span> <span class="p">{</span>
                <span class="k">continue</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">let</span> <span class="n">t</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">-</span> <span class="n">cell</span><span class="na">.1</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">contrib</span> <span class="o">=</span> <span class="n">t</span> <span class="o">*</span> <span class="n">alpha</span><span class="p">;</span>
            <span class="n">cell</span><span class="na">.0</span> <span class="o">+=</span> <span class="n">contrib</span> <span class="o">*</span> <span class="n">color</span><span class="p">;</span>
            <span class="n">cell</span><span class="na">.1</span> <span class="o">+=</span> <span class="n">contrib</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="tiled-parallel-compositing">tiled parallel compositing</h3>

<p>now because each tile owns a disjoint pixel sets so we can composite all tiles in parallel with zero synchronization, no atomics and no locks, this becomes very effective on CPU</p>

<p>we use rayon’s <code class="language-plaintext highlighter-rouge">par_iter</code> over tile indices, and raw-pointer writes to give each tile direct access to its pixel rectangle without violating Rust’s <code class="language-plaintext highlighter-rouge">&amp;mut</code> aliasing rules:</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">composite_tiled</span><span class="p">(</span>
    <span class="n">projected</span><span class="p">:</span> <span class="o">&amp;</span><span class="p">[</span><span class="n">Projected</span><span class="p">],</span>
    <span class="n">bins</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">TileBins</span><span class="p">,</span>
    <span class="n">fb</span><span class="p">:</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="p">[(</span><span class="n">Vec3</span><span class="p">,</span> <span class="nb">f32</span><span class="p">)],</span>
    <span class="n">width</span><span class="p">:</span> <span class="nb">u32</span><span class="p">,</span>
    <span class="n">height</span><span class="p">:</span> <span class="nb">u32</span><span class="p">,</span>
    <span class="n">params</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">RenderParams</span><span class="p">,</span>
    <span class="n">pool</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">Option</span><span class="o">&lt;</span><span class="nn">rayon</span><span class="p">::</span><span class="n">ThreadPool</span><span class="o">&gt;</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">w</span> <span class="o">=</span> <span class="n">width</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">h_i</span> <span class="o">=</span> <span class="n">height</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">w_i</span> <span class="o">=</span> <span class="n">width</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">num_tiles_x</span> <span class="o">=</span> <span class="n">bins</span><span class="py">.num_tiles_x</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">num_tiles</span> <span class="o">=</span> <span class="n">bins</span><span class="nf">.num_tiles</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">fb_ptr</span> <span class="o">=</span> <span class="nf">FbPtr</span><span class="p">(</span><span class="n">fb</span><span class="nf">.as_mut_ptr</span><span class="p">());</span>

    <span class="k">let</span> <span class="n">do_composite</span> <span class="o">=</span> <span class="p">||</span> <span class="p">{</span>
        <span class="p">(</span><span class="mi">0</span><span class="o">..</span><span class="n">num_tiles</span><span class="p">)</span><span class="nf">.into_par_iter</span><span class="p">()</span><span class="nf">.for_each</span><span class="p">(</span><span class="k">move</span> <span class="p">|</span><span class="n">tile_idx</span><span class="p">|</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">fbp</span> <span class="o">=</span> <span class="n">fb_ptr</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">tile_x</span> <span class="o">=</span> <span class="p">(</span><span class="n">tile_idx</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">)</span> <span class="o">%</span> <span class="n">num_tiles_x</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">tile_y</span> <span class="o">=</span> <span class="p">(</span><span class="n">tile_idx</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">)</span> <span class="o">/</span> <span class="n">num_tiles_x</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">px0</span> <span class="o">=</span> <span class="n">tile_x</span> <span class="o">*</span> <span class="n">TILE_W</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">py0</span> <span class="o">=</span> <span class="n">tile_y</span> <span class="o">*</span> <span class="n">TILE_H</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">px1</span> <span class="o">=</span> <span class="p">(</span><span class="n">px0</span> <span class="o">+</span> <span class="n">TILE_W</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span><span class="nf">.min</span><span class="p">(</span><span class="n">w_i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
            <span class="k">let</span> <span class="n">py1</span> <span class="o">=</span> <span class="p">(</span><span class="n">py0</span> <span class="o">+</span> <span class="n">TILE_H</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span><span class="nf">.min</span><span class="p">(</span><span class="n">h_i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
            <span class="k">if</span> <span class="n">px0</span> <span class="o">&gt;</span> <span class="n">px1</span> <span class="p">||</span> <span class="n">py0</span> <span class="o">&gt;</span> <span class="n">py1</span> <span class="p">{</span>
                <span class="k">return</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="k">let</span> <span class="n">start</span> <span class="o">=</span> <span class="n">bins</span><span class="py">.offsets</span><span class="p">[</span><span class="n">tile_idx</span><span class="p">]</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">end</span> <span class="o">=</span> <span class="n">bins</span><span class="py">.offsets</span><span class="p">[</span><span class="n">tile_idx</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">;</span>
            <span class="k">if</span> <span class="n">start</span> <span class="o">==</span> <span class="n">end</span> <span class="p">{</span>
                <span class="k">return</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">let</span> <span class="n">splat_ids</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">bins</span><span class="py">.splat_indices</span><span class="p">[</span><span class="n">start</span><span class="o">..</span><span class="n">end</span><span class="p">];</span>

            <span class="k">for</span> <span class="o">&amp;</span><span class="n">sid</span> <span class="k">in</span> <span class="n">splat_ids</span> <span class="p">{</span>
                <span class="k">let</span> <span class="n">p</span> <span class="o">=</span> <span class="k">unsafe</span> <span class="p">{</span> <span class="n">projected</span><span class="nf">.get_unchecked</span><span class="p">(</span><span class="n">sid</span> <span class="k">as</span> <span class="nb">usize</span><span class="p">)</span> <span class="p">};</span>
                <span class="k">let</span> <span class="p">[</span><span class="n">bx0</span><span class="p">,</span> <span class="n">by0</span><span class="p">,</span> <span class="n">bx1</span><span class="p">,</span> <span class="n">by1</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span><span class="py">.bbox</span><span class="p">;</span>
                <span class="k">let</span> <span class="n">x0</span> <span class="o">=</span> <span class="n">bx0</span><span class="nf">.max</span><span class="p">(</span><span class="n">px0</span><span class="p">);</span>
                <span class="k">let</span> <span class="n">y0</span> <span class="o">=</span> <span class="n">by0</span><span class="nf">.max</span><span class="p">(</span><span class="n">py0</span><span class="p">);</span>
                <span class="k">let</span> <span class="n">x1</span> <span class="o">=</span> <span class="n">bx1</span><span class="nf">.min</span><span class="p">(</span><span class="n">px1</span><span class="p">);</span>
                <span class="k">let</span> <span class="n">y1</span> <span class="o">=</span> <span class="n">by1</span><span class="nf">.min</span><span class="p">(</span><span class="n">py1</span><span class="p">);</span>
                <span class="k">if</span> <span class="n">x0</span> <span class="o">&gt;</span> <span class="n">x1</span> <span class="p">||</span> <span class="n">y0</span> <span class="o">&gt;</span> <span class="n">y1</span> <span class="p">{</span>
                    <span class="k">continue</span><span class="p">;</span>
                <span class="p">}</span>
                <span class="k">unsafe</span> <span class="p">{</span>
                    <span class="nf">composite_splat_region</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">fbp</span><span class="na">.0</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">x0</span><span class="p">,</span> <span class="n">y0</span><span class="p">,</span> <span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">,</span> <span class="n">params</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">});</span>
    <span class="p">};</span>

    <span class="k">match</span> <span class="n">pool</span><span class="nf">.as_ref</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">Some</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">p</span><span class="nf">.install</span><span class="p">(</span><span class="n">do_composite</span><span class="p">),</span>
        <span class="nb">None</span> <span class="k">=&gt;</span> <span class="nf">do_composite</span><span class="p">(),</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />
<p><strong>Note: the <code class="language-plaintext highlighter-rouge">FbPtr</code> trick</strong></p>

<p>Rust’s borrow checker does not let multiple threads hold <code class="language-plaintext highlighter-rouge">&amp;mut</code> references to the same buffer. but we know that each tile writes to a disjoint pixel rectangle the geometry guarantees it. so we wrap the raw pointer in a <code class="language-plaintext highlighter-rouge">Send + Sync</code> newtype and give each tile direct access:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="nf">FbPtr</span><span class="p">(</span><span class="o">*</span><span class="k">mut</span> <span class="p">(</span><span class="n">Vec3</span><span class="p">,</span> <span class="nb">f32</span><span class="p">));</span>
<span class="k">unsafe</span> <span class="k">impl</span> <span class="nb">Send</span> <span class="k">for</span> <span class="n">FbPtr</span> <span class="p">{}</span>
<span class="k">unsafe</span> <span class="k">impl</span> <span class="nb">Sync</span> <span class="k">for</span> <span class="n">FbPtr</span> <span class="p">{}</span>
</code></pre></div></div>

<p>this is the one place in the rasterizer where we need <code class="language-plaintext highlighter-rouge">unsafe</code>. the safety argument is spatial here and tile <code class="language-plaintext highlighter-rouge">(tx, ty)</code> only writes to pixels <code class="language-plaintext highlighter-rouge">[tx*16..(tx+1)*16, ty*16..(ty+1)*16]</code>, and no two tiles share the same <code class="language-plaintext highlighter-rouge">(tx, ty)</code>.</p>

<hr />

<h2 id="optimizations">optimizations</h2>

<p>Here are some additional optimizations that can be applied to make it fast for realtime use on CPU</p>

<h3 id="fast-approximate-exp">fast approximate exp</h3>

<p>the <code class="language-plaintext highlighter-rouge">exp()</code> function in the inner loop is by far the most expensive operation — it dominates everything else, if we don’t need 64-bit libm precision. the Schraudolph trick reinterprets a scaled float as an IEEE 754 bit pattern:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">fast_exp</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="nb">f32</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">f32</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">x</span> <span class="o">=</span> <span class="n">x</span><span class="nf">.max</span><span class="p">(</span><span class="o">-</span><span class="mf">87.0</span><span class="p">);</span>
    <span class="k">let</span> <span class="n">v</span> <span class="o">=</span> <span class="p">(</span><span class="mf">12102203.0f32</span> <span class="o">*</span> <span class="n">x</span> <span class="o">+</span> <span class="mf">1065353216.0</span><span class="p">)</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
    <span class="nn">f32</span><span class="p">::</span><span class="nf">from_bits</span><span class="p">(</span><span class="n">v</span> <span class="k">as</span> <span class="nb">u32</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="row-level-early-out">row-level early-out</h3>

<p>in the tiled compositor, before entering the inner pixel loop for a row, we check whether the peak of the Gaussian along that row is below the visibility threshold. the Gaussian exponent along a row is a concave quadratic in dx:</p>

\[\text{power}(dx) = \text{dx\_coeff} \cdot dx^2 + \text{row\_slope} \cdot dx + \text{row\_base}\]

<p>the peak of this quadratic (at the vertex) is:</p>

\[\text{row\_peak} = \text{row\_base} + \frac{\text{row\_slope}^2}{2 \cdot a}\]

<p>if even the peak is below <code class="language-plaintext highlighter-rouge">ln(alpha_threshold / opacity)</code>, the entire row is invisible and we skip it:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">row_peak_cutoff</span> <span class="o">=</span> <span class="p">(</span><span class="n">alpha_threshold</span> <span class="o">/</span> <span class="n">opacity</span><span class="p">)</span><span class="nf">.ln</span><span class="p">();</span>
<span class="k">let</span> <span class="n">inv_a</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">/</span> <span class="n">a</span><span class="p">;</span>

<span class="k">for</span> <span class="n">py</span> <span class="k">in</span> <span class="n">y0</span><span class="o">..=</span><span class="n">y1</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">dy</span> <span class="o">=</span> <span class="n">py</span> <span class="k">as</span> <span class="nb">f32</span> <span class="o">-</span> <span class="n">sy</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">row_base</span> <span class="o">=</span> <span class="n">dy_coeff</span> <span class="o">*</span> <span class="n">dy</span> <span class="o">*</span> <span class="n">dy</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">row_slope</span> <span class="o">=</span> <span class="n">cross_coeff</span> <span class="o">*</span> <span class="n">dy</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">row_peak</span> <span class="o">=</span> <span class="n">row_base</span> <span class="o">+</span> <span class="mf">0.5</span> <span class="o">*</span> <span class="n">row_slope</span> <span class="o">*</span> <span class="n">row_slope</span> <span class="o">*</span> <span class="n">inv_a</span><span class="p">;</span>
    <span class="k">if</span> <span class="n">row_peak</span> <span class="o">&lt;</span> <span class="n">row_peak_cutoff</span> <span class="p">{</span>
        <span class="k">continue</span><span class="p">;</span>    <span class="c1">// entire row is below threshold, skip it</span>
    <span class="p">}</span>
    <span class="c1">// ... inner pixel loop ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>a subtlety: the peak is at the vertex of the quadratic, not at $dx = 0$. using <code class="language-plaintext highlighter-rouge">row_base</code> alone (which is the value at $dx = 0$) would miss peaks that sit off-center and could incorrectly skip visible rows for elongated, tilted Gaussians.</p>

<h3 id="scratch-buffer-reuse">scratch buffer reuse</h3>

<p>the radix sort needs an auxiliary buffer the same size as the input. allocating this every frame would be wasteful. instead we allocate it once and reuse it across frames via a <code class="language-plaintext highlighter-rouge">ScratchBuffers</code> struct:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">pub</span> <span class="k">struct</span> <span class="n">ScratchBuffers</span> <span class="p">{</span>
    <span class="k">pub</span> <span class="n">sort_aux</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Projected</span><span class="o">&gt;</span><span class="p">,</span>
    <span class="k">pub</span> <span class="n">tiles</span><span class="p">:</span> <span class="n">TileBins</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>the <code class="language-plaintext highlighter-rouge">sort_aux</code> vec grows to fit the first frame and never shrinks this enables us to do zero per-frame heap allocation once the system has warmed up.</p>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="splatting" />
      
        <category term="rendering" />
      
        <category term="3d" />
      
        <category term="reconstruction" />
      

      
      
        <summary type="html"><![CDATA[Gaussian Splatting is a fascinating scene reconstruction technique introduced by INRIA and last year I had a lot of fun tinkering with it while on my semex. I recently discovered some of my notes related to it and decided to digitize it this weekend, along the way I reimplemented the forward rasterization pass in rust and decided it would be fun to write a tutorial explaining gaussian splatting to everyone, so here it is what is a gaussian splat?]]></summary>
      
    </entry>
  
    <entry>
      

      <title type="html">What Happens When You SFT a Human on an LLM</title>
      <link href="https://darshanmakwana412.github.io/2026/04/sft-a-human-on-an-llm/" rel="alternate" type="text/html" title="What Happens When You SFT a Human on an LLM" />
      <published>2026-04-08T00:00:00+05:30</published>
      <updated>2026-04-08T00:00:00+05:30</updated>
      <id>https://darshanmakwana412.github.io/2026/04/sft-a-human-on-an-llm</id>
      <content type="html" xml:base="https://darshanmakwana412.github.io/2026/04/sft-a-human-on-an-llm/"><![CDATA[<p>We have spent the last few years training LLMs on human conversation data with the explicit goal of making them sound like us, but we have also been training ourselves on them in return. I have spent a lot of hours talking to claude over the past couple of months, my juniors grew up doing their assignments with gpt, and there are kids in undergrad right now whose first real intellectual sparring partner was a language model instead of another human. This is not a one way street. The gradients are running in both directions, they are doing SGD on us and we are doing something like SFT on them, and the effect is small enough per interaction that you don’t notice it until one day you start hearing “Yes absolutely I can help you with that” from freinds and you realize you have never heard them say that phrase in your life. There is a <a href="https://news.ycombinator.com/item?id=47673541">USC study</a> that measures this drift in spoken youtube videos after chatgpt, words like delve and meticulous and underscore spiking in frequency in actual human speech, not copy pasted text but mouths, and the comments under the post are full of people confessing that they now say “you’re absolutely right” out loud in real conversations, which is maybe the most claude coded sentence a human being can utter and also the kind of thing you would only notice if someone else said it to you first</p>

<p>The usual response I get when I bring this up with friends is that tools have always shaped the people using them, the printing press did it, radio did it, standardized spelling did it, twitter did it, so what is the big deal about this one. And I think the big deal is that the loop is closed now and the loop is short. Previous tools did not update, the printing press in 1500 was the same printing press in 1600, twitter is roughly the same twitter it was five years ago. But claude 4.6 is not claude 3.5 and it will not be claude 5 in six months, and every retraining cycle grounds on an updated human interaction, and the 2026 internet is written by humans who have spent the last two years being subtly finetuned by the 2024 version of the model, which was itself trained on humans that had been talking to the 2022 version. The model distribution and the human distribution are idk sort of converging, and maybe at one point the reference you would use to even measure the drift, stops existing because nobody is untouched anymore. I don’t know if this converges into something stable or diverges into something strange but either way the concept of “what unassisted human thinking sounds like” quietly diminishes, and I don’t think we are going to notice the day that happens</p>

<p>I heard the other day that people have stopped open sourcing work that they would have happily published a few years ago, specifically because they know it will end up inside a training corpus and the edge they got from writing it will evaporate. The funny second order thing here is that the models got good in the first place because people were generous in public, and the models being good is now the exact reason people are not being generous in public anymore. The commons is being fed into a thing that makes continuing to contribute to the commons feel like a bad trade, and I don’t see any version of that where the commons ends up richer</p>

<p>The other thought which I came across while writing this post is a quiet shift in what humans are actually for in this whole arrangement. I met a second year undergrad a couple weeks ago building a startup, smart kid, and he walked me through his workflow with real pride. He maintains this enormous knowledge base about his company and the agent acts as the CEO in his framing, and he is the hands, the agent drafts the email and he clicks send, the agent preps the talking points and he goes and says them to the investor, the agent reads the contract and tells him where to sign, and I could not figure out in the moment how to tell him that from where I was sitting he had just described himself as a peripheral, an io device with legs, a face and a bank account rented out to something else’s cognition. And the same shape is showing up in people who route feedback and delegation and even performance reviews through a model, so your manager is not writing your review and your response will also not be written by you, and the two models are going to have a little conversation about your career while the two humans pretend to have opinions in the meeting afterwards. The core limiting factor of most industries used to be intelligence, and the story of the last century is basically the story of that factor getting commoditized in stages, first physical labour, then access to knowledge, and now the parsing of that knowledge itself, which is the thing we used to call being smart. What you cannot hand off yet is the legal identity, the bank account, the face on the zoom call, the body that walks into the room, and so that is quietly what humans are being optimized into in this new stack. Not the thinker anymore, the thing that makes the agent’s output count as a real action in the real world, maybe humans will become just an extended version of tools to interact with the world</p>

<p>What if all this becomes the norm and we all shrug and get along with it. In an earlier post I wrote about <a href="/2026/03/claude-code-chronicles/">asking claude to apply for my visa</a> and I framed it as a fun anecdote because it genuinely was, but there is a darker version of that same story and it is the one where my juniors are firing up a claude and asking it to apply to 400 jobs a night while recruiters on the other side are firing up their own claude to screen the same 400 applications, and the only humans in the loop are the ones who wrote the prompts on either end. I already see it happening, my juniors are not landing interviews and I do not think they are less capable than I was at their age, the signal to noise ratio of the application pool has just collapsed because the noise is free to generate and the signal costs the same it always did. I do not want to live in a world where two models negotiate the hiring decision in the middle while the humans on both sides wait around to be told the outcome, and I am not sure if we can get a vote on this</p>]]></content>

      
      <author>
        <name>Darshan Makwana</name>
      </author>

      
        <category term="ai" />
      
        <category term="llm" />
      
        <category term="philosophy" />
      
        <category term="society" />
      

      
      
        <summary type="html"><![CDATA[We have spent the last few years training LLMs on human conversation data with the explicit goal of making them sound like us, but we have also been training ourselves on them in return. I have spent a lot of hours talking to claude over the past couple of months, my juniors grew up doing their assignments with gpt, and there are kids in undergrad right now whose first real intellectual sparring partner was a language model instead of another human. This is not a one way street. The gradients are running in both directions, they are doing SGD on us and we are doing something like SFT on them, and the effect is small enough per interaction that you don’t notice it until one day you start hearing “Yes absolutely I can help you with that” from freinds and you realize you have never heard them say that phrase in your life. There is a USC study that measures this drift in spoken youtube videos after chatgpt, words like delve and meticulous and underscore spiking in frequency in actual human speech, not copy pasted text but mouths, and the comments under the post are full of people confessing that they now say “you’re absolutely right” out loud in real conversations, which is maybe the most claude coded sentence a human being can utter and also the kind of thing you would only notice if someone else said it to you first]]></summary>
      
    </entry>
  
</feed>
