<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Chris Collins</title>
  <subtitle>Principal engineer. I build software, objects, and the occasional retro machine — and write up what I learn.</subtitle>
  <link href="https://geekychris.github.io/feed.xml" rel="self"/>
  <link href="https://geekychris.github.io/"/>
  <id>https://geekychris.github.io/</id>
  <updated>2026-10-30T00:00:00Z</updated>
  <author><name>Chris Collins</name></author>
  
  <entry>
    <title>From transistor to CPU</title>
    <link href="https://geekychris.github.io/lab/explorations/from-transistor-to-cpu/"/>
    <id>https://geekychris.github.io/lab/explorations/from-transistor-to-cpu/</id>
    <updated>2026-10-30T00:00:00Z</updated>
    <summary>Start with one transistor switching an LED, wire switches into gates, gates into an adder, feedback into memory, and memory plus a clock into a tiny CPU — then watch a real 6502 run a program you can read byte by byte.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A computer is the most complicated thing most of us own, and it is made almost entirely of one
very simple part, repeated. A 6502 — the processor in the Apple II, the Commodore 64, the BBC Micro
and the Atari 800 — has about 3,500 transistors. A modern laptop chip has tens of billions. In both,
each transistor does the same humble job: it is a switch that one wire can turn on and off.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This exploration climbs from that switch to a working processor in six steps, and at every step
there is a live circuit on the page for you to poke. Nothing is hidden behind a diagram: each level
is built, visibly, from the one below it. By the end you will have run a program on a CPU made of
gates you can see, and then on a real 6502, and the two programs will look remarkably alike.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-1-a-switch-that-a-wire-can-flip&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-1-a-switch-that-a-wire-can-flip&quot;&gt;&lt;/a&gt;Step 1: a switch that a wire can flip&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A transistor has three connections. In the bipolar kind used below, a small current into the &lt;strong&gt;base&lt;/strong&gt;
lets a much larger current flow from the &lt;strong&gt;collector&lt;/strong&gt; to the &lt;strong&gt;emitter&lt;/strong&gt;. Push a milliamp into the
base and the transistor turns hard on; take it away and it turns off. For analogue work we care
about the region in between, where the transistor amplifies. For digital work we deliberately
avoid it: we only ever drive the transistor fully on or fully off.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here is the simplest useful circuit there is. A pulse generator, &lt;code&gt;Vin&lt;/code&gt;, swings between 0 V and 5 V
every millisecond. It feeds the base of transistor Q1 through a 4.7 kΩ resistor. Q1&amp;#8217;s collector
pulls current through a red LED and a 220 Ω resistor from the 5 V supply.&lt;/p&gt;
&lt;/div&gt;
&lt;style&gt;.tool-embed{margin:1.6rem 0}.tool-embed .te-frame{position:relative;height:var(--te-h,520px);max-height:90vh;border:1px solid var(--line,#d9d6d0);border-radius:10px;overflow:hidden;background:var(--wash,#f3efe9)}@media (max-width:640px){.tool-embed .te-frame{height:min(var(--te-h,520px),75vh);border-radius:8px}}.tool-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent}.tool-embed .te-loading{position:absolute;inset:0;margin:0;display:grid;place-items:center;color:var(--faint,#8a9096);font:.9rem var(--sans,system-ui,sans-serif)}.tool-embed .te-ready .te-loading{visibility:hidden}.tool-embed figcaption{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.25rem 1rem;margin-top:.45rem;font-size:.86rem;color:var(--muted,#5f646b)}.tool-embed .te-open{white-space:nowrap;font-weight:600;margin-left:auto}.tool-embed-missing{padding:.8rem 1rem;border:1px dashed var(--line,#d9d6d0);border-radius:8px;color:var(--muted,#5f646b)}&lt;/style&gt;&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;circuit-sim&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Circuit simulator…&lt;/p&gt;&lt;iframe src=&quot;/tools/circuit-sim/?bare=1&amp;amp;embed=1&amp;amp;ex=ledswitch#tran&quot; title=&quot;Circuit simulator (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: scroll down inside the frame to the scope, then compare the two traces.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/circuit-sim/?ex=ledswitch#tran&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Scroll down inside the frame to the &lt;strong&gt;Transient (scope)&lt;/strong&gt; plots. The orange trace is the input, the
blue trace is &lt;code&gt;vc&lt;/code&gt;, the voltage at the collector — the point between the LED and the transistor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Look at the two traces side by side. When &lt;code&gt;vin&lt;/code&gt; is low, &lt;code&gt;vc&lt;/code&gt; sits high, at about 4.2 V. When
&lt;code&gt;vin&lt;/code&gt; goes high, &lt;code&gt;vc&lt;/code&gt; drops to almost nothing. The &lt;strong&gt;Currents&lt;/strong&gt; plot shows why: about 13.5 mA flows
through the LED only while the input is high.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select &lt;code&gt;Rb&lt;/code&gt; and change it to &lt;code&gt;100k&lt;/code&gt;, then press &lt;strong&gt;Run transient&lt;/strong&gt;. Less base current means the
transistor is no longer driven hard on, and &lt;code&gt;vc&lt;/code&gt; no longer falls cleanly to zero. That
in-between region is exactly what digital designers keep away from.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Put it back to &lt;code&gt;4k7&lt;/code&gt; and change &lt;code&gt;Rled&lt;/code&gt; to &lt;code&gt;1k&lt;/code&gt;. The LED current falls, but the shape of &lt;code&gt;vc&lt;/code&gt;
does not: on is still on, off is still off.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;What to notice: &lt;strong&gt;the output is the opposite of the input.&lt;/strong&gt; High in, low out; low in, high out. Read
&lt;code&gt;vc&lt;/code&gt; as the output and you have not just built a switch, you have built a logic gate — a NOT gate,
or &lt;strong&gt;inverter&lt;/strong&gt;. Everything else in this exploration is made of the same trick, applied over and over.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Real chips use MOSFETs rather than bipolar transistors, because a MOSFET&amp;#8217;s gate draws almost no
current, and they pair an n-type with a p-type transistor (CMOS) so that one is always off and the
circuit draws almost no power when it is not switching. The palette has an NMOS part if you want to
try building one. The idea is the same: a voltage on one wire decides whether another wire is
connected.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-2-from-switches-to-gates&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-2-from-switches-to-gates&quot;&gt;&lt;/a&gt;Step 2: from switches to gates&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Put two transistors in series, so current only flows when &lt;strong&gt;both&lt;/strong&gt; are on, and the output is pulled
low only when both inputs are high: that is a NAND gate. Put them in parallel, so either can pull
the output low, and you have NOR. Add an inverter after either and you get AND and OR. From here on
we stop drawing transistors and draw gates instead, the way chip designers do. It is the first
abstraction on the climb, and it is a safe one: every gate below could be swapped for a handful of
transistors without changing a thing.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;logic-sim&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Digital logic simulator…&lt;/p&gt;&lt;iframe src=&quot;/tools/logic-sim/?bare=1&amp;amp;embed=1&amp;amp;example=gates&quot; title=&quot;Digital logic simulator (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: flip A and B and watch all seven gates answer at once.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/logic-sim/?example=gates&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Two switches, &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt;, feed seven gates. Bright wires and outputs are 1, dim ones are 0.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;With both switches off, notice that NAND, NOR, XNOR and NOT A are already lit. A gate whose name
starts with N is the inverse of its namesake.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Flip &lt;code&gt;A&lt;/code&gt; on. AND stays dark (it needs both), OR lights (it needs either), XOR lights (it wants
them &lt;strong&gt;different&lt;/strong&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now turn &lt;code&gt;B&lt;/code&gt; on too. XOR goes dark again, XNOR lights. XOR is the odd one out of the seven, and
it is about to become important: it is the gate that adds.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;What to notice: NAND on its own is enough to build every other gate — NOT is a NAND with its inputs
tied together, AND is NAND then NOT, and so on. That is why early logic families sold chips full of
nothing but NAND gates, and why &quot;everything is NAND&quot; is the usual first surprise in a digital
course.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-3-gates-that-add&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-3-gates-that-add&quot;&gt;&lt;/a&gt;Step 3: gates that add&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Add two one-bit numbers and there are only four cases: 0 + 0 = 0, 0 + 1 = 1, 1 + 0 = 1 and
1 + 1 = 10 in binary — that is, a sum bit of 0 and a carry of 1. Look at those sum bits: 0, 1, 1, 0.
That is XOR. Look at the carries: 0, 0, 0, 1. That is AND. Two gates make a &lt;strong&gt;half adder&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It is only half an adder because it cannot accept a carry coming in from the column to its right.
A &lt;strong&gt;full adder&lt;/strong&gt; takes three inputs — A, B and a carry in — and makes a sum and a carry out. You can
build one from two half adders and an OR gate.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;logic-sim&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Digital logic simulator…&lt;/p&gt;&lt;iframe src=&quot;/tools/logic-sim/?bare=1&amp;amp;embed=1&amp;amp;example=half-adder#truth&quot; title=&quot;Digital logic simulator (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: read down the S and C columns, then the SUM and COUT columns.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/logic-sim/?example=half-adder#truth&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This opens on the &lt;strong&gt;Truth table&lt;/strong&gt; tab, which simulates the circuit for every combination of its
inputs. The top part of the circuit is the half adder (inputs &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, outputs &lt;code&gt;S&lt;/code&gt; and &lt;code&gt;C&lt;/code&gt;); the
bottom part is a full adder packaged as a reusable component (inputs &lt;code&gt;A2&lt;/code&gt;, &lt;code&gt;B2&lt;/code&gt;, &lt;code&gt;Cin&lt;/code&gt;, outputs
&lt;code&gt;Sum&lt;/code&gt; and &lt;code&gt;Cout&lt;/code&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Find the rows where &lt;code&gt;A2&lt;/code&gt;, &lt;code&gt;B2&lt;/code&gt; and &lt;code&gt;CIN&lt;/code&gt; are all 1. Both &lt;code&gt;SUM&lt;/code&gt; and &lt;code&gt;COUT&lt;/code&gt; are 1: one plus one
plus one is three, which is 11 in binary.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Scroll down past the table: for each output the tool reads the logic back as an expression and
minimises it. The full adder&amp;#8217;s sum turns out to be a three-way XOR.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Switch to the &lt;strong&gt;Circuit&lt;/strong&gt; tab and double-click the &lt;strong&gt;Full adder&lt;/strong&gt; block to look inside it. A component
is just a circuit with pins — the second abstraction on the climb.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Chain four full adders, each one&amp;#8217;s carry out feeding the next one&amp;#8217;s carry in, and you can add two
four-bit numbers.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;logic-sim&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Digital logic simulator…&lt;/p&gt;&lt;iframe src=&quot;/tools/logic-sim/?bare=1&amp;amp;embed=1&amp;amp;example=adder4&quot; title=&quot;Digital logic simulator (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: set A to 0111 and B to 0001 and watch the carry ripple up.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/logic-sim/?example=adder4&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The switches come in pairs: &lt;code&gt;A0&lt;/code&gt;/&lt;code&gt;B0&lt;/code&gt; is the lowest bit, &lt;code&gt;A3&lt;/code&gt;/&lt;code&gt;B3&lt;/code&gt; the highest. The four sum bits
and the final carry come out on the right, with the sum shown in hex.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Turn on &lt;code&gt;A0&lt;/code&gt; and &lt;code&gt;B1&lt;/code&gt;: 1 + 2. The display reads 3.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now set A to 7 (&lt;code&gt;A0&lt;/code&gt;, &lt;code&gt;A1&lt;/code&gt;, &lt;code&gt;A2&lt;/code&gt; on) and B to 1 (&lt;code&gt;B0&lt;/code&gt; on). Every column produces a carry that the
next column has to absorb, and the display jumps to 8.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set every switch on: 15 + 15. The display shows E and &lt;code&gt;Carry&lt;/code&gt; lights: 30 is 1 1110 in binary, one
bit too many for four, so the fifth bit falls out as the carry.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Turn on &lt;strong&gt;Gate delays&lt;/strong&gt; in the toolbar and repeat 7 + 1. Now each gate takes a moment, and you can
watch the carry &lt;strong&gt;ripple&lt;/strong&gt; from bit 0 to bit 3, the sum briefly showing wrong values on the way.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;What to notice: this is a &lt;strong&gt;ripple-carry&lt;/strong&gt; adder, and the ripple is its weakness. A 64-bit version
has to wait for a carry to crawl through 64 stages. Real processors spend extra gates on
carry-lookahead logic that works out all the carries at once. Speed in hardware is often bought
with more transistors.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you would rather write this than wire it, the
&lt;a href=&quot;/tools/hdl-trainer/?lesson=adder#course&quot;&gt;Adders and hierarchy lesson&lt;/a&gt; of the HDL trainer has
you build the same full adder and four-bit chain in Verilog, the language chips are actually
designed in.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-4-memory-from-feedback&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-4-memory-from-feedback&quot;&gt;&lt;/a&gt;Step 4: memory from feedback&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Everything so far has been &lt;strong&gt;combinational&lt;/strong&gt;: the outputs depend only on the inputs right now. Turn
the switches back and the answer goes back too. A computer also needs to remember — the number it
is adding to, the instruction it is on — and the trick that gives gates a memory is wonderfully
small. Feed a gate&amp;#8217;s output back into its own input.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;logic-sim&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Digital logic simulator…&lt;/p&gt;&lt;iframe src=&quot;/tools/logic-sim/?bare=1&amp;amp;embed=1&amp;amp;example=sr&quot; title=&quot;Digital logic simulator (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Set, release it, and notice that Q stays on.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/logic-sim/?example=sr&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Two NOR gates, each feeding the other. &lt;code&gt;Set&lt;/code&gt; and &lt;code&gt;Reset&lt;/code&gt; are push buttons. When the page opens,
the outputs are red: &lt;strong&gt;unknown&lt;/strong&gt;. Nobody has told the circuit what to remember yet, and the
simulator is honest about it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Hold &lt;code&gt;Set&lt;/code&gt; for a moment and let go. &lt;code&gt;Q&lt;/code&gt; goes to 1 and &lt;strong&gt;stays&lt;/strong&gt; there, even though nothing is
pressing it any more. Each gate is holding the other in place.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Press &lt;code&gt;Reset&lt;/code&gt;. &lt;code&gt;Q&lt;/code&gt; drops to 0, &lt;code&gt;Q̄&lt;/code&gt; rises, and that state holds too.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Press &lt;code&gt;Set&lt;/code&gt; again while it is already set. Nothing changes. The latch only cares about which
button was pressed last.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;What to notice: that is one bit of memory, built from two NOR gates — eight transistors in CMOS. This
&lt;strong&gt;SR latch&lt;/strong&gt; is the ancestor of every register in a CPU. Add a little gating so that it only listens
when a clock signal says so, and you have a &lt;strong&gt;flip-flop&lt;/strong&gt;: a bit that changes only on the tick of a
clock. Line up four flip-flops and you have a four-bit register. Wire their outputs back to their
inputs through some logic and you have a circuit that steps through states on its own — the
&lt;a href=&quot;/tools/logic-sim/?example=counter&quot;&gt;binary counter example&lt;/a&gt; does exactly that, counting 0 to 15
from four flip-flops and two AND gates.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The clock is the quiet hero of digital design. Between ticks, the combinational logic is free to
glitch and ripple as much as it likes, as you saw in the adder. On the tick, every flip-flop
captures the settled answer at once. A designer&amp;#8217;s main job is to make sure the slowest path
through the logic settles before the next tick; the length of that path sets the clock speed.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-5-a-cpu-you-can-see-inside&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-5-a-cpu-you-can-see-inside&quot;&gt;&lt;/a&gt;Step 5: a CPU you can see inside&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We now have every ingredient. A register to hold a number. An adder to change it. A counter to
step through a list. Some memory to hold that list. Put them together with one more idea, and you
have a processor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The idea is that &lt;strong&gt;the list is instructions&lt;/strong&gt;. A counter, called the &lt;strong&gt;program counter&lt;/strong&gt; or PC, points
at a byte in a read-only memory. That byte is decoded: some of its bits say &lt;strong&gt;what&lt;/strong&gt; to do (the
opcode), the rest give a number to do it with (the operand). The decoded bits steer multiplexers
— switches made of gates — that route the right values into the adder and the right result into
the right register. On the next clock tick, the result is captured and the program counter moves
on. Then it all happens again.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;logic-sim&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:620px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Digital logic simulator…&lt;/p&gt;&lt;iframe src=&quot;/tools/logic-sim/?bare=1&amp;amp;embed=1&amp;amp;example=cpu&quot; title=&quot;Digital logic simulator (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: watch the two hex displays, then press Pause and step one clock cycle at a time.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/logic-sim/?example=cpu&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is a whole four-bit CPU in about two dozen parts. Find the pieces from earlier: the &lt;code&gt;Counter&lt;/code&gt; on
the left is the program counter, &lt;code&gt;ROM&lt;/code&gt; holds the program, &lt;code&gt;DEC&lt;/code&gt; decodes the opcode, the &lt;code&gt;ALU&lt;/code&gt; is an
adder that can also subtract, &lt;code&gt;A&lt;/code&gt; is the accumulator register, &lt;code&gt;C&lt;/code&gt; is the carry flag (a single
flip-flop, with a yellow LED) and &lt;code&gt;OUT&lt;/code&gt; is an output register. Hex displays show the PC, A and OUT. It starts running as soon as it
loads.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The program in the ROM is four bytes long:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-none hljs&quot;&gt;00   LDI 0      ; A ← 0
13   loop: ADD 3 ; A ← A + 3
20   OUT        ; OUT ← A
31   JMP loop   ; PC ← 1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Each byte is one instruction: the high hex digit is the opcode, the low digit the operand. So &lt;code&gt;13&lt;/code&gt;
means &quot;opcode 1 (ADD), operand 3&quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Watch the &lt;code&gt;OUT&lt;/code&gt; display, on the far right: 3, 6, 9, C, F, then 2. Four bits can only count to 15, so 15 + 3 wraps
round to 2 — and the carry flag notes that it happened.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Press &lt;strong&gt;Pause&lt;/strong&gt;, then &lt;strong&gt;Cycle&lt;/strong&gt; to advance one clock period at a time. The display under the
counter is the PC; watch it go 1, 2, 3, then jump back to 1.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Turn on &lt;strong&gt;Gate delays&lt;/strong&gt; and run slowly. Every instruction becomes a little wave of activity
spreading from the ROM through the decoder and ALU, settling before the next tick.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select the ROM to edit the program. Try &lt;code&gt;LDI 5&lt;/code&gt;, &lt;code&gt;l: OUT&lt;/code&gt;, &lt;code&gt;SUB 2&lt;/code&gt;, &lt;code&gt;JC l&lt;/code&gt;, &lt;code&gt;HLT&lt;/code&gt;. It counts 5,
3, 1 and then stops, because the third subtraction borrows, clears the carry flag, and the
conditional jump &lt;code&gt;JC&lt;/code&gt; falls through to the halt.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;What to notice: the program is &lt;strong&gt;data&lt;/strong&gt;. Nothing in the hardware changed when you edited the ROM,
yet the machine did something different. That is the whole point of a processor, and it is why
the same chip can run a word processor, a game, or this web page. The second program also shows
the other essential ingredient: a &lt;strong&gt;conditional&lt;/strong&gt; jump. A machine that can choose what to do next
based on a result is, given enough memory, able to compute anything computable.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-6-a-real-6502&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-6-a-real-6502&quot;&gt;&lt;/a&gt;Step 6: a real 6502&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The 6502 was launched in 1975 at a price of $25, when its rivals cost several times as much, and it
ended up in a huge share of the home computers and consoles of the next decade. It is an eight-bit
processor rather than a four-bit one, with three registers (A, X and Y), a stack, more addressing
modes, and around 150 opcodes instead of eight. But at heart it is our tiny CPU, grown up: a
program counter, a decoder, an ALU, an accumulator and a handful of flags.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here is the &quot;count in threes&quot; program again, written for the 6502.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;mos6502&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading 6502 assembler…&lt;/p&gt;&lt;iframe src=&quot;/tools/mos6502/?bare=1&amp;amp;embed=1&amp;amp;src=%3B%20count%20in%20threes%2C%20like%20the%20tiny%20CPU%0A%20%20%20%20%20%20%20%20*%20%3D%20%240600%0AOUT%20%20%20%20%20%3D%20%240200%0A%20%20%20%20%20%20%20%20lda%20%230%0Aloop%20%20%20%20clc%0A%20%20%20%20%20%20%20%20adc%20%233%0A%20%20%20%20%20%20%20%20sta%20OUT%0A%20%20%20%20%20%20%20%20jmp%20loop%0A#asm&quot; title=&quot;6502 assembler (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: read the listing, the address and the bytes of each instruction, then compare it with the tiny CPU&amp;#39;s ROM.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/mos6502/?src=%3B%20count%20in%20threes%2C%20like%20the%20tiny%20CPU%0A%20%20%20%20%20%20%20%20*%20%3D%20%240600%0AOUT%20%20%20%20%20%3D%20%240200%0A%20%20%20%20%20%20%20%20lda%20%230%0Aloop%20%20%20%20clc%0A%20%20%20%20%20%20%20%20adc%20%233%0A%20%20%20%20%20%20%20%20sta%20OUT%0A%20%20%20%20%20%20%20%20jmp%20loop%0A#asm&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The listing on the right shows each instruction&amp;#8217;s address and the bytes it assembles to: &lt;code&gt;A9 00&lt;/code&gt;
for &lt;code&gt;lda #0&lt;/code&gt;, &lt;code&gt;18&lt;/code&gt; for &lt;code&gt;clc&lt;/code&gt;, &lt;code&gt;69 03&lt;/code&gt; for &lt;code&gt;adc #3&lt;/code&gt;, &lt;code&gt;8D 00 02&lt;/code&gt; for &lt;code&gt;sta $0200&lt;/code&gt; and &lt;code&gt;4C 02 06&lt;/code&gt; for
&lt;code&gt;jmp loop&lt;/code&gt;. Eleven bytes in all.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Compare it with the tiny CPU&amp;#8217;s ROM. Load, add, output, jump: the same four ideas. The 6502&amp;#8217;s
instructions are longer because its addresses are sixteen bits, so &lt;code&gt;jmp loop&lt;/code&gt; needs two bytes to
say where &lt;code&gt;loop&lt;/code&gt; is — &lt;code&gt;02 06&lt;/code&gt;, low byte first.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Notice the extra &lt;code&gt;clc&lt;/code&gt; (clear carry). The 6502 has no plain add: &lt;code&gt;adc&lt;/code&gt; always adds the carry flag
too, so that a sixteen-bit sum can be built from two eight-bit ones. Delete the &lt;code&gt;clc&lt;/code&gt; and the
program may count in fours whenever the previous sum overflowed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Switch to the &lt;strong&gt;Opcode table&lt;/strong&gt; tab and filter for &lt;code&gt;ADC&lt;/code&gt;. There are eight versions of it, one for
each way of finding the number to add, each with its own cycle count.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Assembling is only half of it. Below is an Atari 800XL, emulated in WebAssembly, with a 6502 that
runs the real Atari operating system. The example program is fifteen lines long and does something
no amount of reading quite prepares you for.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;atari800-emu&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Atari 8-bit computer: emulator, BASIC, assembler and debugger…&lt;/p&gt;&lt;iframe src=&quot;/tools/atari800-emu/?bare=1&amp;amp;embed=1&amp;amp;mode=asm&amp;amp;example=bars&quot; title=&quot;Atari 8-bit computer: emulator, BASIC, assembler and debugger (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Assemble &amp;amp; run, then read the loop on lines 14 to 19.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/atari800-emu/?mode=asm&amp;amp;example=bars&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Press &lt;strong&gt;Assemble &amp;amp; run&lt;/strong&gt;. The machine powers on, the program is assembled and loaded at &lt;code&gt;$2000&lt;/code&gt;,
and the screen fills with scrolling bands of colour.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Read the loop. It reads &lt;code&gt;VCOUNT&lt;/code&gt;, the number of the scan line the TV beam is drawing; adds the
frame counter so the pattern moves; then &lt;code&gt;sta WSYNC&lt;/code&gt;, which freezes the CPU until the beam
reaches the end of the line; then stores the value in &lt;code&gt;COLBK&lt;/code&gt;, the background colour. Every
coloured stripe on the screen is one trip round a six-instruction loop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Press &lt;strong&gt;Pause&lt;/strong&gt; and open the &lt;strong&gt;Debugger&lt;/strong&gt; tab. The registers you see there — PC, A, X, Y, S and the
flags — are the 6502&amp;#8217;s equivalents of the tiny CPU&amp;#8217;s counter, accumulator and carry flip-flop.
Press &lt;strong&gt;Step&lt;/strong&gt; and watch the PC move through the loop one instruction at a time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Replace &lt;code&gt;adc RTCLOK&lt;/code&gt; with &lt;code&gt;adc #0&lt;/code&gt;, or add &lt;code&gt;asl a&lt;/code&gt; after &lt;code&gt;lda VCOUNT&lt;/code&gt; to double the line number,
and run it again. Small edits, very different pictures.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;What to notice: that loop is racing a television. Each 6502 instruction takes two to six clock
cycles, at about 1.8 MHz, and a scan line lasts 114 cycles. Programming these machines meant
counting cycles the way we counted gate delays in the adder: the logic has to settle before the
next tick, only now the tick is the edge of the picture.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-climb-looking-back&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-climb-looking-back&quot;&gt;&lt;/a&gt;The climb, looking back&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Six steps, each a small idea:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;A transistor is a switch controlled by a wire, and wired the right way it is a NOT gate.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A few transistors make NAND and NOR, and NAND alone can make every other gate.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;XOR and AND make a half adder; two of those make a full adder; a chain of them adds numbers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Feedback turns gates into memory; a clock makes that memory change in step.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A counter, a ROM, a decoder, an adder and a register make a processor, and the program is just
data in the ROM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Grow every part — wider words, more registers, more opcodes — and you have a 6502. Grow it again,
with caches, pipelines and billions of transistors, and you have the chip you are reading this on.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;At no point did anything magic happen. Each level is the one below it, wrapped up and given a name.
The only genuinely surprising thing is how few kinds of idea it takes.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;where-to-go-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#where-to-go-next&quot;&gt;&lt;/a&gt;Where to go next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/logic-sim/&quot;&gt;Digital logic simulator&lt;/a&gt; — build your own circuits, or open the
&lt;a href=&quot;/tools/logic-sim/?example=traffic&quot;&gt;traffic-light state machine&lt;/a&gt;,
&lt;a href=&quot;/tools/logic-sim/?example=seg7&quot;&gt;seven-segment decoder&lt;/a&gt; and
&lt;a href=&quot;/tools/logic-sim/?example=glitch&quot;&gt;glitch&lt;/a&gt; examples.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/logic/&quot;&gt;Boolean logic&lt;/a&gt; — minimise expressions and draw the Karnaugh maps behind the
adder&amp;#8217;s equations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/circuit-sim/&quot;&gt;Circuit simulator&lt;/a&gt; — the transistor level, with real device models and
ngspice to compare against.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/hdl-trainer/?lesson=adder#course&quot;&gt;HDL trainer&lt;/a&gt; — learn Verilog from wires and gates up
to UARTs, VGA and FIFOs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/fpga/?example=cpu&quot;&gt;FPGA playground&lt;/a&gt; — a small CPU in Verilog, synthesised and placed
on a real FPGA in the browser; the &lt;a href=&quot;/tools/fpga/?example=rv32i&quot;&gt;RISC-V example&lt;/a&gt; goes further.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/mos6502/&quot;&gt;6502 assembler&lt;/a&gt; — assemble, disassemble and browse every opcode with its
cycle counts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/atari800-emu/?mode=asm&quot;&gt;Atari 8-bit computer&lt;/a&gt; — more 6502 examples, from display-list
interrupts to player-missile graphics, with a full debugger. The 6502 also lives in the
&lt;a href=&quot;/tools/apple2/&quot;&gt;Apple II&lt;/a&gt;, &lt;a href=&quot;/tools/c64-emu/&quot;&gt;Commodore 64&lt;/a&gt; and
&lt;a href=&quot;/tools/bbc-micro/&quot;&gt;BBC Micro&lt;/a&gt; tools.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>How a brushless motor spins</title>
    <link href="https://geekychris.github.io/lab/explorations/how-a-brushless-motor-spins/"/>
    <id>https://geekychris.github.io/lab/explorations/how-a-brushless-motor-spins/</id>
    <updated>2026-10-30T00:00:00Z</updated>
    <summary>From magnets and coils to a quadcopter in the air: how a brushless motor is wound, how an ESC switches it in six steps, how it finds the rotor without sensors, what field-oriented control does better, and what happens when you bolt four of them to a frame — with the simulators running alongside.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Pick up the motor from a racing drone and turn it with your fingers. It is not smooth: it
clicks gently from one resting place to the next, as if it had little detents. Spin it hard and
it hums. There are no brushes, no commutator, nothing touching anything except the bearings.
Three wires come out of it, and on its own it will do absolutely nothing — connect it straight
to a battery and you get a twitch, a lot of current and a warm motor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Everything interesting about a brushless motor lives in the gap between that inert lump of
copper and magnets and the controller that drives it. This exploration walks through both, from
the windings to the switching pattern to the trick that lets the controller find the rotor
without any sensors, and ends with four of these motors holding a quadcopter in the air. The
simulators are embedded as we go. They all run in your browser, and nothing you do in them can
burn anything out, which is more than I can say for my first ESC.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;magnets-go-round-coils-stay-still&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#magnets-go-round-coils-stay-still&quot;&gt;&lt;/a&gt;Magnets go round, coils stay still&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;An ordinary brushed DC motor has its coils on the spinning part and its magnets in the case. To
get current into coils that are turning, it uses brushes rubbing on a segmented copper ring, the
commutator. As the rotor turns, the brushes slide from segment to segment and the current is
switched into whichever coils are in the right place to keep pulling. It is clever, mechanical
and self-timing, and it is also the thing that wears out, sparks and limits speed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A brushless motor turns this inside out. The coils sit still, wound around the teeth of a
laminated iron stator, and the magnets are on the rotor. In a drone motor the rotor is the bell
on the outside — an &lt;em&gt;outrunner&lt;/em&gt; — with a ring of curved magnets glued inside it. Nothing that
carries current moves, so there is nothing to wear. The price is that the commutator has gone:
something else now has to switch the current between coils at exactly the right moments. That
something is the electronic speed controller, the ESC.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Before we get to the switching, look at the copper. Here is a typical 5-inch quad motor, a
&quot;2207&quot; (a 22 mm stator, 7 mm tall) wound for 1,950 Kv:&lt;/p&gt;
&lt;/div&gt;
&lt;style&gt;.tool-embed{margin:1.6rem 0}.tool-embed .te-frame{position:relative;height:var(--te-h,520px);max-height:90vh;border:1px solid var(--line,#d9d6d0);border-radius:10px;overflow:hidden;background:var(--wash,#f3efe9)}@media (max-width:640px){.tool-embed .te-frame{height:min(var(--te-h,520px),75vh);border-radius:8px}}.tool-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent}.tool-embed .te-loading{position:absolute;inset:0;margin:0;display:grid;place-items:center;color:var(--faint,#8a9096);font:.9rem var(--sans,system-ui,sans-serif)}.tool-embed .te-ready .te-loading{visibility:hidden}.tool-embed figcaption{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.25rem 1rem;margin-top:.45rem;font-size:.86rem;color:var(--muted,#5f646b)}.tool-embed .te-open{white-space:nowrap;font-weight:600;margin-left:auto}.tool-embed-missing{padding:.8rem 1rem;border:1px dashed var(--line,#d9d6d0);border-radius:8px;color:var(--muted,#5f646b)}&lt;/style&gt;&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;bldc-motor&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Brushless motors — inside and how they are driven…&lt;/p&gt;&lt;iframe src=&quot;/tools/bldc-motor/?bare=1&amp;amp;embed=1&amp;amp;example=fpv-5in#construction&quot; title=&quot;Brushless motors — inside and how they are driven (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: watch which coils light as the bell turns; then change Poles from 14 to 8 and see the winding pattern, winding factor and cogging steps change.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/bldc-motor/?example=fpv-5in#construction&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The drawing shows twelve teeth (slots) and fourteen magnets (poles): this is &lt;strong&gt;12N14P&lt;/strong&gt;, the
combination on almost every multirotor motor. Each tooth carries a coil, coloured by the phase
it belongs to — red, amber and blue for A, B and C — and the pattern written above the picture,
&lt;code&gt;AabBCcaABbcC&lt;/code&gt;, reads round the stator from the top. A capital letter is a coil wound one way
round its tooth, a small one is wound the other way.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The rotor is already turning, with the coils lit as an ESC would switch them (&lt;strong&gt;Pause&lt;/strong&gt;
stops it). Watch how far the bell moves during one full cycle of the lighting pattern: one &lt;em&gt;pole pair&lt;/em&gt;, two
magnets&#39; worth, about 51°. Fourteen magnets is seven pole pairs, so the electrical cycle runs
seven times for every turn of the shaft. That is why the panel says 25,000 rpm is
175,000 eRPM — electrical revolutions per minute — a number you will meet again in a moment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Read the &lt;strong&gt;cogging steps per turn&lt;/strong&gt;: 84, the lowest common multiple of 12 and 14. Those are the
little detents you felt with your fingers. Many small steps make for smooth running; a
combination like 12N8P has only 24, and they are much stronger.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change &lt;strong&gt;Poles&lt;/strong&gt; to 8, then 10, then back to 14. The &lt;strong&gt;winding factor&lt;/strong&gt; — how much of each coil&amp;#8217;s
effort actually adds up in the phase, the designer&amp;#8217;s efficiency of copper — is 0.866 for
12N8P and 0.933 for both 12N10P and 12N14P. The cogging steps go from 24 to 60 to 84: same
copper, smoother motor.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Switch &lt;strong&gt;Layout&lt;/strong&gt; to &quot;alternate teeth&quot;. Half the teeth go bare. This is the LRK winding:
easier to wind by hand, slightly different character.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The important idea to carry forward is that the stator can make a magnetic field that points in
any of several directions, depending on which phases carry current and which way. If you can make
that field point a little ahead of the rotor&amp;#8217;s magnets, the magnets chase it. Keep moving the
field and the rotor keeps chasing. That is the whole motor.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;six-steps-round-the-circle&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#six-steps-round-the-circle&quot;&gt;&lt;/a&gt;Six steps round the circle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Three phases, each of which can be pulled up to the battery, pulled down to ground, or left
floating. The ESC does this with six MOSFETs, a high and a low switch per phase — a three-phase
bridge. The simplest way to drive it is to always have exactly one phase high, one phase low
and one floating. There are six ways to choose that, and stepping through them in order rotates
the field round the stator in six 60° jumps. This is &lt;strong&gt;six-step&lt;/strong&gt; or &lt;strong&gt;trapezoidal&lt;/strong&gt; commutation,
and it is what nearly every hobby ESC did for twenty years.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The simulator reduces the motor to the simplest possible picture — two poles, three coils —
so that one electrical cycle is one turn of the drawing:&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;bldc-motor&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:720px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Brushless motors — inside and how they are driven…&lt;/p&gt;&lt;iframe src=&quot;/tools/bldc-motor/?bare=1&amp;amp;embed=1&amp;amp;example=fpv-5in#sixstep&quot; title=&quot;Brushless motors — inside and how they are driven (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: follow the highlighted row of the table as the rotor turns, then scroll to the terminal-voltage plot and find the dotted zero crossings.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/bldc-motor/?example=fpv-5in#sixstep&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The magnet is the red and grey bar. The arrow is the stator field. The three dots around the
edge are Hall-effect sensors, each filling in when it sees a north pole, and the table on the
right lists the six steps: which Hall code is showing, which phase is switched high, which low,
and which is left floating.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Pause the animation and step through it by eye. The field arrow always sits somewhere between
60° and 120° ahead of the magnet&amp;#8217;s north pole — 90° on average — and jumps 60° at every
commutation. Torque is greatest when the field is at right angles to the magnet, so six-step
spends its whole life a little either side of ideal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Look at the three Hall bits. Of the eight codes three bits can make, only six ever appear:
000 and 111 mean a sensor has failed. Three cheap sensors, 120° apart, are enough to know which
sixth of a cycle the rotor is in, and that is all six-step needs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Scroll down to &lt;strong&gt;One electrical cycle&lt;/strong&gt;. The phase currents are blocks — positive for 120°,
off, negative for 120°, off — with their corners rounded by the coil&amp;#8217;s inductance. Now look at
the torque trace: it dips at every commutation. The summary above it puts a number on it,
about 30% torque ripple for this motor. You hear that ripple as part of the motor&amp;#8217;s whine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change &lt;strong&gt;Back-EMF shape&lt;/strong&gt; from sinusoidal to trapezoidal. The ripple drops: block currents
suit a motor whose back-EMF is flat-topped. Most drone motors are closer to sinusoidal, which
is a hint of where we are going.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;hearing-the-rotor-without-sensors&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#hearing-the-rotor-without-sensors&quot;&gt;&lt;/a&gt;Hearing the rotor without sensors&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Drone motors have no Hall sensors. They spin at tens of thousands of rpm in a cloud of dust,
next to a lot of switching noise, and every gram matters. So how does the ESC know when to
commutate?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The motor tells it. A magnet sweeping past a coil induces a voltage in it — the motor is also a
generator — and this &lt;strong&gt;back-EMF&lt;/strong&gt; rises with speed. Kv is just that relationship turned round: a
1,950 Kv motor generates one volt for every 1,950 rpm, and on a 22.2 V battery it would spin up
to a little over 43,000 rpm with no load, the speed at which its own back-EMF cancels the
supply. In six-step there is always one phase floating, not driven by the ESC, and the voltage on
a floating phase &lt;em&gt;is&lt;/em&gt; its back-EMF (plus the star point it sits on). When that back-EMF crosses
the midpoint — a &lt;strong&gt;zero crossing&lt;/strong&gt; — the rotor is exactly halfway through the step. The ESC
waits another 30° electrical, judged from the time between the last two crossings, and switches.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Go back to the six-step view above and scroll to the &lt;strong&gt;terminal voltage&lt;/strong&gt; plot. The sloping lines
are the floating phases&#39; back-EMF, rising or falling across the gap between the flat driven
levels. The dotted verticals mark where each crosses the star-point voltage; the dashed ones,
30° later, are the commutations the ESC schedules from them.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Notice the short vertical spike just after each commutation, where the phase that was
switched off is clamped to 0 V or the full bus by a diode until its current dies away. A
real ESC&amp;#8217;s comparator has to ignore that window, or it sees a false zero crossing and
stumbles. Desyncs — the motor suddenly stuttering in a hard punch — are often exactly this.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Type a &lt;strong&gt;ZC filter delay&lt;/strong&gt; of 10 µs. The commutations slide late, the field falls behind the
ideal angle, and torque and efficiency drop. At 175,000 eRPM one electrical cycle lasts only
about 340 µs, so 10 µs is already several degrees.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now set a &lt;strong&gt;Timing advance&lt;/strong&gt; of 15°. The ESC commutates early to make up for delays and for the
inductance that slows the current&amp;#8217;s rise. The &lt;strong&gt;Timing advance&lt;/strong&gt; chart at the bottom sweeps it
for you: at this speed, more advance buys torque. BLHeli and Bluejay&amp;#8217;s &quot;timing&quot; setting is this
number.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lower the &lt;strong&gt;Speed&lt;/strong&gt; to 2,000 rpm. The back-EMF shrinks to a few volts and the crossings get
hard to see — which is why a sensorless motor has to be started open-loop, kicked round blind
until it is fast enough to hear itself. It is also why sensorless motors are rough at very low
speed, and why an RC car, which has to crawl away from a standstill under load, often has
sensors.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;field-oriented-control-steering-the-field-smoothly&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#field-oriented-control-steering-the-field-smoothly&quot;&gt;&lt;/a&gt;Field-oriented control: steering the field smoothly&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Six-step is cheap and effective, but it chops a smooth rotating field into six lurches. The
better answer is to make the stator field rotate continuously, always exactly at right angles
to the rotor&amp;#8217;s magnets, with just enough current to make the torque you want. That is
&lt;strong&gt;field-oriented control&lt;/strong&gt;, FOC, and it is what VESC, ODrive, most e-bike controllers and a
growing number of drone ESCs (AM32 among them) do.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The difficulty is that three sinusoidal phase currents are awkward things to control — they
never sit still. FOC&amp;#8217;s trick is two changes of point of view. The &lt;strong&gt;Clarke&lt;/strong&gt; transform adds the
three currents (which always sum to zero) into a single vector on two fixed axes, α and β. The
&lt;strong&gt;Park&lt;/strong&gt; transform then looks at that vector from the rotor itself, on axes d (along the magnet)
and q (at right angles to it). In the rotor&amp;#8217;s frame, a perfectly driven motor&amp;#8217;s currents stop
rotating and become two constant numbers.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;bldc-motor&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:760px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Brushless motors — inside and how they are driven…&lt;/p&gt;&lt;iframe src=&quot;/tools/bldc-motor/?bare=1&amp;amp;embed=1&amp;amp;example=fpv-5in#foc&quot; title=&quot;Brushless motors — inside and how they are driven (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: watch the three phase currents swirl while id and iq stay flat; then scroll to the six-step, sine and FOC comparison.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/bldc-motor/?example=fpv-5in#foc&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Watch the vector diagram. The three coloured arrows — each phase&amp;#8217;s contribution — change
length all the time, but they always add up to the same black vector, which turns with the
rotor. In the table, &lt;code&gt;ia&lt;/code&gt;, &lt;code&gt;ib&lt;/code&gt; and &lt;code&gt;ic&lt;/code&gt; change continuously; &lt;code&gt;id&lt;/code&gt; stays at 0 and &lt;code&gt;iq&lt;/code&gt; stays at
10 A. Those two steady numbers are what the controller actually regulates.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set the &lt;strong&gt;id reference&lt;/strong&gt; to −5. The current vector leans back towards the magnet&amp;#8217;s south pole
and starts to weaken the magnet&amp;#8217;s field. That is &lt;strong&gt;field weakening&lt;/strong&gt;: it costs current but lets
the motor run above the speed its back-EMF would otherwise allow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Scroll to &lt;strong&gt;Six-step, sine and FOC at the same speed and torque&lt;/strong&gt;. All three are asked for the
same torque at 25,000 rpm. FOC makes it with the least current and no ripple at all; six-step
gets there with about 30% ripple and slightly more current; and plain sinusoidal voltage
drive, with no current feedback, cannot make the torque at all at this speed, because the
coil&amp;#8217;s inductance makes the current lag the voltage and nobody corrects for it. That one row
is the whole case for measuring the currents.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At the bottom, &lt;strong&gt;The d and q current loops&lt;/strong&gt; are two ordinary PI controllers, one for each
axis. Untick &lt;strong&gt;Decoupling feed-forward&lt;/strong&gt; and step again: the d current now kicks when q moves,
because at speed the two axes leak into each other. Then try a loop bandwidth of 200 Hz and
of 5,000 Hz and see how quickly iq reaches its target.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;FOC is more arithmetic — a sine, a cosine and two multiplications per axis, twenty thousand
times a second — and it needs current sensors. A modern 32-bit microcontroller shrugs at both,
which is why it is slowly taking over.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;how-the-flight-controller-speaks-to-the-esc&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#how-the-flight-controller-speaks-to-the-esc&quot;&gt;&lt;/a&gt;How the flight controller speaks to the ESC&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A multirotor&amp;#8217;s flight controller decides, thousands of times a second, how hard each motor
should push. It used to say so with a servo pulse — 1,000 to 2,000 µs wide, fifty times a
second — which the ESC had to measure and calibrate. Then came OneShot and MultiShot, the same
idea with shorter pulses. Now almost everything uses &lt;strong&gt;DShot&lt;/strong&gt;: the number itself, sent as bits.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;bldc-motor&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Brushless motors — inside and how they are driven…&lt;/p&gt;&lt;iframe src=&quot;/tools/bldc-motor/?bare=1&amp;amp;embed=1&amp;amp;example=fpv-5in#protocols&quot; title=&quot;Brushless motors — inside and how they are driven (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: drag the throttle slider and watch the DShot frame change bit by bit; then scroll down to the eRPM telemetry.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/bldc-motor/?example=fpv-5in#protocols&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A DShot frame is sixteen bits: an 11-bit throttle value (48 to 2,047; the values below 48 are
commands such as &quot;beep&quot; or &quot;reverse direction&quot;), one bit asking for telemetry, and a 4-bit
checksum. At DShot600 each bit takes 1.67 µs, so a whole frame is under 27 µs — no calibration,
no jitter, and a corrupt frame is simply ignored.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Move &lt;strong&gt;Throttle (%)&lt;/strong&gt; and watch the blue value bits and the green checksum. Switch &lt;strong&gt;Send&lt;/strong&gt; to a
command and send &quot;1: Beep 1&quot; — this is how your quad beeps when you have lost it in long grass.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Untick &lt;strong&gt;Bidirectional&lt;/strong&gt;. The checksum flips. With bidirectional DShot the line is inverted
and, after each frame, the ESC talks back on the same wire.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Scroll to &lt;strong&gt;eRPM telemetry&lt;/strong&gt;. What the ESC sends back is the electrical period — from its
zero-crossing timing, the same thing it measures to commutate — and the flight controller
divides by the pole pairs to get shaft speed. 140,000 eRPM on a 14-pole motor is about
20,000 rpm. Set &lt;strong&gt;Motor poles&lt;/strong&gt; to 12 and the same reply reads as a different speed: this is why
Betaflight&amp;#8217;s &lt;code&gt;motor_poles&lt;/code&gt; must match your motor, or its RPM filter notches the wrong
frequencies.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;four-motors-and-a-battery&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#four-motors-and-a-battery&quot;&gt;&lt;/a&gt;Four motors and a battery&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A motor alone is only half an answer. What it does depends on the propeller it turns and the
battery that feeds it, and the three pull against each other: a bigger prop makes more thrust
and draws more current; the current makes the battery sag; the sag slows the motor. The RC power
solver finds the point where they agree.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;rc-power&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:720px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading RC power setup — motor, prop and battery…&lt;/p&gt;&lt;iframe src=&quot;/tools/rc-power/?bare=1&amp;amp;embed=1&quot; title=&quot;RC power setup — motor, prop and battery (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: drag the battery from 4 to 6 cells and watch current, thrust and the warnings; then change the propeller pitch.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/rc-power/&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It opens on a 5-inch freestyle quad: four 2306 2,450 Kv motors, 5.1 × 4.3 × 3 props, a 4S
1,500 mAh pack, 650 g all up. Four kilograms of static thrust for a 650 g machine, a
thrust-to-weight of about 6, hovering at a little over a quarter throttle. Then the warnings: at
full throttle each motor is just over its current rating, and the pack is asked for more than
170 A, beyond what its C rating promises, sagging to around 3 V a cell.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Change &lt;strong&gt;Cells in series&lt;/strong&gt; to 6 without touching anything else. The motors spin half as fast
again and the current goes through the roof. This is why 6S quads use lower-Kv motors — the
1,950 Kv motor we started with is a 6S motor, and 2,450 Kv is a 4S one. Set Kv to 1,700 and
see things calm down.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Back on 4S, raise the prop pitch from 4.3 to 5. More thrust at speed, more current
everywhere. Look at &lt;strong&gt;g per W&lt;/strong&gt; in the hover column: that is your flight time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Switch the sweep chart to &lt;strong&gt;Efficiency&lt;/strong&gt;. A motor is most efficient well below full throttle,
which is happy news for a quad that spends most of its life hovering at 30%.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;putting-it-in-a-frame&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#putting-it-in-a-frame&quot;&gt;&lt;/a&gt;Putting it in a frame&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The last step is the whole aircraft. A quad is the motors, props, frame, flight controller,
ESCs, camera, video transmitter and battery, and every gram of those has to be lifted.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;fpv-quad&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:700px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading FPV quad: build, rates, CLI diff and blackbox…&lt;/p&gt;&lt;iframe src=&quot;/tools/fpv-quad/?bare=1&amp;amp;embed=1#build&quot; title=&quot;FPV quad: build, rates, CLI diff and blackbox (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: add the action camera (set its quantity to 1) and watch the all-up weight, thrust-to-weight and flight time below.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/fpv-quad/#build&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The template is a 5-inch freestyle build on 6S. Scroll below the parts list to &lt;strong&gt;Frame, power
and battery&lt;/strong&gt;, where the full-throttle thrust per motor goes in — from a maker&amp;#8217;s thrust table or
from the solver above — and the tool works out all-up weight, thrust-to-weight, hover throttle
and flight time, and checks the props clear each other on the frame.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Set the &lt;strong&gt;Action camera&lt;/strong&gt; quantity to 1. Seventy-five grams is a lot on a quad whose parts add up to 535 g: watch the
hover throttle rise and the flight time fall.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Swap the 1,300 mAh battery for a heavier 1,800 mAh one (about 270 g). Flight time goes up, but
by less than the capacity would suggest, because you are now carrying the extra cells too.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Make the &lt;strong&gt;Wheelbase&lt;/strong&gt; 200 mm with 5-inch props and look at the prop clearance: the discs nearly
touch.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;one-more-loop&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#one-more-loop&quot;&gt;&lt;/a&gt;One more loop&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There is a control loop at the heart of FOC, and another wrapped around the whole quad: the
flight controller compares the rotation rate the gyro measures with the rate your sticks ask
for, and adjusts the four motors&#39; throttle to close the gap. Both are PID controllers, and the
same few ideas govern both.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;pid&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading PID loop simulator…&lt;/p&gt;&lt;iframe src=&quot;/tools/pid/?bare=1&amp;amp;embed=1&quot; title=&quot;PID loop simulator (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: raise Kp until the response rings, then set the dead time to 0 and see the same gain settle cleanly.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/pid/&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The plant here is generic — a gain, a time constant and a delay — but the behaviour is the one
you tune on a quad.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Raise &lt;strong&gt;Kp&lt;/strong&gt; from 1.5 to 3. The response overshoots and rings. On a quad, that is the
oscillation you hear and see in the footage when the P term is too high.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set &lt;strong&gt;Dead time&lt;/strong&gt; to 0. With no delay, the same high gain settles cleanly. Delay is what makes
control hard — which is exactly why ESC latency, gyro filtering and the speed of DShot all
matter to how a quad flies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set &lt;strong&gt;Ti&lt;/strong&gt; blank (no integral) and look at the load disturbance at 50 s: the output never quite
comes back. On a quad, I is what holds the attitude against a steady gust or an
off-centre battery.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;what-to-take-away&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-to-take-away&quot;&gt;&lt;/a&gt;What to take away&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A brushless motor is a ring of magnets chasing a rotating magnetic field. The windings decide how
smooth and strong that chase is; the ESC decides where the field points. Six-step jumps the field
round in 60° steps, timed by Hall sensors or by listening for zero crossings in the back-EMF of
the floating phase. FOC measures the currents and rotates the field smoothly, exactly ahead of
the magnets. And once you have the motor, the propeller and the battery decide what it can
actually do, and a PID loop decides how well it is used.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;where-to-go-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#where-to-go-next&quot;&gt;&lt;/a&gt;Where to go next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/bldc-motor/&quot;&gt;Brushless motors&lt;/a&gt; — the full tool, with the &lt;strong&gt;Motor&lt;/strong&gt; tab&amp;#8217;s torque–speed
and efficiency maps, &lt;strong&gt;Measure it&lt;/strong&gt; for finding your own motor&amp;#8217;s Kv and resistance with a drill
and a meter, and the &lt;strong&gt;ESC&lt;/strong&gt; tab&amp;#8217;s MOSFET losses and bus capacitor ripple.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/rc-power/&quot;&gt;RC power setup&lt;/a&gt; — compare three motor, prop and battery setups side by
side, for planes as well as multirotors.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/fpv-quad/&quot;&gt;FPV quad&lt;/a&gt; — the build planner, Betaflight rates, a CLI diff and a
blackbox log viewer that plots the motor traces this page explained.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/pid/&quot;&gt;PID loop simulator&lt;/a&gt; — tuning rules and the controller as C code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/h-bridge/&quot;&gt;H-bridge&lt;/a&gt; — the brushed motor and single-phase bridge that the
three-phase inverter grows out of; &lt;a href=&quot;/tools/lipo/&quot;&gt;LiPo&lt;/a&gt; — the batteries on the other end of
the wires; &lt;a href=&quot;/tools/motion-drive/&quot;&gt;motion and drive&lt;/a&gt; — gearing a motor for a robot joint.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>How a database survives a crash</title>
    <link href="https://geekychris.github.io/lab/explorations/how-a-database-survives-a-crash/"/>
    <id>https://geekychris.github.io/lab/explorations/how-a-database-survives-a-crash/</id>
    <updated>2026-10-30T00:00:00Z</updated>
    <summary>Pull the plug on a database halfway through a write and it comes back with every committed transaction and none of the half-finished ones. How? Follow a write through the log, the buffer pool and the disk, compare B+trees with LSM-trees, watch compaction, and then see the other half of the promise — what concurrent transactions are allowed to see — with real PostgreSQL and SQLite running between the paragraphs.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here is a promise every serious database makes, and almost nobody stops to think about: once it
says &lt;code&gt;COMMIT&lt;/code&gt; succeeded, your data is safe. Not &quot;safe unless the power goes&quot;, not &quot;safe unless the
kernel panics in the next forty milliseconds&quot; — safe. And the transaction that was halfway through
when the lights went out? Gone, cleanly, as if it had never started.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;That is a strange thing to be able to promise. Disks write in blocks of 4 KB or so; a table and
its indexes are spread over thousands of those blocks; a single &lt;code&gt;UPDATE&lt;/code&gt; might touch a dozen of
them. The power can fail between any two. So how does the database come back up with every block
consistent?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The answer is one idea, applied relentlessly — &lt;strong&gt;write down what you are going to do before you do
it&lt;/strong&gt; — plus a lot of engineering to make that idea cheap. This exploration walks through it with the
site&amp;#8217;s database tools running live in the page. Nothing connects to a server: the PostgreSQL and
SQLite you will poke at are real engines compiled to WebAssembly, and the storage and isolation
simulators are models built to show the mechanisms.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-problem-pages-in-memory-pages-on-disk&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-problem-pages-in-memory-pages-on-disk&quot;&gt;&lt;/a&gt;The problem: pages in memory, pages on disk&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A database keeps its data in fixed-size &lt;strong&gt;pages&lt;/strong&gt; — 8 KB in PostgreSQL, 16 KB in InnoDB, 4 KB by
default in SQLite. To change a row it reads the page into memory, changes it there, and at some
point writes the page back.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Reading pages is slow, so databases keep a cache of them: the &lt;strong&gt;buffer pool&lt;/strong&gt;. A page that has been
changed in memory but not yet written back is &lt;strong&gt;dirty&lt;/strong&gt;. The buffer pool would love to keep dirty
pages around for as long as possible — if the same page is updated a hundred times, it only needs
writing once — but every dirty page is a change the disk does not know about yet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So we have a tension. For speed, we want to delay writing pages. For safety, we want every
committed change on disk before we say &quot;committed&quot;. Writing every touched page at every commit
would be correct and ruinously slow: random writes all over the disk, and a torn mess if the power
fails halfway through a page.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-write-ahead-log&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-write-ahead-log&quot;&gt;&lt;/a&gt;The write-ahead log&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The way out is the &lt;strong&gt;write-ahead log&lt;/strong&gt; (WAL). Before a page is allowed to change on disk, a record
describing the change is appended to a log — a single file that only ever grows at the end. At
commit, the database makes sure the log is on disk up to and including that transaction&amp;#8217;s commit
record (an &lt;code&gt;fsync&lt;/code&gt;), and then it says yes. The pages themselves can be written whenever it is
convenient.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Why is that better? Because appending to one file is sequential I/O, which even spinning disks do
quickly, and because a log record is small: &quot;on page 4117, slot 3, set column 2 to 150&quot; is tens of
bytes, not 8 KB. And if the machine dies, the log is the truth: replay it from the last known-good
point and every committed change is redone; a transaction with no commit record is undone or simply
ignored.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You can watch PostgreSQL&amp;#8217;s log grow. The console below is real PostgreSQL 18, running in your
browser through PGlite. &lt;code&gt;pg_current_wal_lsn()&lt;/code&gt; returns the &lt;strong&gt;log sequence number&lt;/strong&gt; — the byte
position at the end of the WAL — so the difference between two of them is how many bytes of log a
piece of work produced.&lt;/p&gt;
&lt;/div&gt;
&lt;style&gt;.tool-embed{margin:1.6rem 0}.tool-embed .te-frame{position:relative;height:var(--te-h,520px);max-height:90vh;border:1px solid var(--line,#d9d6d0);border-radius:10px;overflow:hidden;background:var(--wash,#f3efe9)}@media (max-width:640px){.tool-embed .te-frame{height:min(var(--te-h,520px),75vh);border-radius:8px}}.tool-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent}.tool-embed .te-loading{position:absolute;inset:0;margin:0;display:grid;place-items:center;color:var(--faint,#8a9096);font:.9rem var(--sans,system-ui,sans-serif)}.tool-embed .te-ready .te-loading{visibility:hidden}.tool-embed figcaption{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.25rem 1rem;margin-top:.45rem;font-size:.86rem;color:var(--muted,#5f646b)}.tool-embed .te-open{white-space:nowrap;font-weight:600;margin-left:auto}.tool-embed-missing{padding:.8rem 1rem;border:1px dashed var(--line,#d9d6d0);border-radius:8px;color:var(--muted,#5f646b)}&lt;/style&gt;&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;postgres&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Postgres playground…&lt;/p&gt;&lt;iframe src=&quot;/tools/postgres/?bare=1&amp;amp;embed=1&amp;amp;sql=CREATE%20TABLE%20t%28id%20int%20PRIMARY%20KEY%2C%20v%20text%29%3B%0ACREATE%20TABLE%20mark%28lsn%20pg_lsn%29%3B%0AINSERT%20INTO%20mark%20SELECT%20pg_current_wal_lsn%28%29%3B%0AINSERT%20INTO%20t%20SELECT%20g%2C%20md5%28g%3A%3Atext%29%20FROM%20generate_series%281%2C%2010000%29%20g%3B%0ASELECT%20pg_wal_lsn_diff%28pg_current_wal_lsn%28%29%2C%20lsn%29%20AS%20wal_bytes_for_10000_rows%20FROM%20mark%3B%0ACHECKPOINT%3B%0AUPDATE%20mark%20SET%20lsn%20%3D%20pg_current_wal_lsn%28%29%3B%0AUPDATE%20t%20SET%20v%20%3D%20%27first%20change%20since%20the%20checkpoint%27%20WHERE%20id%20%3D%201%3B%0ASELECT%20pg_wal_lsn_diff%28pg_current_wal_lsn%28%29%2C%20lsn%29%20AS%20wal_bytes_first_update%20FROM%20mark%3B%0AUPDATE%20mark%20SET%20lsn%20%3D%20pg_current_wal_lsn%28%29%3B%0AUPDATE%20t%20SET%20v%20%3D%20%27second%20change%2C%20same%20page%27%20WHERE%20id%20%3D%202%3B%0ASELECT%20pg_wal_lsn_diff%28pg_current_wal_lsn%28%29%2C%20lsn%29%20AS%20wal_bytes_second_update%20FROM%20mark%3B#console&quot; title=&quot;Postgres playground (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: scroll through the results and compare the three numbers, then change the 10000 and press Run.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/postgres/?sql=CREATE%20TABLE%20t%28id%20int%20PRIMARY%20KEY%2C%20v%20text%29%3B%0ACREATE%20TABLE%20mark%28lsn%20pg_lsn%29%3B%0AINSERT%20INTO%20mark%20SELECT%20pg_current_wal_lsn%28%29%3B%0AINSERT%20INTO%20t%20SELECT%20g%2C%20md5%28g%3A%3Atext%29%20FROM%20generate_series%281%2C%2010000%29%20g%3B%0ASELECT%20pg_wal_lsn_diff%28pg_current_wal_lsn%28%29%2C%20lsn%29%20AS%20wal_bytes_for_10000_rows%20FROM%20mark%3B%0ACHECKPOINT%3B%0AUPDATE%20mark%20SET%20lsn%20%3D%20pg_current_wal_lsn%28%29%3B%0AUPDATE%20t%20SET%20v%20%3D%20%27first%20change%20since%20the%20checkpoint%27%20WHERE%20id%20%3D%201%3B%0ASELECT%20pg_wal_lsn_diff%28pg_current_wal_lsn%28%29%2C%20lsn%29%20AS%20wal_bytes_first_update%20FROM%20mark%3B%0AUPDATE%20mark%20SET%20lsn%20%3D%20pg_current_wal_lsn%28%29%3B%0AUPDATE%20t%20SET%20v%20%3D%20%27second%20change%2C%20same%20page%27%20WHERE%20id%20%3D%202%3B%0ASELECT%20pg_wal_lsn_diff%28pg_current_wal_lsn%28%29%2C%20lsn%29%20AS%20wal_bytes_second_update%20FROM%20mark%3B#console&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The script measures three things. First, inserting 10,000 rows produces about 1.6 MB of log —
more than the table and its primary-key index occupy. That is the price of writing everything
twice: once to the log now, and once more to the table&amp;#8217;s pages later.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The second and third numbers are the interesting ones. After a &lt;code&gt;CHECKPOINT&lt;/code&gt;, the &lt;strong&gt;first&lt;/strong&gt; change to
a page writes far more log than the second: when I ran it, about 19,000 bytes against about 300.
Both updates change one short text value. Why the difference?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Try this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Press &lt;strong&gt;Reset&lt;/strong&gt; (so the tables are fresh), delete the &lt;code&gt;CHECKPOINT;&lt;/code&gt; line and press &lt;strong&gt;Run&lt;/strong&gt;. The first
update no longer pays the big price, because no checkpoint has happened since its page was last
logged.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reset&lt;/strong&gt; again, put the checkpoint back and change &lt;code&gt;WHERE id = 2&lt;/code&gt; to &lt;code&gt;WHERE id = 9000&lt;/code&gt;: a row on a
different page. Now the &quot;second&quot; update is also a first change to its page, and it costs about
16,000 bytes too.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;What you are seeing is &lt;strong&gt;full-page writes&lt;/strong&gt;, and it is about torn pages.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;torn-pages-and-checkpoints&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#torn-pages-and-checkpoints&quot;&gt;&lt;/a&gt;Torn pages and checkpoints&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A PostgreSQL page is 8 KB; the disk underneath may only guarantee that 512-byte or 4 KB sectors
are written atomically. If the power fails while a page is being written back, the page on disk can
be half old and half new — &lt;strong&gt;torn&lt;/strong&gt;. A log record that says &quot;set slot 3 to 150&quot; is no use against a
torn page: it describes a change to a page that no longer exists in any consistent form.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;PostgreSQL&amp;#8217;s answer is to put a complete copy of the page into the WAL the first time the page
changes after each &lt;strong&gt;checkpoint&lt;/strong&gt;. Recovery starts from that checkpoint, so for every page it touches
it first finds a whole, known-good image in the log, restores it, and then replays the small records
on top. The image costs about one page of log (your ~19,000 bytes were two or three page images:
the table page, the index page and the &lt;code&gt;mark&lt;/code&gt; table&amp;#8217;s page); every later change to the same page,
until the next checkpoint, is just the small record.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A &lt;strong&gt;checkpoint&lt;/strong&gt;, then, is the database saying: &quot;every dirty page that existed when I started has
now been written to disk, so recovery never needs to look further back than here.&quot; It bounds how
much log has to be kept and how long recovery takes, and it resets the full-page-write clock.
Checkpoints too often and you write page images constantly; too rarely and recovery after a crash
takes a long time. That is a real tuning knob (&lt;code&gt;checkpoint_timeout&lt;/code&gt;, &lt;code&gt;max_wal_size&lt;/code&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The storage-engines simulator draws all of this: the WAL, the buffer pool with its dirty pages, and
the disk, with a counter of every byte written. This view runs its update-heavy scenario on the
B+tree alone with full-page writes switched on.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;storage-engines&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:760px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Storage engines: LSM-tree vs B-tree, simulated…&lt;/p&gt;&lt;iframe src=&quot;/tools/storage-engines/?bare=1&amp;amp;embed=1&amp;amp;example=update-heavy&amp;amp;engine=btree&amp;amp;fpw=true#sim&quot; title=&quot;Storage engines: LSM-tree vs B-tree, simulated (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Start, then Next step twice. Watch the WAL and disk boxes and the write amplification counter.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/storage-engines/?example=update-heavy&amp;amp;engine=btree&amp;amp;fpw=true#sim&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Try this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;After step 2 (&lt;strong&gt;Skewed updates&lt;/strong&gt;) look at &lt;strong&gt;write amp.&lt;/strong&gt; — with no buffer pool, every update of a
116-byte entry writes a whole page, and full-page writes add page images to the log on top. It
lands in the high thirties.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Step 3 turns on a 128-page buffer pool. Hot pages now absorb many updates in memory and are
written once, at eviction or checkpoint. Watch the write-amplification chart bend downwards.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tap the WAL box or the buffer pool to inspect them; &lt;strong&gt;Checkpoint&lt;/strong&gt; is one of the actions. Dots on
pages show which are cached, orange ones are dirty.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is the whole bargain of a classical database engine: the buffer pool makes writes cheap by
delaying them, the WAL makes the delay safe, and checkpoints stop the WAL growing for ever.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;two-ways-to-organise-the-pages-btree-and-lsm-tree&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#two-ways-to-organise-the-pages-btree-and-lsm-tree&quot;&gt;&lt;/a&gt;Two ways to organise the pages: B+tree and LSM-tree&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So far the pages have been the leaves and branches of a &lt;strong&gt;B+tree&lt;/strong&gt;: one big sorted tree, updated in
place. PostgreSQL&amp;#8217;s indexes, InnoDB&amp;#8217;s tables and SQLite&amp;#8217;s files all work this way. Updating in
place is what makes the WAL and the torn-page dance necessary.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There is another family that takes the log idea to its logical conclusion. A &lt;strong&gt;log-structured
merge-tree&lt;/strong&gt; (LSM-tree) — RocksDB, LevelDB, Cassandra, ScyllaDB, the storage under many modern
systems — never updates a file on disk at all. A write goes to the WAL (for durability, exactly as
before) and into a sorted structure in memory, the &lt;strong&gt;memtable&lt;/strong&gt;. When the memtable is full it is
frozen and written out in one sequential pass as an immutable sorted file, an &lt;strong&gt;SSTable&lt;/strong&gt;. Its part of
the WAL can then be thrown away.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;storage-engines&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:720px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Storage engines: LSM-tree vs B-tree, simulated…&lt;/p&gt;&lt;iframe src=&quot;/tools/storage-engines/?bare=1&amp;amp;embed=1&amp;amp;example=lsm-flush#sim&quot; title=&quot;Storage engines: LSM-tree vs B-tree, simulated (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Start and step through all four steps; tap the memtable and an SSTable to inspect them.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/storage-engines/?example=lsm-flush#sim&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Try this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In step 1, notice that a write touches only two things: the WAL and the memtable. Nothing on disk
is modified in place, so there is no page to tear.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In step 2 the memtable fills (six entries here — millions in real life), becomes the &lt;strong&gt;immutable
memtable&lt;/strong&gt;, and is flushed to level 0. The WAL&amp;#8217;s live size drops: those records are no longer
needed for recovery, because the data is now in a file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In step 4, follow a lookup: memtable first, then the L0 files newest to oldest. A ✕ means a Bloom
filter said &quot;definitely not here&quot; and the file was skipped without a disk read.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Crash recovery in an LSM-tree is pleasingly simple. The SSTables are immutable and were fsynced
before they were recorded in the manifest, so they are either fully there or not there at all.
The only thing lost in a crash is the memtable — and the WAL holds exactly the writes that were in
it. Replay the WAL into a new memtable and you are back.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;compaction-paying-the-bill-later&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#compaction-paying-the-bill-later&quot;&gt;&lt;/a&gt;Compaction: paying the bill later&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;An LSM-tree buys fast writes by deferring work. Every flush adds another file, files overlap, and an
updated or deleted key may now have several versions scattered across them. Reads get slower the
more files there are, and old versions waste space. So, in the background, the engine &lt;strong&gt;compacts&lt;/strong&gt;:
it merge-sorts files together, keeps the newest version of each key, and writes fewer, bigger,
non-overlapping files further down.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;storage-engines&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:720px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Storage engines: LSM-tree vs B-tree, simulated…&lt;/p&gt;&lt;iframe src=&quot;/tools/storage-engines/?bare=1&amp;amp;embed=1&amp;amp;example=leveled#sim&quot; title=&quot;Storage engines: LSM-tree vs B-tree, simulated (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Start, then Next step. Watch L0 fill to four files and get merged into L1, and L1 spill into L2.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/storage-engines/?example=leveled#sim&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Try this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Watch the level labels: &lt;code&gt;30/32&lt;/code&gt; means L1 holds 30 entries against a target of 32. When a level
goes over its target, one of its files is merged with everything it overlaps in the level below.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Notice L0&amp;#8217;s files overlap each other (they span most of the key range), while L1 and L2 are tidy
rows of non-overlapping files. That is the payoff: below L0, a lookup reads at most one file per
level.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In step 3, look at the LSM&amp;#8217;s write amplification. Each entry is rewritten once per level it
passes through — the cost of keeping reads cheap.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Compaction also matters for crashes, in a quiet way: it, too, writes new files and only then
records them in the manifest and deletes the old ones. A crash mid-compaction leaves some orphaned
half-written output, which is simply ignored and deleted on restart. Immutable files make &quot;atomic&quot;
cheap.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you want to see the two families side by side, the simulator&amp;#8217;s &lt;strong&gt;Range scans favour the B+tree&lt;/strong&gt;
and &lt;strong&gt;Bloom filters&lt;/strong&gt; scenarios are good next stops; the trade-off is usually summarised as &lt;strong&gt;read,
write, space — pick two&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;sqlite-atomic-commit-in-a-single-file&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#sqlite-atomic-commit-in-a-single-file&quot;&gt;&lt;/a&gt;SQLite: atomic commit in a single file&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;SQLite is a useful contrast, because it has two complete answers to the crash problem and lets you
switch between them with one &lt;code&gt;PRAGMA&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the default &lt;strong&gt;rollback journal&lt;/strong&gt; mode, SQLite does the opposite of a WAL: before changing a page
in the database file, it copies the page&amp;#8217;s &lt;strong&gt;original&lt;/strong&gt; content into a separate &lt;code&gt;-journal&lt;/code&gt; file and
fsyncs that. Then it writes the new pages into the database. If the process dies, the next program
to open the database finds a &quot;hot&quot; journal and copies the old pages back. The database is rolled
back to the last commit. The commit itself is the moment the journal is deleted.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In &lt;strong&gt;WAL mode&lt;/strong&gt;, the database file is left alone during a transaction; new versions of pages are
appended to a &lt;code&gt;-wal&lt;/code&gt; file, readers look there first, and a checkpoint later copies them home. That
is the same shape as PostgreSQL&amp;#8217;s WAL, with whole pages as the records.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;sqlite-internals&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:700px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading SQLite internals…&lt;/p&gt;&lt;iframe src=&quot;/tools/sqlite-internals/?bare=1&amp;amp;embed=1&amp;amp;lab=journal#labs&quot; title=&quot;SQLite internals (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Run every step, then read the header table under each step — offsets 18, 19 and 24 are the ones that change.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/sqlite-internals/?lab=journal#labs&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Try this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Under step 1, find the &lt;strong&gt;file change counter&lt;/strong&gt; at offset 24. Every write transaction in rollback
mode bumps it; other processes use it to notice their cached pages are stale.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Under &lt;strong&gt;Switch to WAL&lt;/strong&gt;, bytes 18 and 19 change from 1 (legacy) to 2 (WAL). The journal mode is
stored in the file itself, so it persists.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open the &lt;strong&gt;Pages&lt;/strong&gt; tab afterwards and look at the map of the file: every page is accounted for,
and the header at the start of page 1 describes the whole database.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The lab is honest about a limit: one browser page cannot pull the plug on itself, so what you see
is the on-disk format the recovery logic relies on rather than a crash. But the mechanism is all
here — a page either has a known-good copy somewhere or it was never touched.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-other-half-of-the-promise-isolation&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-other-half-of-the-promise-isolation&quot;&gt;&lt;/a&gt;The other half of the promise: isolation&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Surviving a crash is the &lt;strong&gt;D&lt;/strong&gt; in ACID — durability — and its sibling &lt;strong&gt;A&lt;/strong&gt;, atomicity. But a database
that is merely durable can still hand your application nonsense, if two transactions run at the
same time and see each other&amp;#8217;s half-finished work. That is the &lt;strong&gt;I&lt;/strong&gt;: isolation.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The machinery is related. Modern databases keep several versions of a row (MVCC — multi-version
concurrency control), and each transaction reads from a &lt;strong&gt;snapshot&lt;/strong&gt;: the set of versions committed
when it started (or, at READ COMMITTED, when each statement started). Old versions are what the
undo log or the heap&amp;#8217;s dead tuples are for. The isolation level decides which snapshot you get and
what happens when two transactions want the same row.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The isolation lab steps two sessions through an interleaving, statement by statement, against
models of PostgreSQL and InnoDB that have been checked against the real servers.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;isolation-lab&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:720px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Isolation lab…&lt;/p&gt;&lt;iframe src=&quot;/tools/isolation-lab/?bare=1&amp;amp;embed=1&amp;amp;scenario=lost-update&amp;amp;engine=postgres&amp;amp;level=read%20committed#lab&quot; title=&quot;Isolation lab (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Step eight times, then change the isolation level to REPEATABLE READ and run it again.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/isolation-lab/?scenario=lost-update&amp;amp;engine=postgres&amp;amp;level=read%20committed#lab&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Try this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;At READ COMMITTED, both sessions read a balance of 100, A writes 150, B writes 125. B waits for
A&amp;#8217;s row lock, then overwrites. The banner says &lt;strong&gt;The anomaly happened&lt;/strong&gt;: one deposit is lost.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Switch to REPEATABLE READ. B&amp;#8217;s &lt;code&gt;UPDATE&lt;/code&gt; now fails with &lt;code&gt;40001 could not serialize access due to
concurrent update&lt;/code&gt;. The application has to retry from &lt;code&gt;BEGIN&lt;/code&gt; — and on the retry it reads 150.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Switch the engine to &lt;strong&gt;MySQL InnoDB&lt;/strong&gt; at REPEATABLE READ. The lost update happens. Same SQL
standard name, different behaviour; this is why the lab exists.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The lost update has well-known fixes — &lt;code&gt;SELECT … FOR UPDATE&lt;/code&gt;, an atomic
&lt;code&gt;SET balance = balance + 25&lt;/code&gt;, a version column — and they are all in the scenario list. The
nastier anomaly is the one no row lock can see.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;isolation-lab&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:720px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Isolation lab…&lt;/p&gt;&lt;iframe src=&quot;/tools/isolation-lab/?bare=1&amp;amp;embed=1&amp;amp;scenario=write-skew&amp;amp;engine=postgres&amp;amp;level=repeatable%20read#lab&quot; title=&quot;Isolation lab (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: step to the end at REPEATABLE READ, then switch to SERIALIZABLE and step through again.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/isolation-lab/?scenario=write-skew&amp;amp;engine=postgres&amp;amp;level=repeatable%20read#lab&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Try this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Alice and Bob are both on call for shift 1. Each checks that two doctors are on call, and each
takes themselves off. They update &lt;strong&gt;different rows&lt;/strong&gt;, so nothing conflicts and the snapshots are
perfectly consistent. At the end, nobody is on call — the &lt;strong&gt;Data&lt;/strong&gt; panel shows both &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At SERIALIZABLE, PostgreSQL&amp;#8217;s Serializable Snapshot Isolation tracks what each transaction read
as well as what it wrote. It spots the dangerous pattern and fails B&amp;#8217;s commit with &lt;code&gt;40001&lt;/code&gt;. Read
the explanation under &lt;strong&gt;Step 8&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try MySQL InnoDB at SERIALIZABLE: there, plain reads take shared locks, and the two updates
deadlock instead. Prevented, but by a different route.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Finally, the whole map at once. Every scenario, both engines, all four levels:&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;isolation-lab&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Isolation lab…&lt;/p&gt;&lt;iframe src=&quot;/tools/isolation-lab/?bare=1&amp;amp;embed=1#matrix&quot; title=&quot;Isolation lab (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: find the Write skew row and read across it; click any cell to step through that combination.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/isolation-lab/#matrix&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Try this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Red cells are anomalies that happen; dark green means the database stopped it with an error you
must retry; pale green means it was stopped by waiting or by the snapshot.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Compare the PostgreSQL RR column with the MySQL RR column. They share a name and very little
else.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Notice how much green at SERIALIZABLE is &lt;strong&gt;an error&lt;/strong&gt;. Serialisable isolation is not free safety:
it is safety in exchange for retry loops in your application.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;putting-it-together&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#putting-it-together&quot;&gt;&lt;/a&gt;Putting it together&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Follow one &lt;code&gt;UPDATE accounts SET balance = 175 WHERE id = 1; COMMIT;&lt;/code&gt; from start to finish:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;The transaction takes a snapshot and a row lock; the isolation level decides what it may see and
whether a concurrent writer makes it wait or fail.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The page holding the row is found in the buffer pool (or read into it) and changed in memory. It
is now dirty.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A WAL record describing the change is appended to the log buffer — with a full-page image if this
is the page&amp;#8217;s first change since the last checkpoint.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At &lt;code&gt;COMMIT&lt;/code&gt;, the log is fsynced up to the commit record. Only then does the client hear &quot;OK&quot;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Some time later — at eviction, or at the next checkpoint — the dirty page is written to its home
on disk.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If the power fails anywhere in that sequence, recovery starts at the last checkpoint, restores
page images, replays every logged change, and discards the work of transactions that never
committed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;An LSM-tree swaps steps 2 and 5 for a memtable and a flush, then spends its spare time compacting;
SQLite in rollback mode inverts the log and saves the old pages instead of the new ones. But every
one of them rests on the same rule: before you change the only copy of something, write down
enough to get back.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;where-to-go-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#where-to-go-next&quot;&gt;&lt;/a&gt;Where to go next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/storage-engines/&quot;&gt;Storage engines&lt;/a&gt; — all ten scenarios, including size-tiered
compaction&amp;#8217;s space spike, tombstones, and the &lt;strong&gt;Cost models&lt;/strong&gt; tab with the textbook formulas.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/postgres/&quot;&gt;Postgres playground&lt;/a&gt; — real PostgreSQL in the browser, with pattern labs for
queues, advisory locks, leases and an animated concurrency model.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/sqlite-internals/&quot;&gt;SQLite internals&lt;/a&gt; — the file decoded byte by byte, b-tree diagrams,
bytecode, and ten more labs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/isolation-lab/&quot;&gt;Isolation lab&lt;/a&gt; — every anomaly, the fixes, the Hermitage tests, and a
mode that runs the same steps on your own PostgreSQL server.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/consistency-lab/&quot;&gt;Consistency lab&lt;/a&gt; — the same questions once the database is
replicated: stale reads, session guarantees and linearisability.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/dist-transactions/&quot;&gt;Transactions across services&lt;/a&gt; — what you can do when one database&amp;#8217;s
WAL is no longer enough: two-phase commit, sagas and the outbox.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/articles/aoee-a-tao-style-social-graph-cache-in-rust/&quot;&gt;AOEE, a TAO-style social graph cache
in Rust&lt;/a&gt; — a cache in front of a database, and what that does to consistency.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>How a jet engine makes thrust</title>
    <link href="https://geekychris.github.io/lab/explorations/how-a-jet-engine-makes-thrust/"/>
    <id>https://geekychris.github.io/lab/explorations/how-a-jet-engine-makes-thrust/</id>
    <updated>2026-10-30T00:00:00Z</updated>
    <summary>Follow a parcel of air through a jet engine — squeezed, burned, expanded, thrown out of the back — with the cycle worked out live at every step, then see why airliner fans keep growing and why a car&#39;s turbocharger is a little gas turbine.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A jet engine looks like magic from the departure lounge: a tube that swallows air and pushes
a hundred tonnes of aeroplane to 900 km/h. Close up it is one of the most satisfying machines
to understand, because every part of it does one job you can name, and the whole thing reduces
to a single idea — &lt;strong&gt;throw air backwards faster than it arrived&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This exploration follows a parcel of air through an engine, from the intake to the nozzle. At
each stop there is a live view of the &lt;a href=&quot;/tools/jet-engine/&quot;&gt;jet engine tool&lt;/a&gt; working the
numbers out as you watch, and some things to try. The numbers are from a real cycle analysis,
the same method the propulsion textbooks use, so when you change something the engine answers
honestly. Near the end we shrink the whole thing down and bolt it to a car.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;thrust-is-momentum&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#thrust-is-momentum&quot;&gt;&lt;/a&gt;Thrust is momentum&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Newton&amp;#8217;s third law does all the work. If an engine takes in ṁ kilograms of air every second at
the aircraft&amp;#8217;s speed V&lt;sub&gt;0&lt;/sub&gt; and throws it out of the back at a jet speed V&lt;sub&gt;9&lt;/sub&gt;, the push on the
engine is&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;F = ṁ × (V~9~ − V~0~)&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;plus a small correction for the fuel&amp;#8217;s own mass and for any pressure left in the jet. That&amp;#8217;s
it. Everything else in this page is about &lt;em&gt;how&lt;/em&gt; to make V&lt;sub&gt;9&lt;/sub&gt; bigger than V&lt;sub&gt;0&lt;/sub&gt;, and how to do
it without wasting fuel.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There are two obvious ways to get more thrust: accelerate a little air a lot, or a lot of air
a little. Hold on to that thought; it decides the shape of every modern airliner engine, and
we&amp;#8217;ll come back to it.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-parts-in-order&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-parts-in-order&quot;&gt;&lt;/a&gt;The parts, in order&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here is the simplest engine there is: a turbojet, on a test bed, standing still.&lt;/p&gt;
&lt;/div&gt;
&lt;style&gt;.tool-embed{margin:1.6rem 0}.tool-embed .te-frame{position:relative;height:var(--te-h,520px);max-height:90vh;border:1px solid var(--line,#d9d6d0);border-radius:10px;overflow:hidden;background:var(--wash,#f3efe9)}@media (max-width:640px){.tool-embed .te-frame{height:min(var(--te-h,520px),75vh);border-radius:8px}}.tool-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent}.tool-embed .te-loading{position:absolute;inset:0;margin:0;display:grid;place-items:center;color:var(--faint,#8a9096);font:.9rem var(--sans,system-ui,sans-serif)}.tool-embed .te-ready .te-loading{visibility:hidden}.tool-embed figcaption{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.25rem 1rem;margin-top:.45rem;font-size:.86rem;color:var(--muted,#5f646b)}.tool-embed .te-open{white-space:nowrap;font-weight:600;margin-left:auto}.tool-embed-missing{padding:.8rem 1rem;border:1px dashed var(--line,#d9d6d0);border-radius:8px;color:var(--muted,#5f646b)}&lt;/style&gt;&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;jet-engine&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:620px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Jet engines and gas turbines…&lt;/p&gt;&lt;iframe src=&quot;/tools/jet-engine/?bare=1&amp;amp;embed=1&amp;amp;type=turbojet&amp;amp;ideal=false&amp;amp;M0=0&amp;amp;alt=0&amp;amp;opr=8&amp;amp;tt4=1200&amp;amp;mdot=20&amp;amp;noz=conv#cutaway&quot; title=&quot;Jet engines and gas turbines (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: click each part of the engine in turn, from the intake to the nozzle, and read the station table underneath.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/jet-engine/?type=turbojet&amp;amp;ideal=false&amp;amp;M0=0&amp;amp;alt=0&amp;amp;opr=8&amp;amp;tt4=1200&amp;amp;mdot=20&amp;amp;noz=conv#cutaway&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The dots are air. They come in on the left cool and blue, turn yellow as the compressor squeezes
them, glow red through the combustor, and leave on the right still hot. The circled numbers are
the &lt;strong&gt;station numbers&lt;/strong&gt; every engine maker uses, so that &quot;T&lt;sub&gt;4&lt;/sub&gt;&quot; means the same thing at Rolls-Royce
and at Pratt &amp;amp; Whitney:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;0&lt;/strong&gt; — the undisturbed air in front of the engine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;2&lt;/strong&gt; — the face of the compressor, after the intake has slowed the air down.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;3&lt;/strong&gt; — the compressor exit: the highest pressure in the engine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;4&lt;/strong&gt; — the turbine inlet, just after the combustor: the hottest point.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;5&lt;/strong&gt; — the turbine exit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;9&lt;/strong&gt; — the nozzle exit, where the jet leaves.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Scroll down to the station table. With this little engine (pressure ratio 8, turbine
inlet 1,200 K, 20 kg of air a second) the compressor takes air at 288 K and 101 kPa and delivers
it at about 558 K and 794 kPa. Nobody added any heat there — that 270 K rise is purely the work
of squeezing it, the same reason a bicycle pump gets warm. The combustor then takes it to 1,200 K
at almost the same pressure (a few per cent is lost to turbulence). The turbine drops it back to
about 968 K and 289 kPa, and the nozzle turns what is left into a jet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice.&lt;/strong&gt; The turbine takes out roughly 230 K of temperature, almost exactly what the
compressor put in (270 K of cold air needs a bit less hot gas to pay for it, because hot
gas carries more energy per kelvin). That is the deal at the heart of a gas turbine: &lt;em&gt;the
turbine exists to drive the compressor&lt;/em&gt;. Whatever energy is left after paying that bill is the
engine&amp;#8217;s profit, and it leaves as the jet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Click the compressor, then the turbine, and compare what the tool says about each.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-brayton-cycle&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-brayton-cycle&quot;&gt;&lt;/a&gt;The Brayton cycle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Strip the engine down to thermodynamics and it does four things to the air, over and over:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compress&lt;/strong&gt; it (0 → 3), adding work.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Heat&lt;/strong&gt; it at roughly constant pressure (3 → 4), by burning fuel.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Expand&lt;/strong&gt; it (4 → 9), through the turbine and then the nozzle, taking work out.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reject&lt;/strong&gt; the leftover heat to the atmosphere, as the hot jet mixes away behind the aircraft.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;That loop is the &lt;strong&gt;Brayton cycle&lt;/strong&gt;, and it is the same cycle as a power station&amp;#8217;s gas turbine,
just with the output taken as a jet instead of a spinning shaft. Here it is for an idealised
turbojet cruising at Mach 0.8 and 10 km, with perfect components:&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;jet-engine&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Jet engines and gas turbines…&lt;/p&gt;&lt;iframe src=&quot;/tools/jet-engine/?bare=1&amp;amp;embed=1&amp;amp;example=ideal-turbojet#cycle&quot; title=&quot;Jet engines and gas turbines (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: scroll to the T–s and P–v diagrams, then raise the overall pressure ratio from 10 to 30 and watch thermal efficiency.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/jet-engine/?example=ideal-turbojet#cycle&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;On the &lt;strong&gt;T–s diagram&lt;/strong&gt; (temperature against entropy) an ideal compressor and an ideal turbine are
vertical lines — no losses means no entropy made. Heating from 3 to 4 is the long curve along the
top; the dashed line along the bottom is heat thrown away to the atmosphere. The area enclosed is
the useful work. On the &lt;strong&gt;P–v diagram&lt;/strong&gt; the same loop looks like a cartoon of the engine: squeeze
the volume down from 0 to 3, swell it at constant pressure from 3 to 4, and let it expand all
the way out to 9.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Note the thermal efficiency, about 54 %. For the ideal cycle it depends on one thing
only: how much the air is squeezed before it is heated, counting the free squeeze it gets from
ramming into the intake at Mach 0.8. Put the pressure ratio up to 30, then 40. The efficiency
climbs every time.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice.&lt;/strong&gt; Now look at the other two efficiencies. &lt;strong&gt;Thermal efficiency&lt;/strong&gt; is how well the
engine turns fuel into jet kinetic energy. &lt;strong&gt;Propulsive efficiency&lt;/strong&gt; is how much of that kinetic
energy actually becomes useful push on the aeroplane — and at 35 % it is poor, because this jet
leaves at over 1,100 m/s while the aeroplane flies at 240. All the energy in a jet that is much
faster than the aircraft goes into stirring the air behind it. Multiply the two and you get
&lt;strong&gt;overall efficiency&lt;/strong&gt;: here 19 %. Hold on to that too.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Switch &lt;strong&gt;Cycle&lt;/strong&gt; to &lt;strong&gt;With losses&lt;/strong&gt;. The compressor and turbine lines on the T–s diagram lean to
the right, every efficiency drops, and the engine is suddenly a real one.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;why-not-just-squeeze-harder&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#why-not-just-squeeze-harder&quot;&gt;&lt;/a&gt;Why not just squeeze harder?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If more pressure ratio always helps efficiency, why did early engines stop at 5 or 8? Partly
because building compressors is hard (more on that below), but there is a subtler reason, and the
trade-study tab shows it.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;jet-engine&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:600px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Jet engines and gas turbines…&lt;/p&gt;&lt;iframe src=&quot;/tools/jet-engine/?bare=1&amp;amp;embed=1&amp;amp;type=turbojet&amp;amp;ideal=false&amp;amp;M0=0&amp;amp;alt=0&amp;amp;opr=8&amp;amp;tt4=1200&amp;amp;mdot=20&amp;amp;noz=conv&amp;amp;sweep=opr#sweeps&quot; title=&quot;Jet engines and gas turbines (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: compare the dashed (ideal) and solid (with losses) curves, then change the swept input to turbine inlet temperature.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/jet-engine/?type=turbojet&amp;amp;ideal=false&amp;amp;M0=0&amp;amp;alt=0&amp;amp;opr=8&amp;amp;tt4=1200&amp;amp;mdot=20&amp;amp;noz=conv&amp;amp;sweep=opr#sweeps&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The left-hand chart is &lt;strong&gt;specific thrust&lt;/strong&gt; — thrust per kilogram of air per second, which decides
how big an engine has to be for a given thrust. It rises steeply, peaks around a pressure ratio
of 10 for this engine, and then falls away. The reason is the ceiling on turbine inlet
temperature. Squeeze the air harder and it leaves the compressor hotter, so there is less room
left to add heat before you hit the 1,200 K the turbine blades can survive. Less heat added means
less energy for the jet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The right-hand chart is &lt;strong&gt;TSFC&lt;/strong&gt;, thrust-specific fuel consumption: grams of fuel per second per
kilonewton of thrust, the airline&amp;#8217;s favourite number. Lower is better. The ideal curve keeps
falling forever; the real one flattens out and eventually turns up, because each extra stage of
compression costs a little more in losses than it gains.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Switch the swept input to &lt;strong&gt;Turbine inlet temperature&lt;/strong&gt;. Specific thrust rises
steadily with it: hotter turbines mean smaller, lighter engines for the same thrust. This is why
turbine blades are single crystals of nickel superalloy, threaded with cooling passages and
coated in ceramic, running in gas hotter than their own melting point. Half the history of jet
engines is metallurgists buying another 50 K.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice.&lt;/strong&gt; On a turbojet, TSFC gets &lt;em&gt;worse&lt;/em&gt; as the turbine gets hotter. More heat makes a
faster jet, and a faster jet has worse propulsive efficiency. Keep that in mind for the turbofan.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-compressor-squeezing-air-uphill&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-compressor-squeezing-air-uphill&quot;&gt;&lt;/a&gt;The compressor: squeezing air uphill&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A compressor is the hardest part of the engine to get right, because it is pushing air from low
pressure to high — uphill, against the flow&amp;#8217;s own wish to go backwards. A turbine has it easy: gas
falling from high pressure to low pressure is happy to push blades round. That asymmetry is why a
modern engine might have ten or more compressor stages driven by just one or two turbine stages.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;jet-engine&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:700px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Jet engines and gas turbines…&lt;/p&gt;&lt;iframe src=&quot;/tools/jet-engine/?bare=1&amp;amp;embed=1&amp;amp;example=stage-50#compressor&quot; title=&quot;Jet engines and gas turbines (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: drag the tip of V1 or W2 in the velocity triangles and watch the de Haller number; then scroll to the compressor map and lower the operating speed.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/jet-engine/?example=stage-50#compressor&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The top table estimates how many stages a compressor needs for the core engine&amp;#8217;s pressure ratio,
using different rules of thumb. Older subsonic stages managed a pressure ratio of about 1.15 each;
transonic front stages manage 1.4 or more. Because the ratios multiply, a few better stages save a
lot of length and weight.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The &lt;strong&gt;velocity triangles&lt;/strong&gt; show what one stage does. The rotor blade moves at speed U; the air
arrives with velocity V1, which the blade sees as W1 relative to itself. The blade turns the
relative flow, the air leaves with more swirl, and that swirl — handed on to a stationary row of
stator vanes and slowed down — becomes pressure. The work done per kilogram is simply U times the
change in swirl.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Drag the tip of W2 to turn the flow more. Stage work and stage pressure ratio go up.
Keep going until the &lt;strong&gt;de Haller&lt;/strong&gt; number goes below 0.72. That is the warning that the relative
flow is being slowed down too much: the boundary layer on the blade can&amp;#8217;t cope and separates,
the blade stalls like an aeroplane wing at too high an angle, and the stage stops pumping.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now scroll to the &lt;strong&gt;compressor map&lt;/strong&gt;. Pressure ratio is up the side, mass flow along the bottom,
and the curved grey lines are speeds. The red line on the top-left is the &lt;strong&gt;surge line&lt;/strong&gt;: try to
make more pressure than that at too little flow and the whole compressor stalls at once. The
high-pressure air rushes back out of the front, often with a bang and a flame out of the intake.
The orange running line is where this simple engine actually operates at each speed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice.&lt;/strong&gt; Pull the operating speed down. The surge margin changes as the point slides
down the running line. Slam the throttle open and the combustor gets more fuel before the spool
has sped up: the operating point jumps upwards, towards surge. That is why engines have bleed
valves, variable stator vanes and fuel controls that are deliberately slow to answer the
throttle.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;combustion-where-the-energy-comes-from&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#combustion-where-the-energy-comes-from&quot;&gt;&lt;/a&gt;Combustion: where the energy comes from&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Between stations 3 and 4, fuel is sprayed into the compressed air and burned. The combustor is
remarkable mostly for what it doesn&amp;#8217;t do: it has to keep a stable flame in air rushing through
at tens of metres a second, without letting the flame touch the walls or the turbine, and it
uses only about a quarter of the air for burning. The rest goes round the outside of the flame
tube and is mixed back in to cool the gas down to something the turbine can survive. Overall,
a jet engine runs very lean: a fuel–air ratio around 0.02, where a car engine runs near 0.068.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The energy all comes from chemistry. Kerosene isn&amp;#8217;t in the thermochemistry tool&amp;#8217;s table, but
natural gas is, and natural gas is what the gas turbines in power stations actually burn:&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;thermochem&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Thermochemistry…&lt;/p&gt;&lt;iframe src=&quot;/tools/thermochem/?bare=1&amp;amp;embed=1&amp;amp;rxn=CH4%20%2B%202%20O2%20-%3E%20CO2%20%2B%202%20H2O&quot; title=&quot;Thermochemistry (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: change H2O to H2O(g) and see how much less heat you get when the water leaves as steam.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/thermochem/?rxn=CH4%20%2B%202%20O2%20-%3E%20CO2%20%2B%202%20H2O&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; The tool says burning one mole of methane releases about 891 kJ. Now edit the
equation so the water is &lt;code&gt;H2O(g)&lt;/code&gt; and the figure drops to about 803 kJ. The difference is the
latent heat of the water. In an engine the water leaves as steam in the exhaust, so engineers use
the lower figure — the &lt;strong&gt;lower heating value&lt;/strong&gt;. Divide 803 kJ by methane&amp;#8217;s 16 g per mole and you get
about 50 MJ/kg. Kerosene gives about 43 MJ/kg, and that 43 is built into the jet engine tool&amp;#8217;s
burner sums.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice.&lt;/strong&gt; A modern airliner engine at cruise turns only around a third of that 43 MJ/kg
into useful work on the aircraft. Where does the rest go? Mostly out of the back as heat, the
dashed line on the T–s diagram — the price the second law of thermodynamics charges any heat
engine.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-turbine-and-the-nozzle&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-turbine-and-the-nozzle&quot;&gt;&lt;/a&gt;The turbine and the nozzle&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After the combustor, the gas passes through the turbine, which takes out just enough energy to
drive the compressor (and in a turbofan, the fan). In our test-bed turbojet that left about 289 kPa
and 968 K at the turbine exit — gas that is still under pressure and still hot, which is exactly
what the nozzle needs.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The nozzle turns pressure into speed. A simple convergent nozzle like the one on our test-bed
engine can only accelerate the gas to the speed of sound; past that the flow &lt;strong&gt;chokes&lt;/strong&gt;. That&amp;#8217;s
what the tool reports on its Cycle tab — a core jet of 563 m/s at Mach 1.00, &quot;choked (pressure
thrust)&quot; — with the gas leaving at about 153 kPa, well above the 101 kPa around it. The extra
pressure pushing on the nozzle exit area is counted as &lt;strong&gt;pressure thrust&lt;/strong&gt;. A
convergent–divergent nozzle (the &quot;Con-di&quot; option on the Cycle tab) expands the gas fully and turns
that pressure into extra velocity instead, which is why supersonic aircraft have nozzles whose
shape changes in flight.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Another trick in the nozzle is the afterburner, or &lt;strong&gt;reheat&lt;/strong&gt;: spraying fuel into the hot exhaust
after the turbine and burning it again. With no turbine downstream to melt, the gas can go far
hotter. Thrust jumps by something like half, and fuel consumption by far more, which is why
fighters use it for take-off and combat and not to get anywhere. Open the
&lt;a href=&quot;/tools/jet-engine/?type=abjet&amp;amp;ideal=false&amp;amp;M0=2&amp;amp;alt=11000&amp;amp;opr=13.5&amp;amp;tt4=1250&amp;amp;tt7=2000&amp;amp;mdot=75&amp;amp;noz=ideal#cutaway&quot;&gt;afterburning
turbojet&lt;/a&gt; on the cutaway to see where it sits: a second burner between the turbine exit (5) and the
nozzle (7 to 9).&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a-lot-of-air-a-little-faster-the-turbofan&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#a-lot-of-air-a-little-faster-the-turbofan&quot;&gt;&lt;/a&gt;A lot of air, a little faster: the turbofan&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Back to the choice we put off: accelerate a little air a lot, or a lot of air a little. Our
turbojet chose the first, and its propulsive efficiency suffered. A &lt;strong&gt;turbofan&lt;/strong&gt; does the second.
It puts a large fan at the front, driven by an extra turbine, and blows most of the air around
the core in a &lt;strong&gt;bypass duct&lt;/strong&gt;, accelerating it only gently.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;jet-engine&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:620px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Jet engines and gas turbines…&lt;/p&gt;&lt;iframe src=&quot;/tools/jet-engine/?bare=1&amp;amp;embed=1&amp;amp;type=turbofan&amp;amp;ideal=false&amp;amp;M0=0.78&amp;amp;alt=10700&amp;amp;opr=45&amp;amp;tt4=1600&amp;amp;bpr=12&amp;amp;fpr=1.4&amp;amp;mdot=230#cutaway&quot; title=&quot;Jet engines and gas turbines (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: compare the size of the bypass stream (stations 13 and 19) with the core, then open the Cycle tab and read the propulsive efficiency.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/jet-engine/?type=turbofan&amp;amp;ideal=false&amp;amp;M0=0.78&amp;amp;alt=10700&amp;amp;opr=45&amp;amp;tt4=1600&amp;amp;bpr=12&amp;amp;fpr=1.4&amp;amp;mdot=230#cutaway&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is a modern geared turbofan at cruise: for every kilogram of air that goes through the
core, twelve go around it. That is the &lt;strong&gt;bypass ratio&lt;/strong&gt;. The bypass stream has its own station
numbers, 13 after the fan and 19 at its own nozzle, and you can see the blue, cool bypass air
flowing round the outside while the core burns down the middle.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Open the Cycle tab. Propulsive efficiency is now about 70 %, against about 40 % for
our little turbojet flying at the same speed and height. Overall efficiency roughly doubles, from
around 15 % to around 31 %, and TSFC falls from about 35 to 17 g/(kN·s). Half the fuel for the
same thrust — that&amp;#8217;s the whole story of airliner engines since the 1960s.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice.&lt;/strong&gt; Specific thrust has collapsed, to about 113 N·s/kg. The engine moves so much air
so gently that it needs an enormous fan to make useful thrust, which is why the engines on a
modern twin look like they barely fit under the wing. Switch the engine to &lt;strong&gt;Turbofan (mixed
exhaust)&lt;/strong&gt; to see the low-bypass fighter arrangement, where the two streams merge before a single
nozzle.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;How far can you push it? The trade study answers:&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;jet-engine&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Jet engines and gas turbines…&lt;/p&gt;&lt;iframe src=&quot;/tools/jet-engine/?bare=1&amp;amp;embed=1&amp;amp;example=bypass-trade#sweeps&quot; title=&quot;Jet engines and gas turbines (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: follow the solid TSFC curve as the bypass ratio rises past 12, then sweep the fan pressure ratio instead.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/jet-engine/?example=bypass-trade#sweeps&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;As bypass ratio rises, TSFC falls and so does specific thrust — the engine gets more economical
and bigger. Keep going and something odd happens around a bypass ratio of 12: the curves kink.
The core can only supply so much power, and past that point the fan is asking for more than the
turbine can give at this fan pressure ratio. The tool&amp;#8217;s note says it plainly: &quot;Past a point the
fan asks more than the core can give.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Sweep &lt;strong&gt;Fan pressure ratio&lt;/strong&gt; instead. A lower fan pressure ratio means a gentler, more
efficient bypass jet, but you need even more air (and an even bigger fan) for the same thrust.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice.&lt;/strong&gt; The fan tip can&amp;#8217;t go much faster than the speed of sound, and a huge fan has
to turn slowly while the turbine that drives it wants to spin fast. Squaring that is why the
newest engines put a gearbox between the fan and its turbine. The limit after that is the
aircraft itself: a fan four metres across has to fit under a wing and clear the ground. Open
rotors, with no duct at all, are the next step in the same direction.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-same-machine-on-a-car-the-turbocharger&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-same-machine-on-a-car-the-turbocharger&quot;&gt;&lt;/a&gt;The same machine on a car: the turbocharger&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A turbocharger is a gas turbine with one important substitution: the car&amp;#8217;s engine is the
combustor. Exhaust gas from the cylinders spins a small turbine, the turbine spins a compressor
on the same shaft, and the compressor pushes extra air into the cylinders, so they can burn more
fuel per stroke. It has stations, a pressure ratio, a turbine paying for a compressor, and the
same kind of compressor map with the same surge line.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;turbocharger&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:680px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Turbocharger matching…&lt;/p&gt;&lt;iframe src=&quot;/tools/turbocharger/?bare=1&amp;amp;embed=1&amp;amp;example=surge-spool#match&quot; title=&quot;Turbocharger matching (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: find the red points left of the surge line, then raise &amp;#39;Boost starts&amp;#39; and &amp;#39;Full boost from&amp;#39; until they move clear.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/turbocharger/?example=surge-spool#match&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is a 2-litre four-cylinder engine with a compressor that is too big for it. Each dot on the
map is the engine at full throttle at one rpm, from where boost begins to peak power. At high rpm
the points sit in the middle of the efficiency islands, which is where you want them. But while
the boost is building, between about 1,800 and 3,000 rpm, the engine isn&amp;#8217;t swallowing much air
yet and the compressor is being asked for a high pressure ratio at a low flow — up and to the
left, past the surge line. The tool flags it in red.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Raise &lt;strong&gt;Boost starts&lt;/strong&gt; and &lt;strong&gt;Full boost from&lt;/strong&gt; so the boost arrives later. The points
slide right and clear the surge line. Or lower the target boost. Either way you&amp;#8217;re doing what a
jet engine&amp;#8217;s fuel control does when it refuses to let you slam the throttle open: keeping the
compressor on the safe side of the map.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice.&lt;/strong&gt; Look at the &lt;strong&gt;compressor outlet&lt;/strong&gt; temperature, about 135 °C at a pressure ratio of
2.3. That&amp;#8217;s the same heat of compression as station 3 in the turbojet, and it is why turbo cars
have an intercooler, to cool the air back down before it reaches the cylinders. The &lt;strong&gt;Turbine &amp;amp;
wastegate&lt;/strong&gt; tab balances the turbine against the compressor, exactly as the jet engine balances
station 4 to 5 against 2 to 3.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;what-weve-seen&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-weve-seen&quot;&gt;&lt;/a&gt;What we&amp;#8217;ve seen&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Follow a parcel of air through a jet engine and every step makes sense. The compressor squeezes
it and heats it up doing so. The combustor heats it more. The turbine takes back just enough to
keep the compressor turning, and the nozzle turns the rest into speed. The Brayton cycle says
squeezing harder makes the engine more efficient, and hot turbines make it smaller. Propulsive
efficiency says that the best way to push an aeroplane is a lot of air, a little faster. That
last idea grew the fan from nothing to four metres across.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;where-to-go-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#where-to-go-next&quot;&gt;&lt;/a&gt;Where to go next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/jet-engine/&quot;&gt;Jet engines and gas turbines&lt;/a&gt;: the full tool. Try the &lt;strong&gt;Real engines&lt;/strong&gt;
tab, which loads approximate figures for real engine families into the cycle, and &lt;strong&gt;Model jets&lt;/strong&gt;,
for the fuel and run time of a KJ66-class model turbine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/jet-engine/?example=turboprop#cycle&quot;&gt;A turboprop&lt;/a&gt; and
&lt;a href=&quot;/tools/jet-engine/?example=ramjet-m3#cycle&quot;&gt;a ramjet at Mach 3&lt;/a&gt;: the same cycle with the
output taken as a propeller, or with no compressor at all because the speed does the squeezing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/turbocharger/&quot;&gt;Turbocharger matching&lt;/a&gt;: the full tool, with fuel, the turbine side
and the intercooler.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/thermochem/&quot;&gt;Thermochemistry&lt;/a&gt;: heats of combustion, and why some fuels give more
energy per kilogram than others.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/rocketry/&quot;&gt;Model rocketry&lt;/a&gt;: thrust without an intake. A rocket carries its oxidiser,
so there is no compressor, no turbine and no ram drag.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/e6b/&quot;&gt;E6B pilot calculators&lt;/a&gt;: the standard atmosphere, true airspeed and Mach number
behind the flight condition.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/fluids/&quot;&gt;Fluids&lt;/a&gt;: Reynolds number, drag and pipe flow, for the air around and through
the engine.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>How the Amiga drew Boing</title>
    <link href="https://geekychris.github.io/lab/explorations/how-the-amiga-drew-boing/"/>
    <id>https://geekychris.github.io/lab/explorations/how-the-amiga-drew-boing/</id>
    <updated>2026-10-30T00:00:00Z</updated>
    <summary>A red and white ball bouncing across a purple grid sold a computer. Take it apart with live tools — the copper and the beam, colour cycling, sprites and blitter objects, the cookie-cut blit, the fight for chip RAM, and Paula&#39;s boom — and see how a 7 MHz machine made it look effortless.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In January 1984, on a prototype that was still mostly breadboards, Commodore&amp;#8217;s soon-to-be
Amiga team showed a checkered ball bouncing across a grid. It spun as it went, it had a shadow,
and every time it hit a wall the room went &lt;em&gt;boom&lt;/em&gt;. Personal computers of the day could barely
scroll a screen of text smoothly. This one was throwing a big, colourful object around at full
frame rate while the processor, by most accounts, had very little to do.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;That last part is the interesting bit. Boing was not a triumph of raw speed — the 68000 at the
heart of the machine ran at about 7 MHz. It was a demonstration of &lt;em&gt;delegation&lt;/em&gt;: a set of custom
chips each doing one job, very fast, in step with the television beam. This exploration takes the
demo apart one chip at a time. Every tool below is live; poke it as you read.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-thing-we-are-taking-apart&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-thing-we-are-taking-apart&quot;&gt;&lt;/a&gt;The thing we are taking apart&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here is a modern reconstruction, computed per pixel on your graphics card. It is cheating, in a
sense — a GPU does billions of calculations a second — but it gives us the target.&lt;/p&gt;
&lt;/div&gt;
&lt;style&gt;.tool-embed{margin:1.6rem 0}.tool-embed .te-frame{position:relative;height:var(--te-h,520px);max-height:90vh;border:1px solid var(--line,#d9d6d0);border-radius:10px;overflow:hidden;background:var(--wash,#f3efe9)}@media (max-width:640px){.tool-embed .te-frame{height:min(var(--te-h,520px),75vh);border-radius:8px}}.tool-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent}.tool-embed .te-loading{position:absolute;inset:0;margin:0;display:grid;place-items:center;color:var(--faint,#8a9096);font:.9rem var(--sans,system-ui,sans-serif)}.tool-embed .te-ready .te-loading{visibility:hidden}.tool-embed figcaption{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.25rem 1rem;margin-top:.45rem;font-size:.86rem;color:var(--muted,#5f646b)}.tool-embed .te-open{white-space:nowrap;font-weight:600;margin-left:auto}.tool-embed-missing{padding:.8rem 1rem;border:1px dashed var(--line,#d9d6d0);border-radius:8px;color:var(--muted,#5f646b)}&lt;/style&gt;&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;shader&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:520px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Shader playground…&lt;/p&gt;&lt;iframe src=&quot;/tools/shader/?bare=1&amp;amp;embed=1&amp;amp;ex=boing&quot; title=&quot;Shader playground (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: drag the uSpin slider to 0, then to −3. Drag uBounce up to 2.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/shader/?ex=boing&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Set &lt;strong&gt;uSpin&lt;/strong&gt; to zero and the ball slides rather than rolls; set it negative and it
spins the wrong way, which looks instantly wrong to the eye. Push &lt;strong&gt;uBounce&lt;/strong&gt; up and watch the
arcs: the motion is just &lt;code&gt;abs(sin(t))&lt;/code&gt;, a string of parabola-like hops with a sharp kiss at the
floor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Notice&lt;/strong&gt; the four ingredients, because each one maps onto a different part of the real Amiga:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;olist arabic&quot;&gt;
&lt;ol class=&quot;arabic&quot;&gt;
&lt;li&gt;
&lt;p&gt;a &lt;strong&gt;background&lt;/strong&gt; — the grid and the floor, which never changes;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a &lt;strong&gt;ball&lt;/strong&gt; that moves around on top of it;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a &lt;strong&gt;spin&lt;/strong&gt; that makes the ball look like a rolling sphere;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a &lt;strong&gt;shadow&lt;/strong&gt;, and (not here) a &lt;strong&gt;sound&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The shader recomputes every pixel, every frame. The Amiga could not afford anything like that. Its
trick was to avoid redrawing almost everything.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-beam-and-the-coprocessor-that-rides-it&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-beam-and-the-coprocessor-that-rides-it&quot;&gt;&lt;/a&gt;The beam, and the coprocessor that rides it&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A 1980s television draws its picture with an electron beam, left to right, top to bottom, fifty
times a second in PAL countries and sixty in NTSC ones. The Amiga&amp;#8217;s display hardware is built
around that beam. It fetches bitplane data just in time for the beam to paint it, and it keeps a
little coprocessor, the &lt;strong&gt;copper&lt;/strong&gt;, watching where the beam is.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The copper has only three instructions. &lt;code&gt;WAIT&lt;/code&gt; stops it until the beam reaches a given line and
horizontal position. &lt;code&gt;MOVE&lt;/code&gt; writes a value into one of the custom chips&#39; registers. &lt;code&gt;SKIP&lt;/code&gt; jumps
over the next instruction if the beam is already past a point. That is all — and it is enough to
change the hardware&amp;#8217;s mind halfway down the screen, every frame, without the processor lifting a
finger.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;copper&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:600px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Copper and blitter playground…&lt;/p&gt;&lt;iframe src=&quot;/tools/copper/?bare=1&amp;amp;embed=1&amp;amp;src=%3B%20WAIT%20holds%20the%20copper%20until%20the%20beam%20reaches%20line%20v%2C%20clock%20h.%0A%3B%20MOVE%20writes%20one%20chip%20register.%20Press%20Play.%0AMOVE%20BPLCON0%2C%240200%0AWAIT%20%2440%2C%2407%0AMOVE%20COLOR00%2C%24006%0AWAIT%20%2460%2C%2407%0AMOVE%20COLOR00%2C%2400A%0AWAIT%20%7B0x80%2Bround%2824%2Asin%28t%2F20%29%29%7D%2C%2407%0AMOVE%20COLOR00%2C%24F80%0AWAIT%20%7B0x84%2Bround%2824%2Asin%28t%2F20%29%29%7D%2C%2407%0AMOVE%20COLOR00%2C%2400A%0AWAIT%20%24C0%2C%2407%0AMOVE%20COLOR00%2C%24040%0AWAIT%20%24E0%2C%2407%0AMOVE%20COLOR00%2C%24000%0AEND#copper&quot; title=&quot;Copper and blitter playground (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Play, then change a WAIT line or a colour value and watch the picture follow.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/copper/?src=%3B%20WAIT%20holds%20the%20copper%20until%20the%20beam%20reaches%20line%20v%2C%20clock%20h.%0A%3B%20MOVE%20writes%20one%20chip%20register.%20Press%20Play.%0AMOVE%20BPLCON0%2C%240200%0AWAIT%20%2440%2C%2407%0AMOVE%20COLOR00%2C%24006%0AWAIT%20%2460%2C%2407%0AMOVE%20COLOR00%2C%2400A%0AWAIT%20%7B0x80%2Bround%2824%2Asin%28t%2F20%29%29%7D%2C%2407%0AMOVE%20COLOR00%2C%24F80%0AWAIT%20%7B0x84%2Bround%2824%2Asin%28t%2F20%29%29%7D%2C%2407%0AMOVE%20COLOR00%2C%2400A%0AWAIT%20%24C0%2C%2407%0AMOVE%20COLOR00%2C%24040%0AWAIT%20%24E0%2C%2407%0AMOVE%20COLOR00%2C%24000%0AEND#copper&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Press &lt;strong&gt;Play&lt;/strong&gt;: the orange bar rides up and down because its &lt;code&gt;WAIT&lt;/code&gt; line is an
expression of the frame number &lt;code&gt;t&lt;/code&gt;. Change the first &lt;code&gt;$00A&lt;/code&gt; to &lt;code&gt;$A0A&lt;/code&gt; and the band above the bar turns
purple. Move over the picture to read the beam position, and click to copy the &lt;code&gt;WAIT&lt;/code&gt; for that
spot. Then pick &lt;strong&gt;Bouncing raster bars&lt;/strong&gt; from the preset menu for the classic demo-scene effect.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Notice&lt;/strong&gt; the line under the editor: fourteen instructions cost 56 bytes of chip memory. Every
colour on this picture is the result of the copper writing one 16-bit register, &lt;code&gt;COLOR00&lt;/code&gt;, at the
right moment. Nothing in memory looks like a picture; the picture exists only as a series of
timed register writes. Boing&amp;#8217;s purple grid and its grey floor were an ordinary bitmap, drawn once,
but the copper is still what loads the palette at the top of every frame — the operating system
builds a copper list from each screen&amp;#8217;s colour table. That matters for the next trick.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-spin-is-a-lie-colour-cycling&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-spin-is-a-lie-colour-cycling&quot;&gt;&lt;/a&gt;The spin is a lie: colour cycling&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Rotating a checkered sphere properly means redrawing every pixel of it each frame. On a 7 MHz
processor, for a ball that size, that was out of the question. So Boing&amp;#8217;s ball was drawn once.
Each checker on it was painted with a &lt;em&gt;different&lt;/em&gt; palette index, arranged so that, going round
the ball, the indices repeat in sequence. To make it spin, you do not touch the ball at all: you
change which of those indices are red and which are white. Shift the red-and-white pattern along
the palette by one entry each frame and the stripes appear to march round the sphere.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is &lt;strong&gt;colour cycling&lt;/strong&gt;, and the copper makes it nearly free, because the palette is loaded from
the copper list every frame anyway. Here is the idea reduced to its bones: sixteen bands, each
given its colour by a single &lt;code&gt;MOVE&lt;/code&gt;, with the pattern shifting every eight frames.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;copper&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:600px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Copper and blitter playground…&lt;/p&gt;&lt;iframe src=&quot;/tools/copper/?bare=1&amp;amp;embed=1&amp;amp;src=%3B%20Colour%20cycling%3A%20the%20bands%20never%20move.%0A%3B%20Only%20the%20colour%20each%20band%20is%20given%20changes.%20Press%20Play.%0AMOVE%20BPLCON0%2C%240200%0AWAIT%20%2440%2C%2407%0AMOVE%20COLOR00%2C%7B%280%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2448%2C%2407%0AMOVE%20COLOR00%2C%7B%281%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2450%2C%2407%0AMOVE%20COLOR00%2C%7B%282%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2458%2C%2407%0AMOVE%20COLOR00%2C%7B%283%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2460%2C%2407%0AMOVE%20COLOR00%2C%7B%284%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2468%2C%2407%0AMOVE%20COLOR00%2C%7B%285%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2470%2C%2407%0AMOVE%20COLOR00%2C%7B%286%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2478%2C%2407%0AMOVE%20COLOR00%2C%7B%287%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2480%2C%2407%0AMOVE%20COLOR00%2C%7B%288%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2488%2C%2407%0AMOVE%20COLOR00%2C%7B%289%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2490%2C%2407%0AMOVE%20COLOR00%2C%7B%2810%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2498%2C%2407%0AMOVE%20COLOR00%2C%7B%2811%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24a0%2C%2407%0AMOVE%20COLOR00%2C%7B%2812%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24a8%2C%2407%0AMOVE%20COLOR00%2C%7B%2813%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24b0%2C%2407%0AMOVE%20COLOR00%2C%7B%2814%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24b8%2C%2407%0AMOVE%20COLOR00%2C%7B%2815%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24C0%2C%2407%0AMOVE%20COLOR00%2C%24000%0AEND#copper&quot; title=&quot;Copper and blitter playground (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Play, then change t&amp;gt;&amp;gt;3 to t&amp;gt;&amp;gt;1 in a few lines, or swap rgb(15,1,1) for rgb(0,8,15).&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/copper/?src=%3B%20Colour%20cycling%3A%20the%20bands%20never%20move.%0A%3B%20Only%20the%20colour%20each%20band%20is%20given%20changes.%20Press%20Play.%0AMOVE%20BPLCON0%2C%240200%0AWAIT%20%2440%2C%2407%0AMOVE%20COLOR00%2C%7B%280%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2448%2C%2407%0AMOVE%20COLOR00%2C%7B%281%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2450%2C%2407%0AMOVE%20COLOR00%2C%7B%282%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2458%2C%2407%0AMOVE%20COLOR00%2C%7B%283%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2460%2C%2407%0AMOVE%20COLOR00%2C%7B%284%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2468%2C%2407%0AMOVE%20COLOR00%2C%7B%285%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2470%2C%2407%0AMOVE%20COLOR00%2C%7B%286%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2478%2C%2407%0AMOVE%20COLOR00%2C%7B%287%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2480%2C%2407%0AMOVE%20COLOR00%2C%7B%288%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2488%2C%2407%0AMOVE%20COLOR00%2C%7B%289%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2490%2C%2407%0AMOVE%20COLOR00%2C%7B%2810%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%2498%2C%2407%0AMOVE%20COLOR00%2C%7B%2811%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24a0%2C%2407%0AMOVE%20COLOR00%2C%7B%2812%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24a8%2C%2407%0AMOVE%20COLOR00%2C%7B%2813%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24b0%2C%2407%0AMOVE%20COLOR00%2C%7B%2814%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24b8%2C%2407%0AMOVE%20COLOR00%2C%7B%2815%2B%28t%3E%3E3%29%29%254%3C2%20%3F%20rgb%2815%2C1%2C1%29%20%3A%20rgb%2815%2C15%2C15%29%7D%0AWAIT%20%24C0%2C%2407%0AMOVE%20COLOR00%2C%24000%0AEND#copper&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Press &lt;strong&gt;Play&lt;/strong&gt; and watch the stripes march down the screen. Nothing is moving in
memory: each band is still at the line its &lt;code&gt;WAIT&lt;/code&gt; names. Change &lt;code&gt;t&amp;gt;&amp;gt;3&lt;/code&gt; to &lt;code&gt;t&amp;gt;&amp;gt;1&lt;/code&gt; in one line and
that band runs four times faster than its neighbours, breaking the illusion. Change the colours to
see how little it takes to restyle the whole thing.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Notice&lt;/strong&gt; the cost: one register write per band per frame. Boing&amp;#8217;s real ball worked through a
handful of palette entries rather than copper bands, but the principle is the same one — move the
&lt;em&gt;colours&lt;/em&gt;, not the &lt;em&gt;pixels&lt;/em&gt;. It is the reason the ball could spin smoothly while the processor did
almost nothing. (The shader at the top cheats the other way: it really does recompute the sphere,
and its spin is honest maths.)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;moving-a-ball-sprites-or-bobs&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#moving-a-ball-sprites-or-bobs&quot;&gt;&lt;/a&gt;Moving a ball: sprites or bobs?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The spin is free. The bounce is not, because the ball&amp;#8217;s pixels genuinely have to appear in a
different place each frame. The Amiga offers three ways to move a picture around: hardware
&lt;strong&gt;sprites&lt;/strong&gt;, which the display overlays on the screen without touching the bitmap at all; &lt;strong&gt;blitter
objects&lt;/strong&gt; (BOBs), which are copied into the bitmap; and moving or scrolling the playfield itself.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Sprites are the tempting option. The display chip fetches them on its own and lays them over the
playfield, so moving one is just two register writes. Here is a small spinning ball done that way.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;amiga-sprites&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:600px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Amiga sprites and BOBs…&lt;/p&gt;&lt;iframe src=&quot;/tools/amiga-sprites/?bare=1&amp;amp;embed=1&amp;amp;example=ball#sprite&quot; title=&quot;Amiga sprites and BOBs (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Play under Preview, then change HSTART and VSTART and watch SPR0POS and SPR0CTL change.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/amiga-sprites/?example=ball#sprite&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Press &lt;strong&gt;Play&lt;/strong&gt; in the preview to run the four frames. Change &lt;strong&gt;HSTART&lt;/strong&gt; and &lt;strong&gt;VSTART&lt;/strong&gt; and
watch the register values on the right: a sprite&amp;#8217;s whole position lives in two words, &lt;code&gt;SPRxPOS&lt;/code&gt; and
&lt;code&gt;SPRxCTL&lt;/code&gt;. Untick &lt;strong&gt;Attached pair&lt;/strong&gt; and the colours collapse from fifteen to three.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Notice&lt;/strong&gt; the width. An Amiga sprite is sixteen pixels wide, full stop. There are eight of them, and
an attached pair spends two to get fifteen colours instead of three. Boing&amp;#8217;s ball was many times
wider than sixteen pixels; building it from sprites would have used them all up and still not been
enough. Sprites were for mouse pointers, bullets and small heroes. A big ball had to live in the
bitplanes.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-blitter-copying-shapes-through-a-mask&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-blitter-copying-shapes-through-a-mask&quot;&gt;&lt;/a&gt;The blitter: copying shapes through a mask&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The &lt;strong&gt;blitter&lt;/strong&gt; (block image transferer) is the chip that copies rectangles of memory. It reads up
to three sources — A, B and C — combines them bit by bit with any Boolean function you choose,
and writes the result to D. It does this one 16-bit word at a time, much faster than the 68000 could
manage with its own instructions.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A blitter object is a shape plus a &lt;strong&gt;mask&lt;/strong&gt;: a one-bit silhouette that says which pixels belong to
the shape. Here is a 32 × 32 Boing ball BOB, two bitplanes deep, with eight frames of spin.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;amiga-sprites&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:600px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Amiga sprites and BOBs…&lt;/p&gt;&lt;iframe src=&quot;/tools/amiga-sprites/?bare=1&amp;amp;embed=1&amp;amp;example=bob:ball#bob&quot; title=&quot;Amiga sprites and BOBs (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press Play under Preview, then change Blit at x from 37 to 32 and watch BLTCON0 and BLTCON1.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/amiga-sprites/?example=bob:ball#bob&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Run the preview to see the frames. Then look at the set-up underneath: &lt;code&gt;BLTCON0&lt;/code&gt; is
&lt;code&gt;$5FCA&lt;/code&gt;. Change &lt;strong&gt;Blit at x&lt;/strong&gt; to 32 and the leading 5 becomes 0 — the shift disappears because 32 is
a multiple of 16. Change it to 47 and the shift is back.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Notice&lt;/strong&gt; the &lt;code&gt;CA&lt;/code&gt; at the end of &lt;code&gt;BLTCON0&lt;/code&gt;. That byte is the &lt;strong&gt;minterm&lt;/strong&gt;: the Boolean function the
blitter applies. &lt;code&gt;$CA&lt;/code&gt; is the famous cookie-cut, &lt;code&gt;D = AB + aC&lt;/code&gt; — where the mask A is set, take the
shape B; where it is not, keep the background C. And the shift is there because the blitter only
deals in whole 16-bit words: to put a shape at pixel 37 it reads the words and barrel-shifts them
five pixels on the way through.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Now watch the blitter do that cookie-cut, word by word.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;blitter&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Amiga blitter lab…&lt;/p&gt;&lt;iframe src=&quot;/tools/blitter/?bare=1&amp;amp;embed=1&amp;amp;example=cookie#area&quot; title=&quot;Amiga blitter lab (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: press ◀ to step back through the blit, then tap the destination to move the shape.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/blitter/?example=cookie#area&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Press the step buttons to walk through the blit one word at a time: green for an A
read, blue for B, orange for C, red for a D write. Tap somewhere else on the destination to move
the shape and watch the shift and the masks change. Change the minterm to &lt;code&gt;$FA&lt;/code&gt; (&lt;code&gt;A + C&lt;/code&gt;) and the
ball becomes a solid silhouette punched into the background.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Notice&lt;/strong&gt; the extra word on each row. Because of the shift, a shape that is two words wide spills
into a third, so the blit is one word wider than the shape and the last-word mask &lt;code&gt;ALWM&lt;/code&gt; is
&lt;code&gt;$0000&lt;/code&gt; to stop rubbish creeping in. Notice too what a bouncing BOB has to do every frame: first
put the background back where the ball &lt;em&gt;was&lt;/em&gt;, then cut the ball in where it &lt;em&gt;is&lt;/em&gt;. That is two blits
per frame, per object — exactly the kind of work the Amiga was designed to hand to the blitter
instead of the processor.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The shadow fits the same scheme. The simplest way to get a see-through shadow on an Amiga is a
bitplane of its own: wherever its bit is set, the palette maps each background colour to a darker
version of itself. The shadow is one more one-bit shape, moved with the ball.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;who-gets-the-memory&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#who-gets-the-memory&quot;&gt;&lt;/a&gt;Who gets the memory?&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;All of this — the display fetching bitplanes, the copper, sprites, the blitter, the disk and the
sound — reads and writes the same &lt;strong&gt;chip RAM&lt;/strong&gt;. The bus is shared out in fixed slots along each scan
line, and the processor gets whatever is left.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;amiga-dma&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Amiga DMA slot timing…&lt;/p&gt;&lt;iframe src=&quot;/tools/amiga-dma/?bare=1&amp;amp;embed=1&amp;amp;example=lores-320-4&quot; title=&quot;Amiga DMA slot timing (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: raise Bitplanes from 4 to 5 and 6, then pick the hires example.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/amiga-dma/?example=lores-320-4&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; This is a low-resolution PAL screen with four bitplanes. Each square is one colour
clock: &lt;code&gt;R&lt;/code&gt; is memory refresh, &lt;code&gt;A0&lt;/code&gt;–&lt;code&gt;A3&lt;/code&gt; the four audio channels, &lt;code&gt;S0a&lt;/code&gt;… the eight sprites,
&lt;code&gt;P1&lt;/code&gt;–&lt;code&gt;P4&lt;/code&gt; bitplane fetches, and &lt;code&gt;M&lt;/code&gt; slots free for the 68000. Raise &lt;strong&gt;Bitplanes&lt;/strong&gt; to 5 and then 6,
and watch the free slots inside the display disappear. Choose &lt;strong&gt;Hires 640 × 256, 4 planes&lt;/strong&gt; and the
processor is locked out of chip RAM for most of every visible line. Tick &lt;strong&gt;Blitter busy&lt;/strong&gt; and see
what it takes for itself.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Notice&lt;/strong&gt; why Boing works the way it does. A low-resolution screen with a few bitplanes leaves room
on the bus for the blitter to move the ball and for the processor to run the program. Every effect
that could be done by the copper or by colour cycling — the grid, the floor, the spin — costs
almost nothing here. Only the ball&amp;#8217;s movement really spends memory bandwidth, and the blitter
spends it far more efficiently than the processor could.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-boom-paula&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-boom-paula&quot;&gt;&lt;/a&gt;The boom: Paula&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The last ingredient is the one people remember from the show floor. When the ball hits a wall
there is a deep, rubbery &lt;em&gt;boom&lt;/em&gt;, and it comes from the side the ball hit. The story goes that the
sound was recorded by thumping a garage door; however it was made, it was a &lt;strong&gt;sample&lt;/strong&gt; — a recording
— played back by the Amiga&amp;#8217;s sound chip, &lt;strong&gt;Paula&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Paula has four channels, each fed by DMA straight from chip RAM (you saw their slots, &lt;code&gt;A0&lt;/code&gt;–&lt;code&gt;A3&lt;/code&gt;,
in the last tool). Each channel plays 8-bit samples at a rate set by its &lt;strong&gt;period&lt;/strong&gt;: the colour clock
divided by a number you write to a register.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;paula&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Paula audio, MOD tracker and player…&lt;/p&gt;&lt;iframe src=&quot;/tools/paula/?bare=1&amp;amp;embed=1&amp;amp;per=428#calc&quot; title=&quot;Paula audio, MOD tracker and player (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: change Period to 214 and to 856, then switch between PAL and NTSC.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/paula/?per=428#calc&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Period 428 plays at 8,287 Hz on a PAL machine — the note ProTracker calls C-2. Halve
the period to 214 and the rate doubles, an octave up; double it to 856 and it drops an octave.
Switch the clock to NTSC and the same period plays very slightly faster, because the colour clock
is different. Type a sample rate on the right, such as 22,050 Hz, to see which period comes
closest.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Notice&lt;/strong&gt; how cheap it is. Once a channel is pointed at a sample and given a period and a volume,
it plays on its own: one DMA word per scan line, two samples per word, no processor at all. And
the channels are wired hard left and right — 0 and 3 to one side, 1 and 2 to the other — so making
the boom come from the wall the ball hit is a matter of choosing which channel to start.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;putting-it-back-together&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#putting-it-back-together&quot;&gt;&lt;/a&gt;Putting it back together&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here is Boing again, rebuilt from the parts:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;grid and floor&lt;/strong&gt; are an ordinary bitmap drawn once, displayed by bitplane DMA, with the
palette loaded every frame by the copper.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;spin&lt;/strong&gt; is colour cycling: the ball is drawn once with a run of palette indices and only the
colours change.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;bounce&lt;/strong&gt; moves real pixels, so the ball lives in the bitplanes and is moved with blits
through a mask, at a shift that puts it on any pixel.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;shadow&lt;/strong&gt; is one more one-bit shape that darkens whatever lies under it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;boom&lt;/strong&gt; is a sample handed to one of Paula&amp;#8217;s channels, left or right.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The processor&amp;#8217;s job was small: work out where the ball goes next, set up a couple of blits, nudge
the palette, and start a sound when the ball hits something. Everything else ran on its own, in
step with the beam. That is the lesson of Boing, and of the Amiga generally: the impressive part was
not how fast it could compute, but how much it did not have to.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;where-to-go-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#where-to-go-next&quot;&gt;&lt;/a&gt;Where to go next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/copper/&quot;&gt;Copper and blitter playground&lt;/a&gt; — write longer copper lists: rainbows, sky
gradients, palette splits and the line-255 trick.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/copper-gradient/&quot;&gt;Copper gradients and raster bars&lt;/a&gt; — design gradients and shaded
bars and export them as a copper list.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/amiga-sprites/&quot;&gt;Amiga sprites and BOBs&lt;/a&gt; — draw your own sprites and BOBs and export the
data words and blitter set-up.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/blitter/&quot;&gt;Amiga blitter lab&lt;/a&gt; — minterms, area fill and line mode.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/amiga-dma/&quot;&gt;Amiga DMA slot timing&lt;/a&gt; — every colour clock of a scan line.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/paula/&quot;&gt;Paula audio, MOD tracker and player&lt;/a&gt; — write music the way Amiga games did.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/amiga-emulator/&quot;&gt;Amiga emulator&lt;/a&gt; and &lt;a href=&quot;/tools/amiga-devbench/&quot;&gt;Amiga dev bench&lt;/a&gt; —
run real Amiga software, or write 68000 code and try these tricks for real.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/boing-effect-lab/&quot;&gt;Boing Lamp effect lab&lt;/a&gt; — the ball as a lamp of 128 LEDs; its
&lt;a href=&quot;/tools/boing-effect-lab/#screensaver&quot;&gt;screensaver&lt;/a&gt; bounces it over the grid.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/projects/the-amiga-40th-exhibit/&quot;&gt;The Amiga 40th exhibit&lt;/a&gt; and
&lt;a href=&quot;/projects/amiga-games/&quot;&gt;Amiga games&lt;/a&gt; — what I have been building on the machine since.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>How web search works</title>
    <link href="https://geekychris.github.io/lab/explorations/how-web-search-works/"/>
    <id>https://geekychris.github.io/lab/explorations/how-web-search-works/</id>
    <updated>2026-10-30T00:00:00Z</updated>
    <summary>Follow a query from the box you type it into to the ten blue links: how text becomes terms, how an inverted index stores and compresses them, how BM25 scores a match, how we know a ranking is any good, and how the whole thing is split across thousands of machines — with the tools running live between the paragraphs.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;You type four words into a box and, a quarter of a second later, ten results come back from an
index of tens of billions of pages. It feels like one operation. It is really a pipeline of fairly
simple ideas, each of which is easy to understand on its own and slightly astonishing when you see
how far it has been pushed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I have spent much of my working life on search — enterprise search, content platforms, more Lucene
indexes than I care to count — and the thing I most wish someone had shown me early on is that
there is no magic in it. There is text processing, a big sorted dictionary, some careful arithmetic
and a lot of engineering to make it fast. This exploration walks through those pieces in the order
a document meets them, with the site&amp;#8217;s own search tools embedded along the way. Everything runs in
your browser; poke at it as you read.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-one-turning-text-into-terms&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-one-turning-text-into-terms&quot;&gt;&lt;/a&gt;Step one: turning text into terms&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A search engine never searches text. It searches &lt;strong&gt;terms&lt;/strong&gt;: normalised tokens that came out of an
&lt;strong&gt;analyser&lt;/strong&gt;. Before a page is indexed, and again when you type a query, the text goes through the
same chain: character filters (strip HTML, map odd characters), a &lt;strong&gt;tokeniser&lt;/strong&gt; that splits the
stream into words, and a sequence of &lt;strong&gt;token filters&lt;/strong&gt; that lower-case, drop very common words and
cut words down to a stem. If the document and the query go through the same chain, &quot;Running&quot; in
one and &quot;runs&quot; in the other can meet in the middle.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here is Elasticsearch&amp;#8217;s &lt;code&gt;english&lt;/code&gt; analyser, rebuilt stage by stage.&lt;/p&gt;
&lt;/div&gt;
&lt;style&gt;.tool-embed{margin:1.6rem 0}.tool-embed .te-frame{position:relative;height:var(--te-h,520px);max-height:90vh;border:1px solid var(--line,#d9d6d0);border-radius:10px;overflow:hidden;background:var(--wash,#f3efe9)}@media (max-width:640px){.tool-embed .te-frame{height:min(var(--te-h,520px),75vh);border-radius:8px}}.tool-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent}.tool-embed .te-loading{position:absolute;inset:0;margin:0;display:grid;place-items:center;color:var(--faint,#8a9096);font:.9rem var(--sans,system-ui,sans-serif)}.tool-embed .te-ready .te-loading{visibility:hidden}.tool-embed figcaption{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.25rem 1rem;margin-top:.45rem;font-size:.86rem;color:var(--muted,#5f646b)}.tool-embed .te-open{white-space:nowrap;font-weight:600;margin-left:auto}.tool-embed-missing{padding:.8rem 1rem;border:1px dashed var(--line,#d9d6d0);border-radius:8px;color:var(--muted,#5f646b)}&lt;/style&gt;&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;search-analyzer&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:620px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Analyzer and inverted-index playground…&lt;/p&gt;&lt;iframe src=&quot;/tools/search-analyzer/?bare=1&amp;amp;embed=1&amp;amp;example=english#analyze&quot; title=&quot;Analyzer and inverted-index playground (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;The english analyser: tokeniser, possessive stemmer, lowercase, stop words, keyword marker and Porter stemmer, one stage at a time.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/search-analyzer/?example=english#analyze&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt; Scroll down to the token stream and read it top to bottom. The sample sentence is
&lt;em&gt;The QUICK brown foxes jumped over the lazy dog&amp;#8217;s bone. Running runners ran!&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Watch the first stemmer turn &lt;code&gt;dog&amp;#8217;s&lt;/code&gt; into &lt;code&gt;dog&lt;/code&gt; — that is the possessive filter, and it runs
before lower-casing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The stop filter removes both &lt;code&gt;the&lt;/code&gt; tokens, but notice that the &lt;strong&gt;positions&lt;/strong&gt; jump from 0 to 1 and
from 5 to 7. The gaps are kept on purpose, so that a phrase query later knows the words were not
really adjacent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Porter stemmer gives &lt;code&gt;fox&lt;/code&gt;, &lt;code&gt;jump&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, &lt;code&gt;runner&lt;/code&gt; — and &lt;code&gt;lazi&lt;/code&gt;. Stems are not words; they
only have to be consistent. It also leaves &lt;code&gt;ran&lt;/code&gt; alone: Porter strips suffixes, it does not know
irregular verbs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Switch the analyser at the top to &lt;code&gt;standard&lt;/code&gt; and the stems disappear. Now a search for &quot;fox&quot;
would not match &quot;foxes&quot;. Switch to &lt;code&gt;keyword&lt;/code&gt; and the whole sentence becomes one term — exactly
what you want for a product code or a tag, and useless for prose.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;That last experiment is the first real lesson of search: most &quot;the search engine is broken&quot;
complaints are analysis problems. The document had the word; the analyser turned it into something
the query never produced.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-two-the-inverted-index&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-two-the-inverted-index&quot;&gt;&lt;/a&gt;Step two: the inverted index&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Once text is terms, the engine builds an &lt;strong&gt;inverted index&lt;/strong&gt;: for every term, the list of documents
that contain it. That list is called a &lt;strong&gt;postings list&lt;/strong&gt;, and each entry — a &lt;strong&gt;posting&lt;/strong&gt; — records the
document ID, how many times the term appears (the term frequency, tf) and, if phrase search is
wanted, the positions.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The same tool shows the index the &lt;code&gt;english&lt;/code&gt; analyser builds over four tiny documents.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;search-analyzer&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Analyzer and inverted-index playground…&lt;/p&gt;&lt;iframe src=&quot;/tools/search-analyzer/?bare=1&amp;amp;embed=1&amp;amp;example=english#index&quot; title=&quot;Analyzer and inverted-index playground (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;Four documents, seven distinct terms: each with its document frequency, total frequency, BM25 idf and postings.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/search-analyzer/?example=english#index&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Find &lt;code&gt;fox&lt;/code&gt;. It appears in all four documents (df 4), including &quot;brown foxes and quick dogs&quot;,
because the stemmer folded &lt;code&gt;foxes&lt;/code&gt; into &lt;code&gt;fox&lt;/code&gt;. Its idf is tiny: a term in every document says
almost nothing about which one you want.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Compare &lt;code&gt;jump&lt;/code&gt;, &lt;code&gt;lazi&lt;/code&gt; and &lt;code&gt;over&lt;/code&gt;, each in one document with an idf more than ten times larger.
Rare terms carry the signal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Look at the &lt;strong&gt;Field lengths and norms&lt;/strong&gt; table under the index. Lucene stores each field&amp;#8217;s length
in a single byte, exact up to 23 and approximate after that. We will need those lengths in a
moment, when we score.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Edit the documents. Add a line that says &quot;the the the the&quot; and watch it contribute nothing at
all — the stop filter has already thrown those words away.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;That is the whole shape of the index. A query for &lt;code&gt;quick fox&lt;/code&gt; becomes &quot;fetch the postings for
&lt;code&gt;quick&lt;/code&gt;, fetch the postings for &lt;code&gt;fox&lt;/code&gt;, and combine them&quot;. The interesting questions are how to
store lists with billions of entries, and how to combine them without reading all of them.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-three-making-postings-small&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-three-making-postings-small&quot;&gt;&lt;/a&gt;Step three: making postings small&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A web index is dominated by its postings. If you stored every document ID as a 32-bit integer you
would spend four bytes per posting, and for a term like &quot;the&quot; (if you kept it) or &quot;online&quot; that is
a great many bytes. The trick that makes postings small is to store the &lt;strong&gt;gaps&lt;/strong&gt; between sorted IDs
— the &lt;strong&gt;d-gaps&lt;/strong&gt; — rather than the IDs themselves. Common terms have small gaps, and small numbers
can be written in very few bits.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The postings lab builds an index from a synthetic collection of 5,000 documents whose word
frequencies follow Zipf&amp;#8217;s law, as real language does, and compresses it every way you might
reasonably try.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;postings&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:620px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Postings lab…&lt;/p&gt;&lt;iframe src=&quot;/tools/postings/?bare=1&amp;amp;embed=1&amp;amp;corpus=synthetic#compress&quot; title=&quot;Postings lab (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;Bits per posting under every codec, for docID gaps, term frequencies and positions, on 5,000 Zipf-distributed documents.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/postings/?corpus=synthetic#compress&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Read the docIDs column. Uncompressed is 32 bits a posting; VByte (Lucene&amp;#8217;s old VInt: seven bits
a byte, the eighth says &quot;more follows&quot;) gets it under 10; Golomb and Rice coding, which are close
to optimal when documents are scattered at random, get it near 6. The whole index drops from
3.66 MB to around 550 KB.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now look at Simple-9, FOR-128, PForDelta and Lucene&amp;#8217;s PForUtil. They are a bit or two worse than
Golomb per posting, and that is the point: they pack 128 values at a fixed bit width so that a
CPU can unpack them with shifts and masks, often with SIMD, instead of decoding bit by bit.
Search engines trade a few per cent of space for several times the decoding speed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Term frequencies compress superbly with γ or Golomb (about 1.4 bits), because nearly every tf
is 1.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Roaring bitmaps look poor here at 18 bits a posting. They store document IDs, not gaps, and shine
on dense lists and set operations — filters, mostly, rather than scored text.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Scroll down, pick a single term, and look at its gaps written out bit by bit.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-four-not-reading-the-whole-list&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-four-not-reading-the-whole-list&quot;&gt;&lt;/a&gt;Step four: not reading the whole list&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Compression makes postings small enough to keep in memory. It does not stop a query for three
common words from touching millions of documents. The second trick is to &lt;strong&gt;avoid scoring documents
that cannot make the top ten&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The idea behind MaxScore and WAND is pleasingly simple. Each term has an &lt;strong&gt;upper bound&lt;/strong&gt;: the best
score it could possibly contribute to any document. Keep a heap of the best k results found so
far; its lowest score is the threshold θ. A document that only contains terms whose upper bounds
add up to less than θ cannot get in, so there is no need to score it — or even to stop at it.
Block-Max WAND refines this with a maximum per block of 128 postings, so it can skip whole blocks.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;postings&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Postings lab…&lt;/p&gt;&lt;iframe src=&quot;/tools/postings/?bare=1&amp;amp;embed=1&amp;amp;corpus=synthetic#topk&quot; title=&quot;Postings lab (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;The top 5 by BM25 over 5,000 documents: exhaustive scoring against MaxScore, WAND and Block-Max WAND, then step by step.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/postings/?corpus=synthetic#topk&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Look at the &lt;strong&gt;Docs scored&lt;/strong&gt; column. Exhaustive term-at-a-time and document-at-a-time scoring both
score 3,463 documents. MaxScore scores 53. WAND and Block-Max WAND land around a hundred. Every
row has a tick in &lt;strong&gt;Same top-k&lt;/strong&gt;: the pruning is exact, not an approximation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Notice why: the query&amp;#8217;s first term, &lt;code&gt;le&lt;/code&gt;, is in 3,111 documents but its upper bound is only
0.404, while the rare &lt;code&gt;vuzesevi&lt;/code&gt; can contribute 2.417. Once the heap fills with documents that
contain the rare words, the common term on its own can never compete.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Raise k from 5 to 100. The threshold rises more slowly, so pruning helps less — one reason
engines retrieve a modest candidate set cheaply and rerank it with something heavier.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;strong&gt;Step by step&lt;/strong&gt; with WAND and watch the pivot: the first document at which the running sum
of upper bounds could beat θ. Everything before it is skipped.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-five-scoring-a-match-with-bm25&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-five-scoring-a-match-with-bm25&quot;&gt;&lt;/a&gt;Step five: scoring a match with BM25&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Retrieval finds the documents; &lt;strong&gt;scoring&lt;/strong&gt; puts them in order. For thirty years the workhorse has
been BM25, and it is still the first-stage scorer in Lucene, Elasticsearch and OpenSearch. For
each query term it multiplies an idf (how rare the term is) by a tf factor (how often it appears
here), with two knobs:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;k1&lt;/strong&gt; controls &lt;strong&gt;saturation&lt;/strong&gt;: how quickly repeating a word stops helping.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;b&lt;/strong&gt; controls &lt;strong&gt;length normalisation&lt;/strong&gt;: how much a long field is penalised for being long.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;search-scoring&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Relevance and scoring explainer…&lt;/p&gt;&lt;iframe src=&quot;/tools/search-scoring/?bare=1&amp;amp;embed=1&amp;amp;example=saturation#explain&quot; title=&quot;Relevance and scoring explainer (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;BM25 on six short documents: “Lucene” three times barely beats “Lucene” once.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/search-scoring/?example=saturation#explain&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The preset searches for &lt;code&gt;lucene&lt;/code&gt; in the short-versus-long set. &quot;Lucene Lucene Lucene&quot; scores
0.395 and plain &quot;Lucene&quot; scores 0.390. Three times the term frequency buys about one per cent.
That is saturation, and it is why keyword stuffing stopped working.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Look at the &lt;strong&gt;Classic TF-IDF&lt;/strong&gt; column on the right. Lucene&amp;#8217;s old scorer, which grows with √tf,
ranks the one-word title first by a wide margin. Two formulas, two different answers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Drag &lt;strong&gt;k1&lt;/strong&gt; to 3. Repetition keeps paying for longer and the gap widens. Drag it to 0 and tf
stops mattering at all.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Drag &lt;strong&gt;b&lt;/strong&gt; to 1 (or open &lt;code&gt;example=length-norm&lt;/code&gt;) and length normalisation is total: a one-word
document is the &quot;densest&quot; possible match, and the two Lucene-only documents tie.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open the &lt;strong&gt;Curves&lt;/strong&gt; tab to see the shapes behind these numbers: the tf curve flattening towards
k1 + 1, the length curve tilting with b, and idf falling as a term gets common.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click a row for Lucene&amp;#8217;s full &lt;code&gt;explain&lt;/code&gt; output, term by term.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;BM25 is rarely the last word now. Production systems add field boosts, freshness, popularity,
link-based authority and, increasingly, a learned model or a vector similarity on top. The &lt;strong&gt;Hybrid&lt;/strong&gt;
tab of the same tool fuses BM25 with toy embeddings using reciprocal rank fusion, which is how
many teams combine lexical and semantic search today. But nearly all of them still use something
like BM25 to pick the first few thousand candidates, because it is cheap, explainable and works
straight off the inverted index.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-six-knowing-whether-the-ranking-is-any-good&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-six-knowing-whether-the-ranking-is-any-good&quot;&gt;&lt;/a&gt;Step six: knowing whether the ranking is any good&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Change k1, add a boost, swap in a reranker — is the result better? Opinions are free, so search
teams measure. The classic method is a set of queries with &lt;strong&gt;relevance judgements&lt;/strong&gt;: people grade
how useful each document is for each query, and metrics turn a ranked list of grades into a
number.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;ranking-metrics&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Ranking metrics workbench…&lt;/p&gt;&lt;iframe src=&quot;/tools/ranking-metrics/?bare=1&amp;amp;embed=1&amp;amp;example=wiki-dcg#explain&quot; title=&quot;Ranking metrics workbench (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;One ranking with grades 3, 2, 3, 0, 1, 2 and two relevant documents never retrieved: precision, recall, AP, RR, nDCG and ERR.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/ranking-metrics/?example=wiki-dcg#explain&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Read the coloured squares: the grades of the six results in rank order, and two dashed ones for
relevant documents the system missed. Precision at 6 is 0.83 (five of six are relevant), but
recall is only 0.71 because of the two it never found.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nDCG@6 is 0.785. DCG rewards high grades near the top, discounting each rank logarithmically;
dividing by the ideal ordering (3, 3, 3, 2, 2, 2) makes it a number between 0 and 1.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Swap the 0 and the 3 in the grades box and watch nDCG drop. Put the best document last and it
drops further. Position matters more than presence.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try the &lt;strong&gt;ERR vs nDCG&lt;/strong&gt; button. ERR models a user who stops at the first good result, so one
perfect answer on top is nearly all it cares about; nDCG keeps counting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For comparing two systems properly, see the &lt;strong&gt;Compare&lt;/strong&gt; tab in the full tool (&lt;code&gt;example=pooled-trec&lt;/code&gt;):
25 queries, BM25 against a reranker, and four significance tests that tell you whether the
difference is real or noise.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Judged collections are expensive, so large engines lean on behaviour as well: clicks, dwell time,
and &lt;strong&gt;interleaving&lt;/strong&gt;, which merges two rankings into one list and lets real users&#39; clicks vote. The
&lt;strong&gt;Interleaving&lt;/strong&gt; tab simulates exactly that.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-seven-one-index-many-machines&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-seven-one-index-many-machines&quot;&gt;&lt;/a&gt;Step seven: one index, many machines&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Everything so far would run happily on one machine for a few million documents. The web is not a
few million documents. Search engines split the index into &lt;strong&gt;shards&lt;/strong&gt; — usually by document, so each
shard holds a complete inverted index for its slice of the collection — and every query goes to
every shard. Each shard returns its own top results with their scores, and a coordinator merges
them.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Elasticsearch does exactly this inside a cluster, and the simulator shows it message by message.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;es-sim&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Elasticsearch, simulated…&lt;/p&gt;&lt;iframe src=&quot;/tools/es-sim/?bare=1&amp;amp;embed=1&amp;amp;example=search&quot; title=&quot;Elasticsearch, simulated (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;Query then fetch: scatter to one copy of every shard, merge the candidates, fetch only the winners — then deep pagination and a slow node.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/es-sim/?example=search&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Press &lt;strong&gt;Start&lt;/strong&gt;. The first step scatters a search for &lt;code&gt;error&lt;/code&gt; to one copy of each of the four
shards; each returns only the IDs and scores of its own top five. The coordinating node merges
those twenty candidates and only then &lt;strong&gt;fetches&lt;/strong&gt; the five winning documents. Fetching every
candidate&amp;#8217;s full text would be far more expensive.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The narration points out something subtle: each shard scores with its &lt;strong&gt;own&lt;/strong&gt; term statistics, so
the same document could get a different idf on a different shard. On a big, well-mixed index the
differences wash out; on a tiny one they can reorder results.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Keep going to &lt;strong&gt;Deep pagination&lt;/strong&gt;. Asking for results 61–70 makes every shard return its top 70.
This is why search engines cap how deep you can page, and why &lt;code&gt;search_after&lt;/code&gt; exists.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The last steps slow one node down. A search waits for its slowest shard, so one sick machine
slows &lt;strong&gt;every&lt;/strong&gt; query that touches it — until adaptive replica selection learns to route around it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open the &lt;strong&gt;Fan-out&lt;/strong&gt; tab. With a per-shard p99 of 60 ms and a 1% chance of any shard being slow, a
search that touches 20 shards meets at least one slow shard 18% of the time, and its p99 is
126 ms — twice the p99 of any single shard. Drag the shard count up to see why this is called
the tail at scale.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;step-eight-the-whole-search-engine&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#step-eight-the-whole-search-engine&quot;&gt;&lt;/a&gt;Step eight: the whole search engine&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A web-scale engine wraps that core in a great deal more: crawlers and a document stream, a
real-time index for pages published minutes ago next to the big batch-built index, a result cache
(a surprising share of queries are repeats), query understanding (spelling, synonyms, intent), and
a tree of &lt;strong&gt;root&lt;/strong&gt; and &lt;strong&gt;mixer&lt;/strong&gt; servers that fan each query out to thousands of index &lt;strong&gt;leaves&lt;/strong&gt; and
merge the answers. Each shard is replicated, partly for safety and mostly for throughput.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The system design tool has that architecture as a simulation, with an experiment that makes one
leaf slow.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;system-design&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:680px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading System design interviews…&lt;/p&gt;&lt;iframe src=&quot;/tools/system-design/?bare=1&amp;amp;embed=1&amp;amp;example=slow-leaf#archs&quot; title=&quot;System design interviews (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;A web-scale search engine under load: one replica of one index shard becomes five times slower.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/system-design/?example=slow-leaf#archs&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Let the run play. Before the change at 5 s, p99 sits around 115 ms. After it, one replica of one
shard is five times slower and p99 roughly triples while the error rate stays near zero. Nothing
is &quot;down&quot;; the system is just slow, which is the hardest kind of failure to notice.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Find the slow leaf in the diagram: it is the busiest box, while its peers look normal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try &lt;strong&gt;Slow leaf, blind routing&lt;/strong&gt; in the experiments list. Without least-loaded routing the damage is
worse; with it, work steers away from the sick replica. &lt;strong&gt;Hedged requests&lt;/strong&gt; — send the same
request to a second replica after a short delay and take whichever answers first — trim the
remaining tail.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Try &lt;strong&gt;Result cache cold&lt;/strong&gt; and watch the index shards take the full load the cache was absorbing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Switch from &lt;strong&gt;Simulate&lt;/strong&gt; to &lt;strong&gt;Design&lt;/strong&gt; or &lt;strong&gt;Deep dives&lt;/strong&gt; for the architecture laid out box by box, or to
&lt;strong&gt;Interview card&lt;/strong&gt; for the condensed version.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;putting-it-together&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#putting-it-together&quot;&gt;&lt;/a&gt;Putting it together&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Follow one query through what you have just played with. The words you type go through the same
analyser the documents did, becoming terms. The root server sends those terms to every shard. Each
shard decodes compressed postings lists, block by block, and uses upper bounds to skip documents
that cannot make its top k. The survivors are scored with BM25 — and, these days, rescored by
something more expensive. The root merges each shard&amp;#8217;s best, fetches titles and snippets for the
winners and returns them. Somewhere else, a team is measuring last week&amp;#8217;s change with nDCG and
interleaving to decide whether it stays.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;None of the individual steps is hard. What makes web search remarkable is that each of them has
been squeezed for decades: a few bits per posting, a few hundred documents scored out of millions,
a few milliseconds per shard, and a tail latency watched as closely as the average.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;where-to-go-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#where-to-go-next&quot;&gt;&lt;/a&gt;Where to go next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The full tools, each with far more than the embeds show:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/search-analyzer/&quot;&gt;Analyzer and inverted-index playground&lt;/a&gt; — build your own analyser,
try synonym graphs and edge n-grams for autocomplete, and phrase queries with slop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/postings/&quot;&gt;Postings lab&lt;/a&gt; — compression codecs bit by bit, AND/OR intersection with
skip pointers and galloping search, and MaxScore, WAND and BMW step by step.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/search-scoring/&quot;&gt;Relevance and scoring explainer&lt;/a&gt; — BM25 in full, multi-field
queries, function scores for popularity and recency, and hybrid search.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/ranking-metrics/&quot;&gt;Ranking metrics&lt;/a&gt; — load your own TREC runs and judgements, compare
systems with significance tests, and simulate interleaving.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/es-sim/&quot;&gt;Elasticsearch, simulated&lt;/a&gt; — indexing, near-real-time refresh, segments and
merges, node failure and master elections.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/system-design/?example=mock-search#framework&quot;&gt;System design interviews&lt;/a&gt; — the web
search engine as a 60-minute mock interview, with the estimator to size it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/vector-search/&quot;&gt;Vector search&lt;/a&gt; — the other half of modern retrieval.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And from my own work: &lt;a href=&quot;/articles/viewscene-a-desktop-luke-alternative-for-inspecting-lucene-indexes/&quot;&gt;ViewScene&lt;/a&gt;,
a desktop tool for looking inside real Lucene indexes, and
&lt;a href=&quot;/articles/building-a-full-social-network-from-scratch-feed-friends-groups-dms-search/&quot;&gt;building a full social network from scratch&lt;/a&gt;,
which has search as one of its parts.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Why sourdough rises</title>
    <link href="https://geekychris.github.io/lab/explorations/why-sourdough-rises/"/>
    <id>https://geekychris.github.io/lab/explorations/why-sourdough-rises/</id>
    <updated>2026-10-30T00:00:00Z</updated>
    <summary>A jar of flour and water raises a loaf with no packet of yeast. Follow it from the microbes in the starter, through temperature, time, water and gluten, to the oven — with the sourdough, bread and artisan-bread tools open beside the text.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A sourdough starter is about the least promising-looking thing in a kitchen: a jar of grey,
slightly boozy paste that you feed flour and water once a day. Yet a spoonful of it, mixed into
a kilogram of dough and left alone for a day, will lift that dough into a tall loaf with an
open, glossy crumb and a crackling crust. No packet of yeast, no baking powder. Just flour,
water, salt, time — and a few billion passengers.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This page follows that rise from start to finish: who lives in the jar, why the clock matters
so much less than the thermometer, what the water is doing, how gluten holds the gas, how an
aliquot jar tells you when to stop, and what happens in the first twenty minutes in the oven.
Along the way the site&amp;#8217;s own bread tools are open in the page, so you can push the numbers
around and see what changes.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;who-lives-in-the-jar&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#who-lives-in-the-jar&quot;&gt;&lt;/a&gt;Who lives in the jar&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A mature starter is a stable little ecosystem of two kinds of microbe.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Wild yeasts&lt;/strong&gt; — often &lt;em&gt;Saccharomyces cerevisiae&lt;/em&gt;, the same species as baker&amp;#8217;s yeast, or
&lt;em&gt;Kazachstania humilis&lt;/em&gt; — eat sugars and turn them into carbon dioxide and ethanol. The carbon
dioxide is the gas that raises the bread. The ethanol mostly bakes off, though you can smell it
in a hungry starter.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Lactic acid bacteria&lt;/strong&gt; — &lt;em&gt;Fructilactobacillus sanfranciscensis&lt;/em&gt; (the one named after the San
Francisco bakeries where it was first described), &lt;em&gt;Lactiplantibacillus plantarum&lt;/em&gt; and their
relatives — eat the same sugars and make lactic and acetic acid. They outnumber the yeasts by
something like a hundred to one. They are where the sour in sourdough comes from, and the acid
they make is what keeps the jar safe: within a few hours of a feed the pH falls low enough that
most spoilage organisms simply cannot compete.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The two get along better than you might expect. Flour contains enzymes, amylases, that slowly
break starch into maltose, a sugar made of two glucose units. &lt;em&gt;F. sanfranciscensis&lt;/em&gt; loves
maltose; &lt;em&gt;K. humilis&lt;/em&gt; cannot use it at all and lives on the glucose the bacteria leave behind.
They are not competing for the same meal, so neither crowds the other out. That partnership is
why a healthy starter can be kept for decades.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Feeding is just dilution. You take a little of the old culture — a crowded, acidic, exhausted
population — and give it a lot of fresh food. The microbes wake up, multiply, and the jar rises
as gas is trapped in the paste. When the food runs low the rise stalls and the top begins to
fall: that is the &lt;strong&gt;peak&lt;/strong&gt;, the moment the culture is most active and the best time to use it.&lt;/p&gt;
&lt;/div&gt;
&lt;style&gt;.tool-embed{margin:1.6rem 0}.tool-embed .te-frame{position:relative;height:var(--te-h,520px);max-height:90vh;border:1px solid var(--line,#d9d6d0);border-radius:10px;overflow:hidden;background:var(--wash,#f3efe9)}@media (max-width:640px){.tool-embed .te-frame{height:min(var(--te-h,520px),75vh);border-radius:8px}}.tool-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent}.tool-embed .te-loading{position:absolute;inset:0;margin:0;display:grid;place-items:center;color:var(--faint,#8a9096);font:.9rem var(--sans,system-ui,sans-serif)}.tool-embed .te-ready .te-loading{visibility:hidden}.tool-embed figcaption{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:.25rem 1rem;margin-top:.45rem;font-size:.86rem;color:var(--muted,#5f646b)}.tool-embed .te-open{white-space:nowrap;font-weight:600;margin-left:auto}.tool-embed-missing{padding:.8rem 1rem;border:1px dashed var(--line,#d9d6d0);border-radius:8px;color:var(--muted,#5f646b)}&lt;/style&gt;&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;sourdough&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Sourdough starter and levain planner…&lt;/p&gt;&lt;iframe src=&quot;/tools/sourdough/?bare=1&amp;amp;embed=1&amp;amp;example=overnight-feed#feed&quot; title=&quot;Sourdough starter and levain planner (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: switch the ratio between 1:1:1, 1:5:5 and 1:10:10 and watch the peak time move.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/sourdough/?example=overnight-feed#feed&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This view opens on an overnight feed: 10 g of starter with 50 g of flour and 50 g of water, a
1:5:5 feed, at 21 °C. The tool estimates it will peak in about ten and a half hours.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Click &lt;strong&gt;1:1:1&lt;/strong&gt;. The peak time roughly halves. With equal parts starter and food,
there are already plenty of microbes, so they only need a couple of doublings to eat it all.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click &lt;strong&gt;1:10:10&lt;/strong&gt;. Now the culture is diluted twenty-one times over, and it needs more
doublings to catch up — and each doubling takes the same time, so the peak moves out by hours.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change &lt;strong&gt;Starter sits at&lt;/strong&gt; from 21 to 26 °C and back. Everything moves — a hint of what the
next section is about.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice:&lt;/strong&gt; the relationship is logarithmic. Going from 1:1:1 to 1:2:2 adds less time
than you might guess, because what matters is how many doublings the culture needs, and that
is the logarithm of how much you diluted it. Bakers use big ratios on purpose: a 1:5:5 feed in
the evening is ripe at breakfast, and a more diluted, slower culture tastes milder.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;temperature-is-the-real-clock&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#temperature-is-the-real-clock&quot;&gt;&lt;/a&gt;Temperature is the real clock&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Microbes are chemistry in a bag, and chemistry speeds up with heat. For yeast and lactic acid
bacteria in the range a kitchen actually covers — roughly 4 °C to 30 °C — a useful rule of
thumb is that activity &lt;strong&gt;doubles for every 10 °C warmer&lt;/strong&gt;. Scientists call this a Q&lt;sub&gt;10&lt;/sub&gt; of about
two. It is not exact, and it breaks down above about 32 °C (the bacteria start to outrun the
yeast) and near the fridge (the yeast nearly stop), but over the range that matters it is
surprisingly good.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;sourdough&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Sourdough starter and levain planner…&lt;/p&gt;&lt;iframe src=&quot;/tools/sourdough/?bare=1&amp;amp;embed=1&amp;amp;t=21#peak&quot; title=&quot;Sourdough starter and levain planner (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: drag your eye along each curve from 16 °C to 30 °C; then change Starter sits at and watch the dashed line move.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/sourdough/?t=21#peak&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The chart plots hours to peak against the jar&amp;#8217;s temperature for four common feeding ratios.
The dashed line marks the temperature you typed into &lt;strong&gt;Starter sits at&lt;/strong&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Follow the 1:5:5 curve. At 16 °C it takes around fifteen hours; at 26 °C, about half that.
Ten degrees, half the time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Look at the gaps between the curves. At every temperature, diluting further adds a roughly
constant stretch of time, because each extra doubling takes as long as the last.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Below the chart there is a form to calibrate the model to &lt;em&gt;your&lt;/em&gt; starter: enter a feed you
actually did, how long it took to peak and how warm it was. Every other estimate on the
page re-scales from that one observation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice:&lt;/strong&gt; the shape is the important part, not the numbers. Your starter, your flour
and your kitchen will shift the curves up or down, but the curve stays a curve. A warm summer
kitchen and a cold winter one can be ten degrees apart, and that is the difference between a
bake that runs on time and one that runs half a day late. Experienced bakers watch the
thermometer far more than the clock.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;water-flour-and-the-bakers-percentage&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#water-flour-and-the-bakers-percentage&quot;&gt;&lt;/a&gt;Water, flour and the baker&amp;#8217;s percentage&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Before the dough can rise it has to be mixed, and bakers write their recipes in a slightly odd
way: every ingredient is a percentage of the flour. A dough with 1,000 g of flour and 750 g of
water is &quot;75 % hydration&quot;. It makes recipes easy to scale, and it makes the one number that
most shapes a loaf — how wet it is — visible at a glance.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Water matters to the rise in three ways. It is the medium the microbes and enzymes work in, so
a wetter dough ferments a little faster. It lets the gluten proteins move and link up. And it
makes the dough more extensible, so gas cells can grow bigger before they tear: wetter doughs
tend to give a more open crumb, at the cost of being harder to handle.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There is a catch with sourdough, though. The levain — the portion of starter built up for this
particular bake — is itself flour and water. If you only count the water you poured in, you are
underestimating how wet the dough really is.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;bread&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:620px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Bread and baker&amp;#39;s percentages…&lt;/p&gt;&lt;iframe src=&quot;/tools/bread/?bare=1&amp;amp;embed=1&amp;amp;example=country#formula&quot; title=&quot;Bread and baker&amp;#39;s percentages (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: scroll to Overall and compare its hydration with the 75 % on the water row; then change the levain to 30 %.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/bread/?example=country#formula&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This is a country loaf in the Tartine style: 90 % bread flour, 10 % whole wheat, 75 % water,
2 % salt and 20 % levain at 100 % hydration, scaled to two 900 g loaves.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Scroll down to &lt;strong&gt;Overall&lt;/strong&gt;. The water row says 75 %, but the true hydration — all the water over
all the flour, the levain&amp;#8217;s included — is 77.3 %. About 9 % of the flour has already been
fermented before the dough is even mixed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change the levain&amp;#8217;s baker&amp;#8217;s percentage from 20 to 30. The true hydration climbs again, and so
does the prefermented flour: more of the dough arrives already full of microbes and acid.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change the water to 68 %. That is closer to a sandwich loaf; 80 % and up is ciabatta
territory.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice:&lt;/strong&gt; &quot;prefermented flour&quot; is a quiet but important number. It is how much of the
dough has already been through fermentation — and so how much acid, flavour and active culture
you are starting with. More levain means a faster bulk and a tangier loaf; less means a longer,
gentler one.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;choosing-the-doughs-temperature&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#choosing-the-doughs-temperature&quot;&gt;&lt;/a&gt;Choosing the dough&amp;#8217;s temperature&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Because temperature sets the pace, bakers aim the dough at a target — usually 24–26 °C — by
choosing the temperature of the water. Jeffrey Hamelman&amp;#8217;s &lt;em&gt;desired dough temperature&lt;/em&gt; method
adds up the flour, the room, the water, the levain and the heat the mixer adds through friction;
together they should come to four times the target. Solve for the water and you are done.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;bread&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Bread and baker&amp;#39;s percentages…&lt;/p&gt;&lt;iframe src=&quot;/tools/bread/?bare=1&amp;amp;embed=1&amp;amp;example=summer-water#ddt&quot; title=&quot;Bread and baker&amp;#39;s percentages (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: lower the room and flour temperatures from 30 °C to 20 °C and watch the ice disappear.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/bread/?example=summer-water#ddt&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This preset is a hot kitchen: 30 °C room and flour, a stand mixer adding about 8 °C of friction,
and a target of 24 °C. The water has to be 4 °C — colder than the tap — so the tool tells you to
replace about 16 % of it with ice.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Change &lt;strong&gt;Mixing&lt;/strong&gt; to &lt;strong&gt;Hand mixing&lt;/strong&gt;. Less friction, so the water can be warmer and the ice share
shrinks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set room and flour to 18 °C, a winter kitchen. Now the water needs to be warm.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice:&lt;/strong&gt; mixing alone can warm a dough by ten degrees. Combined with the doubling rule,
that is the difference between a four-hour and an eight-hour bulk — set before the first fold.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;gluten-the-balloon-around-the-gas&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#gluten-the-balloon-around-the-gas&quot;&gt;&lt;/a&gt;Gluten: the balloon around the gas&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Yeast makes gas, but gas alone does not make bread; it would bubble straight out. What holds it
is &lt;strong&gt;gluten&lt;/strong&gt;. Wheat flour carries two families of storage proteins, glutenins and gliadins. Dry,
they do nothing. Wet them and work them, and the glutenins link end to end into long elastic
chains while the gliadins slip between them like a lubricant. The result is a network that is
both springy (glutenin) and stretchy (gliadin) — a sheet that can inflate.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;One surprising detail: the yeast do not make the bubbles. Mixing folds in tiny pockets of air,
and every gas cell in the finished crumb grew from one of those. The carbon dioxide dissolves in
the dough&amp;#8217;s water and diffuses into the pockets that already exist. That is why mixing and
folding matter so much: they decide how many cells there are and how strong their walls are.
Stretch-and-folds and coil folds during the first half of bulk build strength without knocking
out gas, and organise the network so the cells grow evenly.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Acid plays both sides. A little acid tightens the gluten and makes dough easier to handle. A lot
of acid, and the flour&amp;#8217;s own protease enzymes given many hours, start to break the network down
again — which is why an over-fermented sourdough turns slack, sticky and impossible to shape.
(Rye is a different story: it has very little useful gluten, and the acid in a rye sour is what
lets the crumb set at all, by slowing the enzymes that would otherwise turn the starch to
paste.)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;knowing-when-bulk-is-done-the-aliquot-jar&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#knowing-when-bulk-is-done-the-aliquot-jar&quot;&gt;&lt;/a&gt;Knowing when bulk is done: the aliquot jar&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Bulk fermentation — the long first rise, after mixing and before shaping — is where most
sourdough goes wrong. Stop too early and the loaf is tight and dense with a few big tunnels
under the crust. Go too far and the gluten weakens, the dough spreads, and it bakes flat and
sour. The clock is a poor guide because, as we have seen, it depends so heavily on temperature
and levain.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The trick many bakers use is the &lt;strong&gt;aliquot jar&lt;/strong&gt;: right after mixing, drop a small piece of the
dough into a straight-sided jar, push it flat and mark the level. The jar sits beside the main
dough, ferments at the same rate, and its straight sides turn a vague &quot;looks puffy&quot; into a
percentage you can read.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;artisan-bread&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:640px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Artisan bread…&lt;/p&gt;&lt;iframe src=&quot;/tools/artisan-bread/?bare=1&amp;amp;embed=1&amp;amp;example=bulk-model#sourdough&quot; title=&quot;Artisan bread (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: scroll down to Fermentation model; change the dough temperature and the levain percentage and watch the bulk time and the aliquot target move.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/artisan-bread/?example=bulk-model#sourdough&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The tab opens on a guide to starter types — liquid levain, stiff levain, rye sour, lievito
madre — which is worth a read in itself: note how a stiff, cooler levain favours acetic acid
and a sharper loaf, while a liquid, warmer one favours milder lactic acid. Scroll down to
&lt;strong&gt;Fermentation model&lt;/strong&gt;. The preset is 15 % levain in a 26 °C dough: about four and a half hours of
bulk, and an aliquot-jar range to aim for before shaping.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Look at the table of bulk times. Read across a row (warmer) and down a column (more levain).
The fastest corner — 30 % levain at 28 °C — is under three hours; the slowest — 5 % at 20 °C
— is nearly ten.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change the dough temperature to 22 °C. The bulk time stretches, and the aliquot target
&lt;em&gt;rises&lt;/em&gt;. A cooler dough ferments more evenly and can be taken further.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tick and untick &lt;strong&gt;Cold retard after bulk&lt;/strong&gt;. If the shaped loaf is going to spend the night in
the fridge, it keeps fermenting slowly there, so bulk should stop earlier.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice:&lt;/strong&gt; the target is a range, not a line, and it moves with the conditions. The tool
says so plainly: these are rules of thumb to calibrate against your own crumb. Bake, cut the
loaf open, and adjust next time.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;when-things-go-wrong&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#when-things-go-wrong&quot;&gt;&lt;/a&gt;When things go wrong&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Every baker has pulled out a pale, flat disc with no ear. The useful thing about knowing the
mechanism is that a bad loaf becomes evidence: the crumb tells you which part of the chain
failed.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;artisan-bread&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:620px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Artisan bread…&lt;/p&gt;&lt;iframe src=&quot;/tools/artisan-bread/?bare=1&amp;amp;embed=1&amp;amp;example=flat-loaf#fix&quot; title=&quot;Artisan bread (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: read the ranked causes, then untick Spread flat and tick Dense instead.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/artisan-bread/?example=flat-loaf#fix&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This preset ticks three symptoms — spread flat, no ear, a tight even crumb — and ranks the likely
causes beneath.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Read the list. Over-proofing comes first, then weak shaping, under-developed gluten, too
little steam, a weak starter or a cold dough, and timid scoring. Each comes with what to
change.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Untick &lt;strong&gt;Spread flat&lt;/strong&gt; and tick &lt;strong&gt;Dense, heavy crumb&lt;/strong&gt; and &lt;strong&gt;Big holes or a tunnel under the top
crust&lt;/strong&gt;. The ranking flips towards under-fermentation: the dough simply did not have enough
gas or time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Scroll down to &lt;strong&gt;Reading the crumb&lt;/strong&gt; for drawings of what well-fermented, tight, dense, gummy and
under-proofed crumbs look like.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice:&lt;/strong&gt; the causes map neatly onto the sections of this page — the microbes, the
temperature, the gluten, and (next) the oven. Change one thing at a time between bakes, or you
will never know which one fixed it.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;shaping-scoring-and-the-oven&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#shaping-scoring-and-the-oven&quot;&gt;&lt;/a&gt;Shaping, scoring and the oven&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;After bulk the dough is divided, pre-shaped, rested and shaped. Shaping is not just cosmetic: it
drags the outside of the dough into a taut skin that holds the loaf up while it proofs and
steers its expansion in the oven. Then comes the final proof — an hour or two on the bench, or
a night in the fridge, which slows fermentation to a crawl, deepens the acid flavour and makes a
cold, firm loaf that is easy to score.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;artisan-bread&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:600px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Artisan bread…&lt;/p&gt;&lt;iframe src=&quot;/tools/artisan-bread/?bare=1&amp;amp;embed=1&amp;amp;example=ear#shape&quot; title=&quot;Artisan bread (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: step through the bâtard drawings, then scroll to Scoring and compare the single ear with the cross.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/artisan-bread/?example=ear#shape&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;This view shows shaping a bâtard in four steps, the poke test for judging the proof, and the
scoring patterns, opened on the single ear.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Read the poke test: a dent that springs straight back needs more time; one that springs back
slowly and only part way is ready; one that stays is over-proofed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In &lt;strong&gt;Scoring&lt;/strong&gt;, compare the single ear (a shallow 30–45° cut that lifts a flap of skin) with the
cross or the square on a boule. Each cut is a deliberate weak point: it decides where the
loaf will burst open as it springs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;When the loaf hits a hot oven, the rise has one last act — &lt;strong&gt;oven spring&lt;/strong&gt;. In the first ten
minutes or so the dough warms from the outside in. Gas expands with heat; the dissolved carbon
dioxide comes out of solution; ethanol and then water turn to vapour inside the cells; and the
yeast have a final frantic burst of activity before they die at around 60 °C. A well-made loaf
can grow by a third in that time. Then the starch gelatinises and the proteins set, freezing the
crumb in place, and the crust dries and browns through the Maillard reaction and caramelisation.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;All of that depends on the crust staying soft long enough. That is what steam is for.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;tool-embed&quot; data-tool=&quot;artisan-bread&quot;&gt;
&lt;div class=&quot;te-frame&quot; style=&quot;--te-h:560px&quot;&gt;&lt;p class=&quot;te-loading&quot; aria-hidden=&quot;true&quot;&gt;Loading Artisan bread…&lt;/p&gt;&lt;iframe src=&quot;/tools/artisan-bread/?bare=1&amp;amp;embed=1&amp;amp;style=country#oven&quot; title=&quot;Artisan bread (live tool)&quot; loading=&quot;lazy&quot; allow=&quot;clipboard-write; fullscreen&quot; onload=&quot;this.parentNode.classList.add(&#39;te-ready&#39;)&quot;&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;span class=&quot;te-cap&quot;&gt;What to try: switch between the Dutch oven, the stone with steam and the baking sheet, and compare the steam phase.&lt;/span&gt;&lt;a class=&quot;te-open&quot; href=&quot;/tools/artisan-bread/?style=country#oven&quot;&gt;Open full tool →&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The country loaf is set up for a Dutch oven: preheat for 50 minutes at 250 °C, bake 20 minutes
with the lid on, then 25 uncovered at 230 °C until the middle reads 96–99 °C.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Switch the set-up to &lt;strong&gt;Stone or steel + steam&lt;/strong&gt;, then to &lt;strong&gt;Baking sheet + cover&lt;/strong&gt;. The steam phase
changes shape, but it is always there.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Read &lt;strong&gt;Why steam, and when it is done&lt;/strong&gt;. Moist air keeps the skin supple so the loaf can expand
and the score can open; condensing steam gelatinises the surface starch for a glossy,
crackly crust. After that, dry heat browns it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;What to notice:&lt;/strong&gt; the lid-on phase is exactly the window of oven spring. The Dutch oven works
because the loaf makes its own steam and the lid traps it. Take the lid off too early and the
crust sets while the loaf is still trying to grow: a pale, tight, earless loaf, just like the one
in the troubleshooter.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And then — the hardest step — leave it to cool. The crumb is still setting as the steam leaves
it. Cut a lean loaf before an hour is up and it will be gummy; cut a high-rye loaf before a day
is up and it will be worse.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;putting-it-together&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#putting-it-together&quot;&gt;&lt;/a&gt;Putting it together&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Follow one loaf through and the whole story fits in a paragraph. A feed dilutes a culture of
yeasts and lactic acid bacteria and gives them maltose to share; they double until the food
runs out, and the jar peaks. That ripe culture goes into a dough whose water you chose to land
it at 25 °C, because temperature sets the pace of everything that follows. Mixing and folding
make the air cells and build the gluten network around them; the yeast fill the cells with
carbon dioxide while the bacteria make the acids that flavour the loaf and, given too long,
weaken it. The aliquot jar tells you when to stop. Shaping makes a skin, scoring makes a weak
point, and in a steamy oven the gas expands one last time before heat sets it for good.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;None of it needs a packet of yeast. It needs a thermometer, a straight-sided jar and patience.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;where-to-go-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#where-to-go-next&quot;&gt;&lt;/a&gt;Where to go next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/sourdough/&quot;&gt;Sourdough starter and levain planner&lt;/a&gt; — feeding, building a levain that
lands exactly on its hydration, peak times calibrated to your starter, and a bake-day schedule
you can export to a calendar.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/bread/&quot;&gt;Bread and baker&amp;#8217;s percentages&lt;/a&gt; — write any formula, see its true hydration,
convert yeast and work out the water temperature.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/artisan-bread/&quot;&gt;Artisan bread&lt;/a&gt; — about ninety breads with their process, a
bake-day plan with fold timers and the aliquot-jar target, shaping and scoring drawings, steam
set-ups and a troubleshooter.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/ferment/&quot;&gt;Fermentation salt and timing&lt;/a&gt; — the same lactic acid bacteria, put to
work on sauerkraut and pickles instead of bread.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;/tools/pizza/&quot;&gt;Pizza dough&lt;/a&gt; — a sourdough pizza is the quickest way to practise all of this
in an evening.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>3-D models</title>
    <link href="https://geekychris.github.io/articles/3d-models/"/>
    <id>https://geekychris.github.io/articles/3d-models/</id>
    <updated>2026-10-18T00:00:00Z</updated>
    <summary>Figures, spacecraft and retro machines I have made as 3-D models with Meshy.ai and prepared for printing — mostly people as Lemmings, Amiga history and 1980s TV. This page grows as I add models.</summary>
    <content type="html">&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open any model to see it from every side — front, side, top and the print plate — and the ones
marked &lt;strong&gt;3-D&lt;/strong&gt; you can turn round in your browser, or stand on your desk in AR on a phone. The chips
pick out one project at a time.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;How the Lemmings came about — friends from the Amiga community, designed with Meshy from photos and
printed on Bambu printers — is in &lt;a href=&quot;/projects/lemmings-lemmings-everywhere/&quot;&gt;Lemmings, Lemmings
everywhere&lt;/a&gt;. This page grows as I add models; the date at the top says when the last one arrived.&lt;/p&gt;
&lt;/div&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/css/models.css&quot;&gt;

&lt;div class=&quot;mg mg-embed&quot; data-mg-index&gt;
  &lt;p class=&quot;mg-count&quot; aria-live=&quot;polite&quot;&gt;&lt;span data-mg-count&gt;93&lt;/span&gt; &lt;span data-mg-count-of&gt;models in 12 projects&lt;/span&gt;&lt;/p&gt;

  &lt;nav class=&quot;mg-chips&quot; aria-label=&quot;Filter by project&quot; data-mg-chips hidden&gt;
    &lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;true&quot; data-mg-project=&quot;&quot;&gt;All &lt;span&gt;93&lt;/span&gt;&lt;/button&gt;
    &lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;lemmings&quot; title=&quot;Lemmings-style figures (mostly people as Lemmings)&quot;&gt;Lemmings &lt;span&gt;19&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;rj-mical&quot; title=&quot;RJ Mical figures&quot;&gt;RJ Mical &lt;span&gt;3&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;amiga&quot; title=&quot;Amiga history: the Juggler, Amy the Squirrel and Sabrina, Agnus, Starchild, Amiga heroes&quot;&gt;Amiga &lt;span&gt;13&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;atari-force&quot; title=&quot;1980s comic-style crew in Atari jumpsuits&quot;&gt;Atari Force &lt;span&gt;6&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;ai-force&quot; title=&quot;AI Force parody figures&quot;&gt;AI Force &lt;span&gt;12&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;v-series&quot; title=&quot;Craft and figures in the style of the 1980s TV series V&quot;&gt;V &lt;span&gt;8&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;space&quot; title=&quot;Spacecraft and spacesuits (Space: 1999, Blake&amp;#39;s 7, Apollo)&quot;&gt;Space &lt;span&gt;6&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;aliens&quot; title=&quot;Grey aliens and the park bench they sit on&quot;&gt;Aliens &lt;span&gt;12&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;gnomes&quot; title=&quot;Garden gnomes&quot;&gt;Gnomes &lt;span&gt;2&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;retro-computers&quot; title=&quot;Retro computers (Sinclair ZX Spectrum, Severance-style terminal)&quot;&gt;Retro computers &lt;span&gt;4&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;characters&quot; title=&quot;Other character figures&quot;&gt;Characters &lt;span&gt;6&lt;/span&gt;&lt;/button&gt;&lt;button type=&quot;button&quot; class=&quot;mg-chip&quot; aria-pressed=&quot;false&quot; data-mg-project=&quot;chibi-beach-girl&quot; title=&quot;Chibi beach girl iterations&quot;&gt;Chibi beach girl &lt;span&gt;2&lt;/span&gt;&lt;/button&gt;
  &lt;/nav&gt;

  &lt;ul class=&quot;mg-grid&quot; data-mg-grid&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/gino-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/gino-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gino Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Bearded lemming in an Amiga Boing-ball cap holding up a floppy disk.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;64 × 55 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill accent&quot; title=&quot;Turn it in 3-D&quot;&gt;3-D&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/rock-star-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/rock-star-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Rock Star Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Rock star lemming in a leather jacket with an electric guitar, horns up.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;75 × 53 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill accent&quot; title=&quot;Turn it in 3-D&quot;&gt;3-D&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/wizard-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/wizard-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Wizard Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Wizard lemming in a starry purple robe with a glowing staff and floating spell book.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;83 × 60 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill warn&quot;&gt;2 parts&lt;/span&gt;&lt;span class=&quot;mg-pill accent&quot; title=&quot;Turn it in 3-D&quot;&gt;3-D&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/bill-the-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/bill-the-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Bill the Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Classic Lemmings-style lemming in a blue robe, shading his eyes.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;66 × 48 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/builder-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/builder-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Builder Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Builder lemming in hard hat and hi-vis vest, reading a blueprint.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;73 × 47 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/chef-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/chef-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Chef Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Chef lemming waving a frying pan and wooden spoon, with a cooking pot.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;70 × 55 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill warn&quot;&gt;2 parts&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/glenn-lemming-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/glenn-lemming-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Glenn Lemming Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Semi-realistic man with spiky green Lemmings hair, glasses and a lavender shirt.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;32 × 25 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/jeff-lemming-character/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/jeff-lemming-character/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Jeff Lemming Character&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Bust of a grinning man with a green mohawk, glasses and a light-bulb bowler hat.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;87 × 58 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/jeff-lemming-full-body/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/jeff-lemming-full-body/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Jeff Lemming Full Body&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Full-body version: green mohawk, light-bulb bowler hat, blue tunic, pointing up.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;46 × 31 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/jerry-lemming-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/jerry-lemming-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Jerry Lemming Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grumpy green-haired lemming in red-and-white checked pyjamas, holding a tablet.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;47 × 45 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/manager-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/manager-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Manager Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Manager lemming in a navy suit with coffee cup and briefcase.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;74 × 47 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/mistress-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/mistress-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Mistress Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Glamorous lemming in a black wrap dress, heels and green beehive updo.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;50 × 45 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/professor-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/professor-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Professor Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Professor lemming in mortarboard and tweed, holding chalk and a small chalkboard.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;70 × 48 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/programmer-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/programmer-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Programmer Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Programmer lemming in a grey hoodie, sitting cross-legged with a laptop and an energy drink.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;78 × 76 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill warn&quot;&gt;2 parts&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/raj-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/raj-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Raj Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Smiling lemming with green spiky hair, glasses and a blue hooded robe.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;50 × 45 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/rock-star-lemming-trophy/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/rock-star-lemming-trophy/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Rock Star Lemming Trophy&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;The rock star lemming as a trophy on a round walnut base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;65 × 65 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/superhero-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/superhero-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Superhero Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Flying superhero lemming with a red cape and an &amp;#39;L&amp;#39; chest emblem.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 82 × 67 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/three-headed-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/three-headed-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Three Headed Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Three-headed lemming in a blue robe — red, grey-bearded and blond heads.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;87 × 41 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;lemmings&quot;&gt;
      &lt;a href=&quot;/models/lemmings/trevor-the-lemming/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/lemmings/trevor-the-lemming/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Trevor the Lemming&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Older classic lemming with grey-and-green hair, shading his eyes.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;67 × 47 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Lemmings&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;rj-mical&quot;&gt;
      &lt;a href=&quot;/models/rj-mical/rj-lemming-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/rj-mical/rj-lemming-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;RJ Lemming Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Cartoon lemming with a green hair tuft, white beard and glasses, in a yellow T-shirt.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;49 × 32 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;RJ Mical&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;rj-mical&quot;&gt;
      &lt;a href=&quot;/models/rj-mical/rj-mical-spacesuit/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/rj-mical/rj-mical-spacesuit/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;RJ Mical Spacesuit&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;RJ in an orange-and-grey spacesuit with bubble helmet, holding a green bottle and pointing.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 53 × 57 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;RJ Mical&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;rj-mical&quot;&gt;
      &lt;a href=&quot;/models/rj-mical/rj-stylized-spacesuit/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/rj-mical/rj-stylized-spacesuit/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;RJ Stylized Spacesuit&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Stylised RJ in an orange spacesuit, thumbs up, holding a green bottle.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;58 × 46 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;RJ Mical&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/agnus-wire-wrap-stack-model/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/agnus-wire-wrap-stack-model/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Agnus Wire Wrap Stack Model&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Seven hinged wire-wrap prototype boards fanned open like a book.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 52 × 88 mm&lt;/span&gt;&lt;span class=&quot;mg-pill warn&quot;&gt;7 parts&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/amiga-cartoon-computer-character/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/amiga-cartoon-computer-character/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Amiga Cartoon Computer Character&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Cheerful cartoon Amiga computer with a magenta CRT face, gloved hands and feet.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 57 × 78 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/amiga-unbounded-starchild/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/amiga-unbounded-starchild/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Amiga Unbounded Starchild&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Bust of a pale, bald child resting chin on hand.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 89 × 48 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/amy-the-squirrel/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/amy-the-squirrel/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Amy the Squirrel&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Cartoon squirrel girl with blonde hair and a huge tail, sitting and smiling.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 36 × 73 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/cartoon-computer-character/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/cartoon-computer-character/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Cartoon Computer Character&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;The first cartoon computer mascot: grey-white case, magenta CRT face, gloved hands.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 58 × 82 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/cartoon-computer-character-amiga-retexture/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/cartoon-computer-character-amiga-retexture/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Cartoon Computer Character AMIGA Retexture&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Beige re-texture of the cartoon computer with &amp;#39;AMIGA&amp;#39; painted across the screen.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 58 × 82 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/cartoon-computer-character-clean-retexture/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/cartoon-computer-character-clean-retexture/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Cartoon Computer Character Clean Retexture&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Beige re-texture of the cartoon computer with a clean magenta face.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 58 × 82 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/dave-amiga-hero-with-cape/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/dave-amiga-hero-with-cape/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Dave Amiga Hero With Cape&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Curly-haired Amiga superhero with a Boing-ball chest emblem, throwing a punch.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;65 × 45 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/dave-amiga-superhero/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/dave-amiga-superhero/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Dave Amiga Superhero&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;The Amiga superhero in flight, fist forward and cape streaming.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 53 × 35 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/dave-stylized-amiga-hero/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/dave-stylized-amiga-hero/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Dave Stylized Amiga Hero&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Chunkier, cartoon-proportioned version of the Amiga superhero.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 55 × 40 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/full-body-starchild/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/full-body-starchild/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Full Body Starchild&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Full-body pearly white &amp;#39;Starchild&amp;#39; figure curled up, hugging its knees.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 45 × 35 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/robot-juggler-character/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/robot-juggler-character/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Robot Juggler Character&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Cartoon robot woman juggling red-and-white chequered balls.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;64 × 39 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill warn&quot;&gt;3 parts&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;amiga&quot;&gt;
      &lt;a href=&quot;/models/amiga/sabrina-online-character/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/amiga/sabrina-online-character/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Sabrina Online 3D Character&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Semi-realistic skunk woman with white hair, blue jacket and black trousers.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 51 × 34 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Amiga&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;atari-force&quot;&gt;
      &lt;a href=&quot;/models/atari-force/dr-lucas-orion/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/dr-lucas-orion/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Dr Lucas Orion&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey-haired Atari Force crew member, arms folded.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;37 × 22 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Atari Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;atari-force&quot;&gt;
      &lt;a href=&quot;/models/atari-force/li-san-orourke/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/li-san-orourke/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Li-San ORourke&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Atari Force crew member with long black hair, hand on hip.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;39 × 19 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Atari Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;atari-force&quot;&gt;
      &lt;a href=&quot;/models/atari-force/lydia-perez/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/lydia-perez/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Lydia Perez&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Atari Force crew member with short curly dark hair, hands on hips.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;37 × 15 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Atari Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;atari-force&quot;&gt;
      &lt;a href=&quot;/models/atari-force/martin-champion/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/martin-champion/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Martin Champion&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Atari Force crew member with swept brown hair, hands on hips — the heroic pose.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;48 × 19 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Atari Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;atari-force&quot;&gt;
      &lt;a href=&quot;/models/atari-force/mohandas-singh/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/mohandas-singh/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Mohandas Singh&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Turbaned, bearded Atari Force crew member, arms folded.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;33 × 22 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Atari Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;atari-force&quot;&gt;
      &lt;a href=&quot;/models/atari-force/sixth-member/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/sixth-member/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Sixth Member&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Sixth Atari Force crew member: reddish-brown hair, hands on hips.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;36 × 17 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Atari Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-big-brain/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-big-brain/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Big Brain&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Big-brained hero in a black circuit-line bodysuit, fists up.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;60 × 26 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-big-brain-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-big-brain-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Big Brain Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the AI Force Big Brain figure.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;60 × 26 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-neckbeard-casual/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-neckbeard-casual/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Neckbeard Casual&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Bearded man in a blue polo and chinos, arms crossed.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;32 × 22 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-neckbeard-casual-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-neckbeard-casual-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Neckbeard Casual Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the casual bearded AI Force figure.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;32 × 22 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-neckbeard-suited/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-neckbeard-suited/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Neckbeard Suited&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;The bearded AI Force figure in the black circuit-line hero suit, arms crossed.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;50 × 25 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-neckbeard-suited-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-neckbeard-suited-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Neckbeard Suited Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the suited bearded AI Force figure.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;50 × 25 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-pudgy-guy/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-pudgy-guy/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Pudgy Guy&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Heavy-set AI Force hero in glasses, running.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 71 × 44 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-pudgy-guy-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-pudgy-guy-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Pudgy Guy Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the running AI Force figure.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 71 × 44 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-python-woman/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-python-woman/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Python Woman&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Curly-haired AI Force heroine flexing a bicep.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 62 × 26 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-python-woman-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-python-woman-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Python Woman Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the curly-haired AI Force heroine.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 62 × 26 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-teen/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-teen/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Teen&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grinning teenage AI Force member with a fist in the air.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;49 × 18 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-teen-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-teen-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Teen Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the AI Force teen.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;49 × 18 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;v-series&quot;&gt;
      &lt;a href=&quot;/models/v-series/chibi-dianna-reptile-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/v-series/chibi-dianna-reptile-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Chibi Dianna Reptile Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Chibi V villain: huge curly hair, half-reptile face, a pet rat, on a round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;72 × 66 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;V&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;v-series&quot;&gt;
      &lt;a href=&quot;/models/v-series/dianna-reptilian-reveal-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/v-series/dianna-reptilian-reveal-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Dianna Reptilian Reveal Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;V-style reveal figure in a red catsuit, raising a rodent, half her face peeled back to reptile.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;66 × 66 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;V&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;v-series&quot;&gt;
      &lt;a href=&quot;/models/v-series/mike-donovan-chibi-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/v-series/mike-donovan-chibi-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Mike Donovan Chibi Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Chibi figure in a red jumpsuit aiming a pistol.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 55 × 47 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;V&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;v-series&quot;&gt;
      &lt;a href=&quot;/models/v-series/mike-donovan-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/v-series/mike-donovan-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Mike Donovan Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Realistic figure in a red jumpsuit and holster, aiming a pistol.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;61 × 38 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;V&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;v-series&quot;&gt;
      &lt;a href=&quot;/models/v-series/v-people-carrier/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/v-series/v-people-carrier/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;V People Carrier&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;White V-style people carrier with windowed modules, red insignia and landing legs.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 46 × 32 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;V&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;v-series&quot;&gt;
      &lt;a href=&quot;/models/v-series/v-people-carrier-symmetric/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/v-series/v-people-carrier-symmetric/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;V People Carrier Symmetric&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Symmetric regeneration of the V people carrier, with a squarer front window grid.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 49 × 31 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;V&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;v-series&quot;&gt;
      &lt;a href=&quot;/models/v-series/v-shuttlecraft/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/v-series/v-shuttlecraft/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;V Shuttlecraft&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Sleek white V-style shuttle with black canopy and swept wings.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 83 × 32 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;V&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;v-series&quot;&gt;
      &lt;a href=&quot;/models/v-series/v-ufo-mothership/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/v-series/v-ufo-mothership/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;V Ufo Mothership&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Flat, lens-shaped mothership saucer in weathered grey-pink metal.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 100 × 22 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;V&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;space&quot;&gt;
      &lt;a href=&quot;/models/space/apollo-11-spacesuit/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/space/apollo-11-spacesuit/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Apollo 11 Spacesuit&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Apollo-era lunar EVA spacesuit with gold visor and PLSS backpack, moon-dusted.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 43 × 44 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Space&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;space&quot;&gt;
      &lt;a href=&quot;/models/space/eagle-transporter/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/space/eagle-transporter/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Eagle Transporter&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Space: 1999 Eagle Transporter (multi-image) — hull plus four leg pods on one plate.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 43 × 21 mm&lt;/span&gt;&lt;span class=&quot;mg-pill warn&quot;&gt;5 parts&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Space&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;space&quot;&gt;
      &lt;a href=&quot;/models/space/eagle-transporter-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/space/eagle-transporter-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Eagle Transporter 3d V2&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Space: 1999 Eagle Transporter (single image) — all in one piece.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 46 × 20 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Space&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;space&quot;&gt;
      &lt;a href=&quot;/models/space/liberator-spacecraft/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/space/liberator-spacecraft/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Liberator Spacecraft&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Blake&amp;#39;s 7 Liberator: green ridged dome, gold ring and three engine nacelles on struts.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 70 × 62 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Space&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;space&quot;&gt;
      &lt;a href=&quot;/models/space/space1999-astronaut/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/space/space1999-astronaut/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Space1999 Astronaut&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Space: 1999-style astronaut in an orange suit and yellow helmet.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;35 × 22 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Space&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;space&quot;&gt;
      &lt;a href=&quot;/models/space/space1999-astronaut-full/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/space/space1999-astronaut-full/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Space1999 Astronaut Full&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Multi-image version of the Space: 1999 astronaut, with the backpack modelled.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;33 × 27 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Space&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/alien-figurine-middle-finger/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/alien-figurine-middle-finger/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Alien Figurine Middle Finger&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien in board shorts and flip-flops giving the finger, on a round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;35 × 35 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/alien-hear-no-evil/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/alien-hear-no-evil/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Alien Hear No Evil&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Seated grey alien covering its ears — the &amp;#39;hear no evil&amp;#39; of a trio.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 33 × 36 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/alien-see-no-evil/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/alien-see-no-evil/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Alien See No Evil&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Seated grey alien covering its eyes — &amp;#39;see no evil&amp;#39;.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 32 × 33 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/alien-speak-no-evil/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/alien-speak-no-evil/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Alien Speak No Evil&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Seated grey alien covering its mouth — &amp;#39;speak no evil&amp;#39;.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 26 × 27 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Barefoot grey alien reaching out with one long arm.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;59 × 27 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-coffee/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-coffee/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Coffee&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Barefoot grey alien sipping from a coffee mug, hand on hip.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;34 × 23 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-coffee-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-coffee-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Coffee V2&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien sipping coffee on a rocky round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;45 × 44 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-phone/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-phone/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Phone&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Barefoot grey alien chatting on a mobile phone.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;28 × 26 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-phone-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-phone-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Phone V2&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien on the phone, standing on a rocky round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;46 × 46 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-picking-nose/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-picking-nose/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Picking Nose&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien picking its nose, on a rocky round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;46 × 45 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-waving-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-waving-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Waving V2&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien reaching out a hand, on a rocky round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;63 × 51 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/weathered-park-bench/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/weathered-park-bench/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Weathered Park Bench&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Weathered park bench with wooden slats and cast-iron ends.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 38 × 48 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;gnomes&quot;&gt;
      &lt;a href=&quot;/models/gnomes/grandma-garden-gnome/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/gnomes/grandma-garden-gnome/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Grandma Garden Gnome&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grandma garden gnome with white hair, sunglasses and a green jacket.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;38 × 33 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Gnomes&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;gnomes&quot;&gt;
      &lt;a href=&quot;/models/gnomes/grenade-throwing-gnome/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/gnomes/grenade-throwing-gnome/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Grenade Throwing Gnome&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Bearded garden gnome winding up to throw a grenade, on a rocky base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;86 × 42 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Gnomes&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;retro-computers&quot;&gt;
      &lt;a href=&quot;/models/retro-computers/severance-retro-computer/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/retro-computers/severance-retro-computer/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Severance Retro Computer&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Severance-style retro office terminal: cream CRT monitor plus teal keyboard with trackball.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 73 × 69 mm&lt;/span&gt;&lt;span class=&quot;mg-pill warn&quot;&gt;2 parts&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Retro computers&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;retro-computers&quot;&gt;
      &lt;a href=&quot;/models/retro-computers/sinclair-zx-spectrum-48k-multiview/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/retro-computers/sinclair-zx-spectrum-48k-multiview/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Sinclair ZX Spectrum 48K MultiView&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Multi-image attempt at the ZX Spectrum — flat slab with garbled keys.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 65 × 9 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Retro computers&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;retro-computers&quot;&gt;
      &lt;a href=&quot;/models/retro-computers/sinclair-zx-spectrum-48k-v1/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/retro-computers/sinclair-zx-spectrum-48k-v1/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Sinclair ZX Spectrum 48K&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Sinclair ZX Spectrum 48K with grey rubber keys and the rainbow stripe.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 68 × 9 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Retro computers&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;retro-computers&quot;&gt;
      &lt;a href=&quot;/models/retro-computers/sinclair-zx-spectrum-48k-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/retro-computers/sinclair-zx-spectrum-48k-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Sinclair ZX Spectrum 48K (second version)&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Second ZX Spectrum generation — taller wedge, same rubber keys and rainbow.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 65 × 15 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Retro computers&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;characters&quot;&gt;
      &lt;a href=&quot;/models/characters/andrea-chibi-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/characters/andrea-chibi-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Andrea Chibi Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Chibi woman with hands over her mouth in surprise, on a rocky clifftop base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;79 × 76 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Characters&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;characters&quot;&gt;
      &lt;a href=&quot;/models/characters/coworker-cartoon-astronaut-model-v1/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/characters/coworker-cartoon-astronaut-model-v1/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Cartoon Astronaut&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Bearded man in a white NASA-style spacesuit, helmet visor open, with flag and mission patches.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;64 × 36 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Characters&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;characters&quot;&gt;
      &lt;a href=&quot;/models/characters/coworker-cartoon-astronaut-model-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/characters/coworker-cartoon-astronaut-model-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Cartoon Astronaut (second version)&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Second generation of the spacesuit figure — same pose, untextured.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;64 × 36 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Characters&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;characters&quot;&gt;
      &lt;a href=&quot;/models/characters/nate-chibi-figure/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/characters/nate-chibi-figure/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Nate Chibi Figure&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Chibi man kneeling with an open ring box, on a rocky clifftop base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;92 × 48 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Characters&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;characters&quot;&gt;
      &lt;a href=&quot;/models/characters/samus-power-suit-model/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/characters/samus-power-suit-model/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Samus Power Suit Model&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Cartoonish woman in Samus Aran&amp;#39;s orange Power Suit, long hair down, helmet under arm.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;59 × 25 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Characters&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;characters&quot;&gt;
      &lt;a href=&quot;/models/characters/samus-power-suit-model-clawclip/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/characters/samus-power-suit-model-clawclip/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Samus Power Suit Model Clawclip&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;The Samus Power Suit figure with hair pinned up in a claw clip.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;59 × 25 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Characters&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;chibi-beach-girl&quot;&gt;
      &lt;a href=&quot;/models/chibi-beach-girl/chibi-beach-girl-flowing-dress/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/chibi-beach-girl/chibi-beach-girl-flowing-dress/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Chibi Beach Girl Flowing Dress&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Chibi beach girl in a flowing pastel floral summer dress and sandals.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 48 × 51 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Chibi beach girl&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;chibi-beach-girl&quot;&gt;
      &lt;a href=&quot;/models/chibi-beach-girl/chibi-beach-girl-fixed-arm/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/chibi-beach-girl/chibi-beach-girl-fixed-arm/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Chibi Beach Girl Fixed Arm&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Chibi beach girl with the free arm relaxed at her side.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;50 × 52 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill warn&quot;&gt;2 parts&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Chibi beach girl&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

  &lt;/ul&gt;
  &lt;p class=&quot;empty&quot; data-mg-none hidden&gt;No models in this project.&lt;/p&gt;
&lt;/div&gt;
&lt;dialog class=&quot;mg-dialog&quot; data-mg-dialog aria-label=&quot;Model&quot;&gt;
  &lt;div class=&quot;mg-dialog-bar&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;mg-close&quot; data-mg-close aria-label=&quot;Close&quot;&gt;Close &lt;kbd&gt;Esc&lt;/kbd&gt;&lt;/button&gt;&lt;/div&gt;
  &lt;div data-mg-dialog-body&gt;&lt;/div&gt;
&lt;/dialog&gt;

&lt;script src=&quot;/js/models.js&quot; defer&gt;&lt;/script&gt;</content>
  </entry>
  
  <entry>
    <title>AI Force</title>
    <link href="https://geekychris.github.io/articles/ai-force/"/>
    <id>https://geekychris.github.io/articles/ai-force/</id>
    <updated>2026-10-18T00:00:00Z</updated>
    <summary>The administration is starting a new military force, so I designed the one I&#39;d expect: the AI Force — a squad of very typical geeks in hero suits, with a nod to Atari Force from the 1980s comics.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The administration is starting a new military force, and it got me wondering what an &lt;strong&gt;AI Force&lt;/strong&gt;
would actually look like. Not the sleek recruiting-poster version — the real one. So I made a
squad of them in Meshy, and they look like the people who would really sign up: typical geeks,
squeezed into black hero suits with glowing circuit lines.&lt;/p&gt;
&lt;/div&gt;
&lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/@google/model-viewer@4/dist/model-viewer.min.js&quot;&gt;&lt;/script&gt;
&lt;figure class=&quot;model-figure&quot;&gt;
&lt;model-viewer src=&quot;/models/ai-force-pudgy-guy-textured/ai-force-pudgy-guy-textured.glb&quot; alt=&quot;3-D model: Re-textured (realistic, 4K) version of the running AI Force figure.&quot; camera-controls touch-action=&quot;pan-y&quot; interaction-prompt=&quot;auto&quot; shadow-intensity=&quot;0.8&quot; exposure=&quot;1&quot; loading=&quot;lazy&quot; reveal=&quot;auto&quot; poster=&quot;/models/ai-force-pudgy-guy-textured/poster.webp&quot; auto-rotate auto-rotate-delay=&quot;1500&quot; rotation-per-second=&quot;18deg&quot; ar ar-modes=&quot;webxr scene-viewer quick-look&quot; ar-scale=&quot;auto&quot; ios-src=&quot;/models/ai-force-pudgy-guy-textured/ai-force-pudgy-guy-textured.usdz&quot;&gt;&lt;img class=&quot;model-poster&quot; slot=&quot;poster&quot; src=&quot;/models/ai-force-pudgy-guy-textured/poster.webp&quot; alt=&quot;3-D model: Re-textured (realistic, 4K) version of the running AI Force figure.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/model-viewer&gt;
&lt;figcaption&gt;&lt;span class=&quot;model-title&quot;&gt;The Pudgy Guy&lt;/span&gt;&lt;span class=&quot;model-facts&quot;&gt;printed 100 × 71 × 44 mm&lt;/span&gt;&lt;span class=&quot;model-hint&quot;&gt;Drag to turn · pinch to zoom · on a phone, view it in your room&lt;/span&gt;&lt;noscript&gt;&lt;a class=&quot;model-file&quot; href=&quot;/models/ai-force-pudgy-guy-textured/ai-force-pudgy-guy-textured.glb&quot;&gt;3-D file (GLB)&lt;/a&gt;&lt;/noscript&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-squad&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-squad&quot;&gt;&lt;/a&gt;The squad&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Big Brain&lt;/strong&gt; — the strategist. Most of him is head.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Neckbeard&lt;/strong&gt; — arms crossed, in two uniforms: the hero suit, and the blue polo and chinos he
wears the rest of the time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pudgy Guy&lt;/strong&gt; — glasses on, running. Probably towards the snacks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Python Woman&lt;/strong&gt; — curly hair, flexing a bicep. She wrote the code everyone else depends on.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The Teen&lt;/strong&gt; — grinning, fist in the air, already better at this than the rest of them.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Each one comes in two versions: Meshy&amp;#8217;s first texture, and a re-textured, more realistic 4K one.&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;model-figure&quot;&gt;
&lt;model-viewer src=&quot;/models/ai-force-python-woman-textured/ai-force-python-woman-textured.glb&quot; alt=&quot;3-D model: Re-textured (realistic, 4K) version of the curly-haired AI Force heroine.&quot; camera-controls touch-action=&quot;pan-y&quot; interaction-prompt=&quot;auto&quot; shadow-intensity=&quot;0.8&quot; exposure=&quot;1&quot; loading=&quot;lazy&quot; reveal=&quot;auto&quot; poster=&quot;/models/ai-force-python-woman-textured/poster.webp&quot; auto-rotate auto-rotate-delay=&quot;1500&quot; rotation-per-second=&quot;18deg&quot; ar ar-modes=&quot;webxr scene-viewer quick-look&quot; ar-scale=&quot;auto&quot; ios-src=&quot;/models/ai-force-python-woman-textured/ai-force-python-woman-textured.usdz&quot;&gt;&lt;img class=&quot;model-poster&quot; slot=&quot;poster&quot; src=&quot;/models/ai-force-python-woman-textured/poster.webp&quot; alt=&quot;3-D model: Re-textured (realistic, 4K) version of the curly-haired AI Force heroine.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/model-viewer&gt;
&lt;figcaption&gt;&lt;span class=&quot;model-title&quot;&gt;Python Woman&lt;/span&gt;&lt;span class=&quot;model-facts&quot;&gt;printed 100 × 62 × 26 mm&lt;/span&gt;&lt;span class=&quot;model-hint&quot;&gt;Drag to turn · pinch to zoom · on a phone, view it in your room&lt;/span&gt;&lt;noscript&gt;&lt;a class=&quot;model-file&quot; href=&quot;/models/ai-force-python-woman-textured/ai-force-python-woman-textured.glb&quot;&gt;3-D file (GLB)&lt;/a&gt;&lt;/noscript&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Open any of them below to see it from every side.&lt;/p&gt;
&lt;/div&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/css/models.css&quot;&gt;

&lt;div class=&quot;mg mg-embed&quot; data-mg-index&gt;
  &lt;p class=&quot;mg-count&quot; aria-live=&quot;polite&quot;&gt;&lt;span data-mg-count&gt;12&lt;/span&gt; &lt;span data-mg-count-of&gt;models&lt;/span&gt;&lt;/p&gt;

  &lt;ul class=&quot;mg-grid&quot; data-mg-grid&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-big-brain/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-big-brain/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Big Brain&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Big-brained hero in a black circuit-line bodysuit, fists up.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;60 × 26 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-big-brain-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-big-brain-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Big Brain Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the AI Force Big Brain figure.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;60 × 26 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-neckbeard-casual/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-neckbeard-casual/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Neckbeard Casual&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Bearded man in a blue polo and chinos, arms crossed.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;32 × 22 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-neckbeard-casual-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-neckbeard-casual-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Neckbeard Casual Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the casual bearded AI Force figure.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;32 × 22 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-neckbeard-suited/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-neckbeard-suited/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Neckbeard Suited&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;The bearded AI Force figure in the black circuit-line hero suit, arms crossed.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;50 × 25 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-neckbeard-suited-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-neckbeard-suited-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Neckbeard Suited Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the suited bearded AI Force figure.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;50 × 25 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-pudgy-guy/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-pudgy-guy/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Pudgy Guy&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Heavy-set AI Force hero in glasses, running.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 71 × 44 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-pudgy-guy-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-pudgy-guy-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Pudgy Guy Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the running AI Force figure.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 71 × 44 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-python-woman/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-python-woman/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Python Woman&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Curly-haired AI Force heroine flexing a bicep.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 62 × 26 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-python-woman-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-python-woman-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Python Woman Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the curly-haired AI Force heroine.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 62 × 26 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-teen/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-teen/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Teen&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grinning teenage AI Force member with a fist in the air.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;49 × 18 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;ai-force&quot;&gt;
      &lt;a href=&quot;/models/ai-force/ai-force-teen-textured/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/ai-force/ai-force-teen-textured/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;AI Force Teen Textured&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Re-textured (realistic, 4K) version of the AI Force teen.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;49 × 18 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;AI Force&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

  &lt;/ul&gt;
  &lt;p class=&quot;empty&quot; data-mg-none hidden&gt;No models in this project.&lt;/p&gt;
&lt;/div&gt;
&lt;dialog class=&quot;mg-dialog&quot; data-mg-dialog aria-label=&quot;Model&quot;&gt;
  &lt;div class=&quot;mg-dialog-bar&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;mg-close&quot; data-mg-close aria-label=&quot;Close&quot;&gt;Close &lt;kbd&gt;Esc&lt;/kbd&gt;&lt;/button&gt;&lt;/div&gt;
  &lt;div data-mg-dialog-body&gt;&lt;/div&gt;
&lt;/dialog&gt;

&lt;script src=&quot;/js/models.js&quot; defer&gt;&lt;/script&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a-nod-to-atari-force&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#a-nod-to-atari-force&quot;&gt;&lt;/a&gt;A nod to Atari Force&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The look is a homage to &lt;strong&gt;Atari Force&lt;/strong&gt;, the comic DC Comics made for Atari in the 1980s: a
crew in matching jumpsuits striking heroic poses. I made the original crew as well — the AI Force
is what you get when you send the same poses to a hackathon.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;a class=&quot;image&quot; href=&quot;/models/atari-force/martin-champion/&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/martin-champion/hero.webp&quot; alt=&quot;Martin Champion, Atari Force crew member&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Martin Champion&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;a class=&quot;image&quot; href=&quot;/models/atari-force/lydia-perez/&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/lydia-perez/hero.webp&quot; alt=&quot;Lydia Perez, Atari Force crew member&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Lydia Perez&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;a class=&quot;image&quot; href=&quot;/models/atari-force/dr-lucas-orion/&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/dr-lucas-orion/hero.webp&quot; alt=&quot;Dr Lucas Orion, Atari Force crew member&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Dr Lucas Orion&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;a class=&quot;image&quot; href=&quot;/models/atari-force/li-san-orourke/&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/li-san-orourke/hero.webp&quot; alt=&quot;Li-San O&amp;#8217;Rourke, Atari Force crew member&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Li-San O&amp;#8217;Rourke&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;a class=&quot;image&quot; href=&quot;/models/atari-force/mohandas-singh/&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/mohandas-singh/hero.webp&quot; alt=&quot;Mohandas Singh, Atari Force crew member&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Mohandas Singh&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;a class=&quot;image&quot; href=&quot;/models/atari-force/sixth-member/&quot;&gt;&lt;img src=&quot;/model-gallery/atari-force/sixth-member/hero.webp&quot; alt=&quot;The sixth crew member, Atari Force crew member&quot;&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The sixth crew member&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;All of these, and the rest of my models, are in &lt;a href=&quot;/articles/3d-models/&quot;&gt;3-D models&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Aliens in my house</title>
    <link href="https://geekychris.github.io/articles/aliens-in-my-house/"/>
    <id>https://geekychris.github.io/articles/aliens-in-my-house/</id>
    <updated>2026-10-18T00:00:00Z</updated>
    <summary>I&#39;m a big fan of the alien genre, so I wanted some funny aliens in my house: greys drinking coffee, on the phone, and doing see-no-evil — modelled in Meshy, printed, and hand painted.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I&amp;#8217;m a big fan of the alien genre, and I wanted some funny aliens around the house. So I made a
little crowd of them in Meshy, printed them, and painted them by hand with airbrush &quot;felt tip
pens&quot; (yes, that is a thing).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;story-img5.jpg&quot; alt=&quot;Three grey aliens sitting on the edge of a glass shelf: one covering its eyes, one its mouth, one its ears&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The true take on see no evil, speak no evil and hear no evil.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-morning-crew&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-morning-crew&quot;&gt;&lt;/a&gt;The morning crew&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock text-center&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;story-img1.jpg&quot; alt=&quot;A grey alien on a rocky base, drinking from a green mug, hand on hip&quot; width=&quot;440&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;A nice cup of Joe in the morning provides clarity for today&amp;#8217;s probing.&lt;/div&gt;
&lt;/div&gt;
&lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/@google/model-viewer@4/dist/model-viewer.min.js&quot;&gt;&lt;/script&gt;
&lt;figure class=&quot;model-figure&quot;&gt;
&lt;model-viewer src=&quot;/models/gray-alien-coffee-v2/gray-alien-coffee-v2.glb&quot; alt=&quot;3-D model: Grey alien sipping coffee on a rocky round base.&quot; camera-controls touch-action=&quot;pan-y&quot; interaction-prompt=&quot;auto&quot; shadow-intensity=&quot;0.8&quot; exposure=&quot;1&quot; loading=&quot;lazy&quot; reveal=&quot;auto&quot; poster=&quot;/models/gray-alien-coffee-v2/poster.webp&quot; auto-rotate auto-rotate-delay=&quot;1500&quot; rotation-per-second=&quot;18deg&quot; ar ar-modes=&quot;webxr scene-viewer quick-look&quot; ar-scale=&quot;auto&quot; ios-src=&quot;/models/gray-alien-coffee-v2/gray-alien-coffee-v2.usdz&quot;&gt;&lt;img class=&quot;model-poster&quot; slot=&quot;poster&quot; src=&quot;/models/gray-alien-coffee-v2/poster.webp&quot; alt=&quot;3-D model: Grey alien sipping coffee on a rocky round base.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/model-viewer&gt;
&lt;figcaption&gt;&lt;span class=&quot;model-title&quot;&gt;The coffee alien — drag to turn it&lt;/span&gt;&lt;span class=&quot;model-facts&quot;&gt;printed 45 × 44 × 100 mm&lt;/span&gt;&lt;span class=&quot;model-hint&quot;&gt;Drag to turn · pinch to zoom · on a phone, view it in your room&lt;/span&gt;&lt;noscript&gt;&lt;a class=&quot;model-file&quot; href=&quot;/models/gray-alien-coffee-v2/gray-alien-coffee-v2.glb&quot;&gt;3-D file (GLB)&lt;/a&gt;&lt;/noscript&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;imageblock text-center&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;story-img2.jpg&quot; alt=&quot;A grey alien on a rocky base, holding out one long hand&quot; width=&quot;440&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Back off… aliens need space too!&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock text-center&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;story-img3.jpg&quot; alt=&quot;A grey alien on a rocky base, talking on a mobile phone&quot; width=&quot;440&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;E.T. phone home.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;not-all-of-them-are-friendly&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#not-all-of-them-are-friendly&quot;&gt;&lt;/a&gt;Not all of them are friendly&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock text-center&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;story-img4.jpg&quot; alt=&quot;A green alien in cargo shorts and flip-flops, giving the finger&quot; width=&quot;440&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Pan-galactic greeting.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;see-no-evil&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#see-no-evil&quot;&gt;&lt;/a&gt;See no evil&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The trio on my shelf, as models:&lt;/p&gt;
&lt;/div&gt;
&lt;figure class=&quot;model-figure&quot;&gt;
&lt;model-viewer src=&quot;/models/alien-see-no-evil/alien-see-no-evil.glb&quot; alt=&quot;3-D model: Seated grey alien covering its eyes — &amp;#39;see no evil&amp;#39;.&quot; camera-controls touch-action=&quot;pan-y&quot; interaction-prompt=&quot;auto&quot; shadow-intensity=&quot;0.8&quot; exposure=&quot;1&quot; loading=&quot;lazy&quot; reveal=&quot;auto&quot; poster=&quot;/models/alien-see-no-evil/poster.webp&quot; auto-rotate auto-rotate-delay=&quot;1500&quot; rotation-per-second=&quot;18deg&quot; ar ar-modes=&quot;webxr scene-viewer quick-look&quot; ar-scale=&quot;auto&quot; ios-src=&quot;/models/alien-see-no-evil/alien-see-no-evil.usdz&quot;&gt;&lt;img class=&quot;model-poster&quot; slot=&quot;poster&quot; src=&quot;/models/alien-see-no-evil/poster.webp&quot; alt=&quot;3-D model: Seated grey alien covering its eyes — &amp;#39;see no evil&amp;#39;.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/model-viewer&gt;
&lt;figcaption&gt;&lt;span class=&quot;model-title&quot;&gt;See no evil&lt;/span&gt;&lt;span class=&quot;model-facts&quot;&gt;printed 100 × 32 × 33 mm&lt;/span&gt;&lt;span class=&quot;model-hint&quot;&gt;Drag to turn · pinch to zoom · on a phone, view it in your room&lt;/span&gt;&lt;noscript&gt;&lt;a class=&quot;model-file&quot; href=&quot;/models/alien-see-no-evil/alien-see-no-evil.glb&quot;&gt;3-D file (GLB)&lt;/a&gt;&lt;/noscript&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;All my aliens — including the park bench they like to sit on — are below; open one to see it
from every side.&lt;/p&gt;
&lt;/div&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;/css/models.css&quot;&gt;

&lt;div class=&quot;mg mg-embed&quot; data-mg-index&gt;
  &lt;p class=&quot;mg-count&quot; aria-live=&quot;polite&quot;&gt;&lt;span data-mg-count&gt;12&lt;/span&gt; &lt;span data-mg-count-of&gt;models&lt;/span&gt;&lt;/p&gt;

  &lt;ul class=&quot;mg-grid&quot; data-mg-grid&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/alien-figurine-middle-finger/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/alien-figurine-middle-finger/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Alien Figurine Middle Finger&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien in board shorts and flip-flops giving the finger, on a round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;35 × 35 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/alien-hear-no-evil/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/alien-hear-no-evil/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Alien Hear No Evil&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Seated grey alien covering its ears — the &amp;#39;hear no evil&amp;#39; of a trio.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 33 × 36 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/alien-see-no-evil/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/alien-see-no-evil/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Alien See No Evil&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Seated grey alien covering its eyes — &amp;#39;see no evil&amp;#39;.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 32 × 33 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/alien-speak-no-evil/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/alien-speak-no-evil/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Alien Speak No Evil&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Seated grey alien covering its mouth — &amp;#39;speak no evil&amp;#39;.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 26 × 27 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Barefoot grey alien reaching out with one long arm.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;59 × 27 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-coffee/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-coffee/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Coffee&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Barefoot grey alien sipping from a coffee mug, hand on hip.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;34 × 23 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-coffee-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-coffee-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Coffee V2&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien sipping coffee on a rocky round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;45 × 44 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-phone/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-phone/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Phone&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Barefoot grey alien chatting on a mobile phone.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;28 × 26 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-phone-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-phone-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Phone V2&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien on the phone, standing on a rocky round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;46 × 46 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-picking-nose/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-picking-nose/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Picking Nose&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien picking its nose, on a rocky round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;46 × 45 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/gray-alien-waving-v2/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/gray-alien-waving-v2/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Gray Alien Waving V2&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Grey alien reaching out a hand, on a rocky round base.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;63 × 51 × 100 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

    &lt;li class=&quot;mg-card&quot; data-project=&quot;aliens&quot;&gt;
      &lt;a href=&quot;/models/aliens/weathered-park-bench/&quot; data-mg-open&gt;
        &lt;span class=&quot;mg-thumb&quot;&gt;&lt;img src=&quot;/model-gallery/aliens/weathered-park-bench/thumb.webp&quot; alt=&quot;&quot; width=&quot;240&quot; height=&quot;240&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;mg-body&quot;&gt;
          &lt;strong class=&quot;mg-name&quot;&gt;Weathered Park Bench&lt;/strong&gt;
          &lt;span class=&quot;mg-sum&quot;&gt;Weathered park bench with wooden slats and cast-iron ends.&lt;/span&gt;
          &lt;span class=&quot;mg-facts&quot;&gt;&lt;span&gt;100 × 38 × 48 mm&lt;/span&gt;&lt;span class=&quot;mg-pill&quot;&gt;Aliens&lt;/span&gt;&lt;/span&gt;
        &lt;/span&gt;
      &lt;/a&gt;
    &lt;/li&gt;

  &lt;/ul&gt;
  &lt;p class=&quot;empty&quot; data-mg-none hidden&gt;No models in this project.&lt;/p&gt;
&lt;/div&gt;
&lt;dialog class=&quot;mg-dialog&quot; data-mg-dialog aria-label=&quot;Model&quot;&gt;
  &lt;div class=&quot;mg-dialog-bar&quot;&gt;&lt;button type=&quot;button&quot; class=&quot;mg-close&quot; data-mg-close aria-label=&quot;Close&quot;&gt;Close &lt;kbd&gt;Esc&lt;/kbd&gt;&lt;/button&gt;&lt;/div&gt;
  &lt;div data-mg-dialog-body&gt;&lt;/div&gt;
&lt;/dialog&gt;

&lt;script src=&quot;/js/models.js&quot; defer&gt;&lt;/script&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Space: 1999</title>
    <link href="https://geekychris.github.io/articles/space-1999/"/>
    <id>https://geekychris.github.io/articles/space-1999/</id>
    <updated>2026-10-18T00:00:00Z</updated>
    <summary>Space: 1999 was a show I loved watching as a kid, so two of my 3-D models are from it: an Eagle Transporter and a Moonbase Alpha astronaut.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;em&gt;Space: 1999&lt;/em&gt; was one of the shows I loved to watch as a kid: the Moon blown out of Earth&amp;#8217;s orbit,
the crew of Moonbase Alpha along for the ride, and the Eagle Transporters — the best spaceships
on television. So when I started making 3-D models, a couple of them had to come from it.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-eagle-transporter&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-eagle-transporter&quot;&gt;&lt;/a&gt;The Eagle Transporter&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;script type=&quot;module&quot; src=&quot;https://cdn.jsdelivr.net/npm/@google/model-viewer@4/dist/model-viewer.min.js&quot;&gt;&lt;/script&gt;
&lt;figure class=&quot;model-figure&quot;&gt;
&lt;model-viewer src=&quot;/models/eagle-transporter-v2/eagle-transporter-v2.glb&quot; alt=&quot;3-D model: Space: 1999 Eagle Transporter (single image) — all in one piece.&quot; camera-controls touch-action=&quot;pan-y&quot; interaction-prompt=&quot;auto&quot; shadow-intensity=&quot;0.8&quot; exposure=&quot;1&quot; loading=&quot;lazy&quot; reveal=&quot;auto&quot; poster=&quot;/models/eagle-transporter-v2/poster.webp&quot; auto-rotate auto-rotate-delay=&quot;1500&quot; rotation-per-second=&quot;18deg&quot; ar ar-modes=&quot;webxr scene-viewer quick-look&quot; ar-scale=&quot;auto&quot; ios-src=&quot;/models/eagle-transporter-v2/eagle-transporter-v2.usdz&quot;&gt;&lt;img class=&quot;model-poster&quot; slot=&quot;poster&quot; src=&quot;/models/eagle-transporter-v2/poster.webp&quot; alt=&quot;3-D model: Space: 1999 Eagle Transporter (single image) — all in one piece.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/model-viewer&gt;
&lt;figcaption&gt;&lt;span class=&quot;model-title&quot;&gt;The Eagle Transporter — drag to turn it&lt;/span&gt;&lt;span class=&quot;model-facts&quot;&gt;printed 100 × 46 × 20 mm&lt;/span&gt;&lt;span class=&quot;model-hint&quot;&gt;Drag to turn · pinch to zoom · on a phone, view it in your room&lt;/span&gt;&lt;noscript&gt;&lt;a class=&quot;model-file&quot; href=&quot;/models/eagle-transporter-v2/eagle-transporter-v2.glb&quot;&gt;3-D file (GLB)&lt;/a&gt;&lt;/noscript&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I made the Eagle twice. This one is a single piece, made from one picture. The
&lt;a href=&quot;/models/space/eagle-transporter/&quot;&gt;other version&lt;/a&gt; was made from several pictures, and prints
as five parts on one plate: the hull and the four leg pods.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a-moonbase-alpha-astronaut&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#a-moonbase-alpha-astronaut&quot;&gt;&lt;/a&gt;A Moonbase Alpha astronaut&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;figure class=&quot;model-figure&quot;&gt;
&lt;model-viewer src=&quot;/models/space1999-astronaut-full/space1999-astronaut-full.glb&quot; alt=&quot;3-D model: Multi-image version of the Space: 1999 astronaut, with the backpack modelled.&quot; camera-controls touch-action=&quot;pan-y&quot; interaction-prompt=&quot;auto&quot; shadow-intensity=&quot;0.8&quot; exposure=&quot;1&quot; loading=&quot;lazy&quot; reveal=&quot;auto&quot; poster=&quot;/models/space1999-astronaut-full/poster.webp&quot; auto-rotate auto-rotate-delay=&quot;1500&quot; rotation-per-second=&quot;18deg&quot; ar ar-modes=&quot;webxr scene-viewer quick-look&quot; ar-scale=&quot;auto&quot; ios-src=&quot;/models/space1999-astronaut-full/space1999-astronaut-full.usdz&quot;&gt;&lt;img class=&quot;model-poster&quot; slot=&quot;poster&quot; src=&quot;/models/space1999-astronaut-full/poster.webp&quot; alt=&quot;3-D model: Multi-image version of the Space: 1999 astronaut, with the backpack modelled.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot;&gt;&lt;/model-viewer&gt;
&lt;figcaption&gt;&lt;span class=&quot;model-title&quot;&gt;An astronaut in the orange suit and yellow helmet&lt;/span&gt;&lt;span class=&quot;model-facts&quot;&gt;printed 33 × 27 × 100 mm&lt;/span&gt;&lt;span class=&quot;model-hint&quot;&gt;Drag to turn · pinch to zoom · on a phone, view it in your room&lt;/span&gt;&lt;noscript&gt;&lt;a class=&quot;model-file&quot; href=&quot;/models/space1999-astronaut-full/space1999-astronaut-full.glb&quot;&gt;3-D file (GLB)&lt;/a&gt;&lt;/noscript&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The astronaut is in the show&amp;#8217;s orange spacesuit and yellow helmet. This version was made from
several pictures, so the backpack is modelled too; the
&lt;a href=&quot;/models/space/space1999-astronaut/&quot;&gt;first version&lt;/a&gt; was made from one.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The rest of my spacecraft — an Apollo spacesuit and Blake&amp;#8217;s 7&amp;#8217;s Liberator among them — are with
everything else in &lt;a href=&quot;/articles/3d-models/#space&quot;&gt;3-D models&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>gitwebsite is now open source</title>
    <link href="https://geekychris.github.io/articles/gitwebsite-is-now-open-source/"/>
    <id>https://geekychris.github.io/articles/gitwebsite-is-now-open-source/</id>
    <updated>2026-10-12T00:00:00Z</updated>
    <summary>The engine behind this site — a Rust static-site generator with a CLI, REST API, MCP server, and desktop app — is now on GitHub under the MIT licence.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;/projects/gitwebsite/&quot;&gt;gitwebsite&lt;/a&gt; is the tool that builds and publishes this site. Until
today the engine and my site&amp;#8217;s content lived in one private repository. I&amp;#8217;ve split them: the
engine is now public at &lt;a href=&quot;https://github.com/geekychris/gitwebsite&quot;&gt;github.com/geekychris/gitwebsite&lt;/a&gt;
under the MIT licence, and my own site&amp;#8217;s source stays in a separate private repository.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;whats-in-it&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#whats-in-it&quot;&gt;&lt;/a&gt;What&amp;#8217;s in it&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A static-site engine in Rust.&lt;/strong&gt; Content is AsciiDoc with a short header (type, date, areas,
tags, summary). Items sit in a tree of &lt;strong&gt;areas&lt;/strong&gt; you define, so one post can belong to several.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;gw&lt;/code&gt;, a command-line tool,&lt;/strong&gt; for creating, checking, building, previewing, and publishing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A REST API and a desktop app&lt;/strong&gt; (Tauri) with a management UI and a live preview. The app has
a 🎤 button beside every text box for macOS dictation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;An MCP server,&lt;/strong&gt; so an AI assistant can work with the site through the same operations — plus
a set of Claude Code skills for writing, curating, adding media, and publishing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A registry, ideas list, and media library:&lt;/strong&gt; where each piece of work came from, what to write
next, and images (including SmugMug-hosted photos).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Publishing to GitHub Pages,&lt;/strong&gt; leaving other folders in the Pages repository alone.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/geekychris/gitwebsite/main/docs/images/app-editor.png&quot; alt=&quot;The gitwebsite app&amp;#8217;s editor&quot; width=&quot;with AsciiDoc on the left and the rendered page on the right&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. The app: editing a post beside a live preview (on a demo site)&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;writing-with-claude&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#writing-with-claude&quot;&gt;&lt;/a&gt;Writing with Claude&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Two workflows use the MCP server and the app together:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Project stories.&lt;/strong&gt; You drop in photos, choose the ones that matter with a comment each, and
tell the story roughly; Claude interviews you a few questions at a time and drafts the post.
The questions appear in the app, so you can answer there.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enhance with Claude.&lt;/strong&gt; Open an existing article or project and press &lt;strong&gt;Enhance with Claude&lt;/strong&gt;.
You answer whichever key questions you like — the inspiration, why it exists, the hard parts,
what&amp;#8217;s next — and Claude reads the post and the code repository behind it, asks follow-ups, and
writes a proposed revision. The app shows it as a diff against the current post; you accept it,
ask for changes, or discard it. Nothing is published until you publish.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/geekychris/gitwebsite/main/docs/images/app-enhance.png&quot; alt=&quot;An enhancement in progress in the gitwebsite app&quot; width=&quot;with a question from Claude&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. Enhance with Claude: a question waiting on the page, and the proposal as a diff&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/geekychris/gitwebsite/main/docs/images/app-proposal.png&quot; alt=&quot;A proposed revision shown as a diff against the post&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;using-it-for-your-own-site&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#using-it-for-your-own-site&quot;&gt;&lt;/a&gt;Using it for your own site&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;gw init&lt;/code&gt; creates a new site with a default theme and starter content, ready to check, build,
and publish. A settings file keeps a list of sites, so the app can switch between several. The
repository&amp;#8217;s README covers building from source, setting up GitHub Pages, and installing the app
and the Claude Code skills.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Some things here are specific to my site and aren&amp;#8217;t part of the engine — the
&lt;a href=&quot;/tools/&quot;&gt;browser tools&lt;/a&gt;, for example, live in my site&amp;#8217;s own repository. The projects page
grouped by area (Software, Retro computing, …) is an engine feature you can configure in
&lt;code&gt;site.yaml&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/geekychris/gitwebsite/main/docs/images/site-projects.png&quot; alt=&quot;A projects page grouped into Software&quot; width=&quot;Making&quot; height=&quot;and Retro computing&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 3. The default theme&amp;#8217;s projects page, grouped by area&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>AOEE: a TAO-style social graph cache in Rust</title>
    <link href="https://geekychris.github.io/articles/aoee-a-tao-style-social-graph-cache-in-rust/"/>
    <id>https://geekychris.github.io/articles/aoee-a-tao-style-social-graph-cache-in-rust/</id>
    <updated>2026-10-10T00:00:00Z</updated>
    <summary>How I built AOEE, an in-memory relationship cache for social-graph queries: posting lists borrowed from search engines, four adaptive encodings, set maths instead of joins, and friend-of-friend suggestions with a budget.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Every social product eventually asks the same handful of questions, millions of times a
second. Who does this person follow? Do these two people know each other? Which friends do
we have in common? Who liked this post, and how? Who might this person want to meet next?&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;None of those questions is hard on its own. What makes them hard is the shape of the data:
a few accounts with millions of edges, millions of accounts with a few dozen, and a workload
that is overwhelmingly reads. Asking a relational database to answer &quot;mutual friends&quot; with
a self-join on an edges table works right up until it doesn&amp;#8217;t.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Facebook&amp;#8217;s answer, described in the 2013 USENIX paper on &lt;strong&gt;TAO&lt;/strong&gt;, was to treat the graph as
a cache of &lt;strong&gt;associations&lt;/strong&gt; in front of MySQL and to serve the read patterns directly. AOEE —
Attribute Object Enterprise Edition — is my take on that idea, built as the graph layer for
the &lt;a href=&quot;/projects/enterprise-social-platform/&quot;&gt;enterprise social platform&lt;/a&gt; I was writing
about in &lt;a href=&quot;/articles/building-a-full-social-network-from-scratch-feed-friends-groups-dms-search/&quot;&gt;another article&lt;/a&gt;.
The code is on &lt;a href=&quot;https://github.com/geekychris/AOEE_Attribute_object_enterprise_edition&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;architecture.svg&quot; alt=&quot;AOEE architecture: React UI&quot; width=&quot;Spring Boot proxy&quot; height=&quot;Java gRPC client&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. How a request reaches the graph: the JVM side handles HTTP, datasets, and benchmarks; the graph lives in a sharded Rust server behind gRPC.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-data-model-ids-edge-types-and-edge-keys&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-data-model-ids-edge-types-and-edge-keys&quot;&gt;&lt;/a&gt;The data model: ids, edge types, and edge keys&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Everything in AOEE is a 64-bit &lt;code&gt;EntityId&lt;/code&gt;. The top 16 bits hold the entity &lt;strong&gt;type&lt;/strong&gt; (user,
post, comment, photo, group, page, event, tag, or a custom type) and the bottom 48 bits the
id itself. Knowing what an id refers to costs a shift, not a lookup, and ids stay plain
integers — which matters a great deal for compression, as we&amp;#8217;ll see.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Ids can also be &lt;strong&gt;generated&lt;/strong&gt; without a coordinator: the 48-bit part becomes a 32-bit
timestamp (seconds since a 2020 epoch) followed by a 16-bit sequence. Any client in any
language can mint them.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Relationships are typed, and almost every type comes as a forward/reverse pair:
&lt;code&gt;FOLLOWS&lt;/code&gt;/&lt;code&gt;FOLLOWED_BY&lt;/code&gt;, &lt;code&gt;LIKES&lt;/code&gt;/&lt;code&gt;LIKED_BY&lt;/code&gt;, &lt;code&gt;AUTHORED&lt;/code&gt;/&lt;code&gt;AUTHORED_BY&lt;/code&gt;, &lt;code&gt;MEMBER_OF&lt;/code&gt;/
&lt;code&gt;HAS_MEMBER&lt;/code&gt;, &lt;code&gt;TAGGED_IN&lt;/code&gt;/&lt;code&gt;HAS_TAG&lt;/code&gt;, and so on. &lt;code&gt;FRIEND_OF&lt;/code&gt; is its own reverse. The
clients know each type&amp;#8217;s reverse, so an application stores both directions and &quot;who follows
Alice?&quot; is as cheap as &quot;whom does Alice follow?&quot; — no reverse index to scan. A few types carry a one-byte metadata value per edge — &lt;code&gt;LIKES&lt;/code&gt; uses it for the
reaction (like, love, haha, wow, sad, angry).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The unit of storage is the &lt;strong&gt;posting list&lt;/strong&gt; for an &lt;code&gt;EdgeKey&lt;/code&gt; — a source entity plus an edge
type. &lt;code&gt;EdgeKey(User:42, FOLLOWS)&lt;/code&gt; is the sorted list of every entity user 42 follows.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;That word &lt;strong&gt;posting list&lt;/strong&gt; is deliberate. Most of my career has been in search, and a search
engine&amp;#8217;s inverted index is exactly this: for each term, a sorted list of document ids,
compressed, with fast intersection. Swap &quot;term&quot; for &quot;user and relationship&quot; and the whole
toolbox comes across — delta encoding, variable-length integers, skip tables, galloping
intersection. AOEE is, in a sense, a search engine whose documents are people.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;posting-lists-an-lsm-tree-in-miniature&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#posting-lists-an-lsm-tree-in-miniature&quot;&gt;&lt;/a&gt;Posting lists: an LSM tree in miniature&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Graphs are mostly read, but they are written constantly, and a perfectly compressed sorted
array is miserable to insert into. So each posting list is a tiny log-structured merge tree.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;posting-list.svg&quot; alt=&quot;Posting list anatomy: write buffer&quot; width=&quot;compaction&quot; height=&quot;encoded segments&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. A posting list is an append-only write buffer plus immutable, sorted, encoded segments; compaction moves one into the other.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;New edges and deletes go into an unsorted &lt;strong&gt;write buffer&lt;/strong&gt;: the destination id, a nanosecond
timestamp, the metadata byte, and a tombstone flag for deletes. When the buffer passes a
threshold (256 entries by default in the current code), it is &lt;strong&gt;compacted&lt;/strong&gt;: sorted,
deduplicated, tombstones applied, and written out as an immutable &lt;strong&gt;segment&lt;/strong&gt; with the
smallest workable encoding. Small segments are merged into larger ones by leveled
compaction, the same idea as in RocksDB and LevelDB.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Reads walk a merged iterator over the buffer and the segments, newest first. Each segment
records its minimum and maximum id, so a &lt;code&gt;contains&lt;/code&gt; check skips segments whose range cannot
hold the target, and delta-encoded segments carry a skip table so a lookup can jump close to
the answer and decode forward from there rather than decoding the whole segment.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;four-encodings-chosen-per-segment&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#four-encodings-chosen-per-segment&quot;&gt;&lt;/a&gt;Four encodings, chosen per segment&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Social-graph lists span six orders of magnitude, and no single encoding suits all of them.
AOEE picks one per segment:&lt;/p&gt;
&lt;/div&gt;
&lt;table class=&quot;tableblock frame-all grid-all stretch&quot;&gt;
&lt;colgroup&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 20%;&quot;&gt;
&lt;col style=&quot;width: 60%;&quot;&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;Encoding&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;When&lt;/th&gt;
&lt;th class=&quot;tableblock halign-left valign-top&quot;&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;SmallVec&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;under 128 ids&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;A plain vector of 64-bit ids. For short lists, compression is not worth the CPU.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;DeltaVarint&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;128 to 4,096&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Store the first id, then the gap to each next id as an LEB128 varint. Gaps under 128 cost one byte.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;BlockPacked&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;over 4,096&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Fixed blocks of 128 ids, bit-packed to the width the block needs — compact, with good random access.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Roaring&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;over 100,000, or dense&lt;/p&gt;&lt;/td&gt;
&lt;td class=&quot;tableblock halign-left valign-top&quot;&gt;&lt;p class=&quot;tableblock&quot;&gt;Roaring bitmaps, which shine when ids cluster and make set operations fast.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Density is simply the count divided by the id range. The repo&amp;#8217;s encoding notes work an
example: a user with 1,000 followers whose ids run 5,000 to 5,999 takes 8,000 bytes as raw
64-bit integers and about 1,003 bytes delta-encoded — an 87% saving — because every gap is
one byte. This is where keeping ids as integers pays off: the 48-bit layout keeps ids of the
same type numerically close, so their deltas stay small.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The design point I like best is that the choice is invisible above the segment. Every
encoding implements the same &lt;code&gt;PostingIterator&lt;/code&gt; trait — &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;peek&lt;/code&gt;, and &lt;code&gt;seek&lt;/code&gt; to the
first id at or above a target — so the set operations never know or care how a list is
stored.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;set-maths-instead-of-joins&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#set-maths-instead-of-joins&quot;&gt;&lt;/a&gt;Set maths instead of joins&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&quot;Mutual friends&quot; is an intersection of two sorted lists. &quot;Liked by any of my friends&quot; is a
union. With every list sorted and seekable, both are linear merges — no hash tables, no
joins, no temporary results.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The interesting case is lopsided input: a user with 80 friends intersected with a celebrity
who has two million followers. Merging two million ids to find a handful of matches is
wasteful. AOEE&amp;#8217;s adaptive intersection checks the sizes, and when one list is more than ten
times larger it switches to &lt;strong&gt;galloping&lt;/strong&gt;: for each id in the small list, &lt;code&gt;seek&lt;/code&gt; forward in the
large one — a binary search over what remains, rather than a step-by-step merge. The cost
grows with the small list and only logarithmically with the large one.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Intersection and union also generalise to many lists at once (&lt;code&gt;intersect_many&lt;/code&gt;,
&lt;code&gt;union_many&lt;/code&gt;), which is how multi-way queries such as &quot;groups we are both in that also
contain this post&amp;#8217;s author&quot; stay cheap.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;friend-of-friend-with-a-budget&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#friend-of-friend-with-a-budget&quot;&gt;&lt;/a&gt;Friend-of-friend, with a budget&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Friend suggestions are the classic two-hop query: for each of my friends, look at their
friends, count how many times each person appears, drop me and the people I already know,
and rank by that count — the number of mutual connections.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Done naively, one well-connected friend can blow the query up. The &lt;code&gt;FofQuery&lt;/code&gt; executor has
explicit limits:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a &lt;strong&gt;fan-out cap&lt;/strong&gt; on how many first-hop friends are expanded (1,000 by default), with
optional deterministic sampling when someone has more;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;an optional &lt;strong&gt;time budget&lt;/strong&gt;, after which it returns what it has and marks the result as
truncated;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a &lt;strong&gt;minimum score&lt;/strong&gt; and a &lt;strong&gt;maximum number of results&lt;/strong&gt;;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;exclusions for yourself, your existing connections, and anything else you pass in, such as
blocked users.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The result reports how many neighbours were processed and whether it was truncated, so the
caller can tell a complete answer from a best effort. I think that honesty matters more than
the algorithm: a recommendation that is cheap and &lt;strong&gt;known&lt;/strong&gt; to be partial beats one that is
exact and occasionally takes two seconds.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;sharding-and-the-server&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#sharding-and-the-server&quot;&gt;&lt;/a&gt;Sharding and the server&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The Rust server is a workspace of crates: &lt;code&gt;aoee-core&lt;/code&gt; (ids, edge types, encodings,
iterators, set operations, friend-of-friend), &lt;code&gt;aoee-storage&lt;/code&gt; (an &lt;code&gt;EdgeStore&lt;/code&gt; trait with an
in-memory implementation), &lt;code&gt;aoee-shard&lt;/code&gt; (shards and routing), &lt;code&gt;aoee-server&lt;/code&gt; (the gRPC
service), plus a Rust client and a benchmark crate.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Each &lt;code&gt;EdgeKey&lt;/code&gt; maps to a shard through a consistent-hash ring with virtual nodes, so adding
shards moves only a fraction of the keys. Within a shard, posting lists live in a &lt;code&gt;DashMap&lt;/code&gt;
— a concurrent hash map with fine-grained locking — so readers of different keys never
contend.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-jvm-side-the-ui-and-persistence&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-jvm-side-the-ui-and-persistence&quot;&gt;&lt;/a&gt;The JVM side, the UI, and persistence&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Most of the systems I work with are Java, so AOEE has a first-class Java client:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-java hljs&quot; data-lang=&quot;java&quot;&gt;AoeeClient client = new AoeeClient(&quot;localhost&quot;, 50051);

client.addEdge(userId, EdgeType.FOLLOWS, targetId);
client.addEdge(userId, EdgeType.LIKES, postId, 0, ReactionType.LOVE.getValue());

List&amp;lt;Long&amp;gt; mutual = client.intersect(
    new EdgeKey(alice, EdgeType.FRIEND_OF),
    new EdgeKey(bob, EdgeType.FRIEND_OF));

FofResponse suggestions = client.friendOfFriend(alice, EdgeType.FRIEND_OF);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;On top of it, a Spring Boot proxy exposes the same operations as REST, loads datasets, and
runs benchmarks; a React UI provides a dashboard, a force-directed graph explorer, an edge
manager with a reaction picker, a query builder for set operations and suggestions, and a
dataset loader. An optional persistence service (REST and GraphQL over H2 or PostgreSQL)
receives writes through the proxy so the cache can be warmed from a database, and both the
server and the proxy have Dockerfiles. To run the whole stack locally:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-bash hljs&quot; data-lang=&quot;bash&quot;&gt;./start-services.sh            # Rust server, Spring proxy, React UI
./start-services.sh --persist  # ...plus the persistence service&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;what-the-numbers-say&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-the-numbers-say&quot;&gt;&lt;/a&gt;What the numbers say&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The benchmark harness generates a synthetic social graph — users with a skewed follow
distribution, posts, groups, likes with reactions, a few very popular accounts and a viral
post — then times each operation. The small preset builds 1,000 users, 2,000 posts, and 10
groups: 125,657 edges.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the run recorded in the repo&amp;#8217;s benchmarking guide, median latencies for every operation —
neighbour lists, lists with reaction metadata, &lt;code&gt;contains&lt;/code&gt;, counts, mutual-friend
intersections, friend-of-friend, the popular-user lists, and the viral post — fell between
64 and 77 microseconds, with 99th percentiles between 93 and 172 microseconds. The slowest
single call was a friend-of-friend query at 1.9 milliseconds.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Two caveats, because benchmarks deserve them. Those numbers are measured end to end from the
JVM through gRPC on a development machine, so most of each microsecond budget is transport,
not graph work. And the throughput figures in that same output came from a calculation I
later found was off by a factor of a thousand — fixed in the commit history, and a good
reminder to sanity-check any number that looks either too good or too bad.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;what-i-learned&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-i-learned&quot;&gt;&lt;/a&gt;What I learned&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Search engine techniques transfer beautifully to graphs.&lt;/strong&gt; Posting lists, gap encoding,
skip tables, and galloping intersection solved most of the problems I expected to fight.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Adaptive representation beats a clever single format.&lt;/strong&gt; The four encodings are each simple;
the win is choosing between them per segment and hiding the choice behind one iterator.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Put limits in the query, not around it.&lt;/strong&gt; Fan-out caps, sampling, and time budgets made
friend-of-friend predictable, and reporting truncation made it trustworthy.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Write paths want buffers; read paths want immutability.&lt;/strong&gt; A small LSM per posting list let
both sides be simple.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#whats-next&quot;&gt;&lt;/a&gt;What&amp;#8217;s next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The repo&amp;#8217;s roadmap covers the obvious next steps for a production cache: a RocksDB-backed
store, leader–follower replication, set operations that span nodes, streaming updates to the
UI, a cache for hot friend-of-friend results, time-range queries (one place TAO is still
ahead — AOEE orders lists by id, not time), and SIMD-accelerated intersection.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock text-center&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;aoee-logo.png&quot; alt=&quot;AOEE logo&quot; width=&quot;240&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Amiga DevBench: a modern dev loop, and an AI pair programmer, for the Amiga</title>
    <link href="https://geekychris.github.io/articles/amiga-devbench/"/>
    <id>https://geekychris.github.io/articles/amiga-devbench/</id>
    <updated>2026-10-10T00:00:00Z</updated>
    <summary>How I connected a Mac to emulated and real Amigas over a serial link, built a source-level debugger and 129 MCP tools on top of it, and let Claude Code write, deploy, and debug Amiga software.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Developing for the Amiga in 2026 is a strange mix of joy and friction. The machine is still
wonderful: a pre-emptive multitasking OS in 512 KB of ROM, a custom chipset you can drive
directly, and a community that has kept it alive for forty years. But the development
experience has not kept up. There is no source-level debugger running on the target, no
network stack on a stock machine, and one bad pointer takes the whole operating system down
with it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I wanted the opposite: write C on my Mac, press one button, and have the program built,
copied onto the Amiga, started, and streaming its logs back to me. I also wanted an AI
assistant to be able to do all of that on its own, so I could say &quot;this game crashes after a
few seconds, find out why&quot; and watch it work. That is what
&lt;a href=&quot;https://github.com/geekychris/amiga_mcp&quot;&gt;Amiga DevBench&lt;/a&gt; does. I started it on 8 March 2026;
it is now about 175 commits, runs against classic 68k AmigaOS and AmigaOS 4.1 on PowerPC, and
exposes 129 tools to Claude Code through MCP.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;dashboard.png&quot; alt=&quot;The DevBench web UI&amp;#8217;s dashboard&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. The DevBench web UI&amp;#8217;s dashboard: connection, heartbeat, free chip and fast memory, connected clients, emulator controls, and a raw protocol console&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-shape-of-it&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-shape-of-it&quot;&gt;&lt;/a&gt;The shape of it&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There are two halves, joined by a byte stream.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;On the Amiga side sits &lt;code&gt;amiga-bridge&lt;/code&gt;, a small C daemon. It opens &lt;code&gt;serial.device&lt;/code&gt; (or, on a
machine with a TCP/IP stack, a socket) and owns it exclusively. It creates a public Exec
message port called &lt;code&gt;AMIGABRIDGE&lt;/code&gt;, and then sits in a &lt;code&gt;Wait()&lt;/code&gt; loop on three signals: serial
I/O, IPC messages from client programs, and its own status window. Programs that want to
take part link a tiny static library, &lt;code&gt;libbridge.a&lt;/code&gt;, and call &lt;code&gt;ab_init(&quot;my_app&quot;)&lt;/code&gt;. From then
on they can log with &lt;code&gt;AB_I(&quot;frame %ld&quot;, (long)n)&lt;/code&gt;, expose variables with &lt;code&gt;ab_register_var&lt;/code&gt;,
publish named memory regions, and register &lt;strong&gt;hooks&lt;/strong&gt;: functions the host can call into the
running program. Client programs never touch the serial port themselves; everything goes
through &lt;code&gt;PutMsg&lt;/code&gt;/&lt;code&gt;GetMsg&lt;/code&gt; to the daemon, which routes it to the host.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;On the Mac sits &lt;code&gt;amiga-devbench&lt;/code&gt;, a Python server (Uvicorn and Starlette, with FastMCP for
the MCP side). It speaks the bridge protocol over a pseudo-terminal that FS-UAE opens as its
serial port, or over TCP to a real Amiga or an emulator on the network, and it presents
everything three ways: a web UI with a live event stream, a REST API with more than 80
endpoints, and the MCP server Claude Code talks to.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The protocol between them is deliberately boring: one line per message, pipe-separated
fields, at most 1024 characters.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-none hljs&quot;&gt;PING                          → PONG|2|1649344|6459000
GETVAR|bouncing_ball|speed    → CVAR|bouncing_ball|speed|i32|10
CLOG|bouncing_ball|I|1567|Frame 100&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Text over a serial line is slow by modern standards, but it is debuggable with a terminal,
trivially portable between 68k and PowerPC, and it never needed changing when I added
AmigaOS 4 support later.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a-session-with-claude&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#a-session-with-claude&quot;&gt;&lt;/a&gt;A session with Claude&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The loop the tools are designed around is: write, build, deploy, run, observe, fix. Each
step is a tool. &lt;code&gt;amiga_build&lt;/code&gt; cross-compiles inside a persistent Docker container (rebuilds
take a second or two), &lt;code&gt;amiga_deploy&lt;/code&gt; copies the binary to the Amiga and checks it with a
CRC32, &lt;code&gt;amiga_launch&lt;/code&gt; starts it, and &lt;code&gt;amiga_watch_logs&lt;/code&gt; streams its output. For the common
case, &lt;code&gt;amiga_build_deploy_run&lt;/code&gt; does all of it in one call.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Here is the kind of exchange that made me believe the whole thing was worth it:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-none hljs&quot;&gt;Me:  My bouncing_ball app crashes after a few seconds.

amiga_build_deploy_run(&quot;bouncing_ball&quot;)
  → build OK, deploy OK, running
amiga_watch_logs(duration_ms=30000)
  → &quot;Frame 100&quot; ... then silence
amiga_last_crash()
  → alert 80000004 (access fault)
    PC 0040A2B8, A0=DEADBEEF
amiga_load_symbols(&quot;bouncing_ball&quot;)
amiga_lookup_symbol(&quot;0040A2B8&quot;)
  → bouncing_ball.c:142, draw_ball()

[reads the source, fixes an uninitialised
 pointer, rebuilds]

amiga_build_deploy_run(&quot;bouncing_ball&quot;)
  → running, no crash&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The assistant does not need to see the screen to make progress, but it can:
&lt;code&gt;amiga_screenshot&lt;/code&gt; grabs the display, and there are tools to list screens and windows,
inject keypresses and mouse clicks, read the palette and the copper list, and even run
ARexx. When a program needs tuning, &lt;code&gt;amiga_set_var&lt;/code&gt; changes a registered variable while
the program runs, which is a lovely way to tweak a game&amp;#8217;s physics without a rebuild.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;debugger.png&quot; alt=&quot;The source-level debugger stopped at a breakpoint in draw_ball()&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. The source-level debugger stopped at a breakpoint in draw_ball(): source, registers, call stack, and local variables&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;two-debuggers-because-one-is-never-enough&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#two-debuggers-because-one-is-never-enough&quot;&gt;&lt;/a&gt;Two debuggers, because one is never enough&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The bridge gives you a &lt;strong&gt;source-level&lt;/strong&gt; debugger. Load the symbols, set a breakpoint on a
function or a line, launch and attach, and you can step into and over code, walk the call
stack, and read locals. There is also a GDB remote-serial-protocol server on port 2159, so
&lt;code&gt;m68k-amigaos-gdb&lt;/code&gt; or an IDE can attach the same way.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;But a debugger that lives inside the Amiga dies with the Amiga. When the OS hangs, or before
it has booted, the bridge is gone. So I also maintain a patched FS-UAE build that exposes the
&lt;strong&gt;emulator&amp;#8217;s&lt;/strong&gt; debugger over HTTP. That one pauses the 68000 itself: it can set hardware-style
watchpoints (&quot;who keeps writing &lt;code&gt;$DFF180&lt;/code&gt;?&quot;), read Kickstart ROM and the CIA registers,
single-step from the reset vector, and save and restore whole machine snapshots. It has no
idea about source lines, but it sees everything. In practice I use them together: a watchpoint
in the emulator finds &lt;strong&gt;where&lt;/strong&gt; something goes wrong, and the bridge debugger lets me step
through the C around it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;inspect_graphics.png&quot; alt=&quot;The Inspect tab&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 3. The Inspect tab: screenshot capture, palette and copper-list readers, sprite inspector, bitmap and IFF viewers, and a visual diff&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-interesting-problems&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-interesting-problems&quot;&gt;&lt;/a&gt;The interesting problems&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Most of the work was not the happy path. A few of the things the Amiga taught me along the
way:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A 4 KB default stack.&lt;/strong&gt; Every large buffer in the daemon is &lt;code&gt;static&lt;/code&gt;. Allocating a
1 KB line buffer on the stack in a function that calls another function is enough to walk
off the end and corrupt something unrelated.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;printf&lt;/code&gt; lies about integers.&lt;/strong&gt; In &lt;code&gt;amiga.lib&lt;/code&gt;, &lt;code&gt;%d&lt;/code&gt; reads a 16-bit word, so passing a
32-bit &lt;code&gt;int&lt;/code&gt; shifts every following argument. The rule in the codebase (and in the
instructions Claude reads) is &lt;code&gt;%ld&lt;/code&gt; with an explicit &lt;code&gt;(long)&lt;/code&gt; cast, always.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reading memory is not always reading memory.&lt;/strong&gt; Under FS-UAE, &lt;code&gt;CopyMem&lt;/code&gt; returned zeros for
some regions, so the memory inspector reads byte by byte through a &lt;code&gt;volatile&lt;/code&gt; pointer.
And the custom chip registers at &lt;code&gt;$DFF000&lt;/code&gt; are word-only: a byte read can corrupt them, so
the inspector refuses to touch that range.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Never let the OS ask a question.&lt;/strong&gt; When the host launches a program, the daemon checks the
path with &lt;code&gt;Lock()&lt;/code&gt; first and sets &lt;code&gt;pr_WindowPtr&lt;/code&gt; to −1, so a missing volume produces an
error message instead of a requester that waits forever for a click nobody will make.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No allocation on the hot path.&lt;/strong&gt; The client library keeps a pool of four pre-allocated
messages, and each client gets fixed slots: 32 variables, 16 hooks, and 8 memory regions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Naked-amiga/i-tjc97Gm/0/MH9kGRT7jwpfvLQ47rw35vF59bs8zTxRLPg93Jbn6/X3/beth_1200-X3.jpg&quot; alt=&quot;An Amiga 1200 motherboard with its case off&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 4. The real thing: an A1200 board, from my SmugMug collection. Everything above has to respect what this hardware can and cannot do.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;amigaos-4-on-powerpc&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#amigaos-4-on-powerpc&quot;&gt;&lt;/a&gt;AmigaOS 4 on PowerPC&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Eleven days after the first commit I started on AmigaOS 4.1, running on QEMU&amp;#8217;s emulation of
the Sam460ex board, and it has been the other half of the project ever since. The architecture held up: the serial protocol, the Python host, and the MCP
tools did not change at all. The bridge needed a PowerPC build (with GCC 11 from the
&lt;code&gt;walkero&lt;/code&gt; Docker images), a TCP transport over &lt;code&gt;bsdsocket.library&lt;/code&gt;, and a PowerPC port of the
debugger and crash handler. The emulator needed rather more coaxing:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;QEMU&amp;#8217;s old &lt;code&gt;-net nic,model=rtl8139&lt;/code&gt; shorthand silently creates &lt;strong&gt;no&lt;/strong&gt; network card on the
Sam460ex machine. The working form is &lt;code&gt;-netdev user,…,hostfwd=…&lt;/code&gt; plus
&lt;code&gt;-device rtl8139&lt;/code&gt;, which I finally wrote down in a reference config after re-deriving it
more than once.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;There is no chip RAM on a Sam460ex, so nothing can poke the custom chips; graphics go
through &lt;code&gt;graphics.library&lt;/code&gt;. There is also no 3D driver for QEMU&amp;#8217;s display, so the OS4
ports of my games use a software Mesa (OSMesa 7.8.2) build.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Deploying means writing files straight into the raw hard-disk image with &lt;code&gt;amitools&lt;/code&gt; while
the OS has it mounted, which is genuinely dangerous: both sides cache the filesystem. The
deploy script refuses if QEMU is running unless you insist.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;And one bug I am still slightly in awe of: on the QEMU guest, a newlib &lt;code&gt;memcpy&lt;/code&gt; of more than
about 300 KB can leave its last 256 KB uncopied. Big buffers are now copied with a loop.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;what-it-has-produced&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-it-has-produced&quot;&gt;&lt;/a&gt;What it has produced&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The best measure of a development environment is what people make with it. The companion
repository, &lt;a href=&quot;/projects/amiga-games/&quot;&gt;amiga_games&lt;/a&gt;, now holds about fifty programs written
with DevBench, most of them with Claude doing the typing: arcade games, demos, a HAM
raster test, an ARexx test, a disk benchmark, and ports of three of my 3DO games. A few
of them were put together in an afternoon.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;frank_the_frog.png&quot; alt=&quot;Frank the Frog crossing the road and river on lily-pad rafts&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 5. Frank the Frog, one of the example games built with DevBench&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;dot_chase.png&quot; alt=&quot;The title screen of Dot Chase: a ladybug in a hedge maze&quot; width=&quot;hunted by four spiders&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 6. The title screen of Dot Chase, a maze game from the examples repository&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There are smaller pieces too. The host can proxy an Amiga terminal program to a language
model, streaming tokens back into a window on the Amiga, which means you can chat with an
LLM from Workbench. And the whole environment is now three commands to set up:
&lt;code&gt;scripts/setup.sh&lt;/code&gt;, &lt;code&gt;scripts/doctor.sh&lt;/code&gt; (which checks every prerequisite and tells you how to
fix each one), and &lt;code&gt;scripts/start.sh&lt;/code&gt;. The commercial parts, the Kickstart ROM and the
AmigaOS installs, still have to come from you.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#whats-next&quot;&gt;&lt;/a&gt;What&amp;#8217;s next&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The backlog in the repository has two big themes. The first is more examples: a list of
clean-room games inspired by 8- and 16-bit classics (a Jetpac-style collect-and-launch
shooter, an isometric room adventure in the Knight Lore mould, a Sentinel-like solid-3D
landscape, a Lemmings-style puzzler, a Nebulus tower climber), each starting with a research
phase on gameplay, graphics, and sound before any code lands, and each reproducing mechanics
rather than anyone&amp;#8217;s assets. The second is reach: the same bridge-and-MCP approach for the
Atari 8-bit, with an emulator the tools can drive. The point of all of it is the line I put
in the README: to make the Amiga community productive with AI. Forty years on, the machine
deserves a modern toolchain.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you have an Amiga, real or emulated, the
&lt;a href=&quot;https://github.com/geekychris/amiga_mcp&quot;&gt;repository&lt;/a&gt; has the installer, and the
&lt;a href=&quot;/projects/amiga-mcp/&quot;&gt;project page&lt;/a&gt; collects everything I write about it.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Transparent file encryption in the Linux kernel</title>
    <link href="https://geekychris.github.io/articles/transparent-file-encryption-in-the-linux-kernel/"/>
    <id>https://geekychris.github.io/articles/transparent-file-encryption-in-the-linux-kernel/</id>
    <updated>2026-10-10T00:00:00Z</updated>
    <summary>How I built CryptoFS, a stacked Linux filesystem that encrypts every file with its own AES-256-GCM key at the VFS layer — authorised processes see plaintext, everyone else sees ciphertext — and what it taught me about nonces.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Most of the encryption I deal with at work happens a long way from the disk: TLS on the
wire, envelope encryption in an application, a cloud provider&amp;#8217;s &quot;encryption at rest&quot;
checkbox. Products like Thales CipherTrust Transparent Encryption take a different
position. They sit in the operating system, encrypt files as they are written, and decide
&lt;strong&gt;per process&lt;/strong&gt; who gets to see plaintext. The database, the search engine, the log shipper —
none of them change a line of code.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I wanted to understand how that actually works, so I built a small version of it:
&lt;a href=&quot;/projects/cryptfs/&quot;&gt;CryptoFS&lt;/a&gt;, a Linux kernel module plus a Rust key daemon and admin
CLI. This is the tour.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-idea-a-filesystem-on-top-of-a-filesystem&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-idea-a-filesystem-on-top-of-a-filesystem&quot;&gt;&lt;/a&gt;The idea: a filesystem on top of a filesystem&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CryptoFS is a &lt;strong&gt;stacked&lt;/strong&gt; filesystem, in the same family as eCryptfs and the classic
wrapfs teaching example. It registers a filesystem type, &lt;code&gt;cryptofs&lt;/code&gt;, and mounts one
directory over another:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-console hljs&quot; data-lang=&quot;console&quot;&gt;sudo mount -t cryptofs /data/encrypted /mnt/decrypted&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The lower directory holds what actually lands on disk. The upper mount is the view
applications use. Every VFS operation on the upper side — lookup, create, read, write,
rename, mmap — arrives at the module first, which does its work and then calls the
corresponding operation on the lower filesystem. Because it stacks on an existing
directory, it works over ext4, xfs, or btrfs without reformatting anything.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;data-path.svg&quot; alt=&quot;Diagram of a write passing from an authorised process through the VFS&quot; width=&quot;the policy engine&quot; height=&quot;4 KB extent splitting and AES-256-GCM encryption to the lower filesystem; an unauthorised process gets EACCES on write and ciphertext on read&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. One write() through CryptoFS: the policy check decides whether data is encrypted on the way down, or the write is refused.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;per-process-policy-enforced-in-the-kernel&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#per-process-policy-enforced-in-the-kernel&quot;&gt;&lt;/a&gt;Per-process policy, enforced in the kernel&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The interesting part of transparent encryption is not the cipher; it is the decision about
&lt;strong&gt;who&lt;/strong&gt; gets plaintext. CryptoFS answers that in &lt;code&gt;policy.c&lt;/code&gt;, on every read and write, by
looking at the calling task. Rules can match on:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;UID or GID;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;the executable&amp;#8217;s path, resolved from the task&amp;#8217;s &lt;code&gt;mm&amp;#8594;exe_file&lt;/code&gt;;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;the executable&amp;#8217;s SHA-256 hash, computed in the kernel by reading the binary — so swapping
&lt;code&gt;/usr/bin/myapp&lt;/code&gt; for something else stops it decrypting;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;the process name (&lt;code&gt;comm&lt;/code&gt;) — convenient, and weak, because a process can set its own name.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;An authorised reader gets plaintext. An unauthorised reader gets the raw extents — real
ciphertext, header stripped — so a backup agent can be left unauthorised and still read the
bytes without ever seeing the clear text (restoring them is another matter, as I explain
below). An unauthorised writer
gets &lt;code&gt;-EACCES&lt;/code&gt;. With no policies configured at all, the module falls back to an allow-all
convenience mode, which is what the demos use.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Policies and keys travel from user space over a Generic Netlink family, and the module keeps
an audit trail in a kernel ring buffer.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;extents-tags-and-an-on-disk-format&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#extents-tags-and-an-on-disk-format&quot;&gt;&lt;/a&gt;Extents, tags, and an on-disk format&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Files are processed in 4 KB extents, the same size as a page. Each extent is encrypted with
AES-256-GCM through the kernel Crypto API (&lt;code&gt;gcm(aes)&lt;/code&gt;), and its 16-byte authentication tag is
stored right after it. A file on the lower filesystem looks like this:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a 128-byte header: the magic &lt;code&gt;CRYPTOFS&lt;/code&gt;, a version, the file&amp;#8217;s wrapped key and wrapping
nonce, the plaintext length, and the ID of the master key that wrapped it;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;then extent after extent of 4096 bytes of ciphertext plus a 16-byte tag.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Mapping a logical offset to the lower file is arithmetic, not a lookup:
&lt;code&gt;128 + (L / 4096) × 4112 + (L % 4096)&lt;/code&gt;. Random reads decrypt only the extents they touch, and
a partial write reads the existing extent, decrypts it, patches it, and encrypts it again.
The overhead is 128 bytes per file plus 16 bytes per extent — about 0.4%.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Memory-mapped files go through the page cache: &lt;code&gt;read_folio&lt;/code&gt; decrypts one extent into one
page, and writeback encrypts it on the way out. Because a page and an extent are both
4 KB, the mapping is one-to-one, which kept that code short.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Every extent&amp;#8217;s GCM tag is checked on read, so if anything on the disk is altered — a flipped
bit, a spliced block — the read fails loudly instead of returning corrupted data.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;keys-three-layers&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#keys-three-layers&quot;&gt;&lt;/a&gt;Keys: three layers&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;key-hierarchy.svg&quot; alt=&quot;Diagram: passphrase → Argon2id KEK → master key held by the key daemon and loaded into the kernel key table; per file&quot; width=&quot;a random FEK wrapped by the master key in the header; the FEK encrypts each extent and derives each nonce with HMAC-SHA256&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. The key hierarchy: a passphrase unlocks the master key; the master key wraps a random key per file; each file key encrypts its extents and derives their nonces.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Master keys&lt;/strong&gt; live in user space with &lt;code&gt;cryptofs-keyd&lt;/code&gt;, a Rust daemon. On disk each one is
encrypted under a key derived from a passphrase with Argon2id. &quot;Unlock&quot; decrypts it into
the daemon&amp;#8217;s memory; &quot;activate&quot; pushes it into the module&amp;#8217;s key table, which holds up to
64 keys by 16-byte ID.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;File encryption keys&lt;/strong&gt; are 256 random bits from &lt;code&gt;get_random_bytes&lt;/code&gt;, generated when a file
is created and stored in its header wrapped by a master key with AES-256-GCM. Losing one
file key exposes one file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extent nonces&lt;/strong&gt; are not stored at all. Each is derived as
&lt;code&gt;HMAC-SHA256(FEK, inode ‖ extent index)&lt;/code&gt; truncated to 12 bytes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There are two ways for the kernel to find a master key. In &lt;strong&gt;transparent&lt;/strong&gt; mode the admin
activates keys and every authorised process uses them. In &lt;strong&gt;guarded&lt;/strong&gt; mode a policy binds a
user to a key that is never loaded globally: the user runs &lt;code&gt;cryptofs-admin key
session-unlock&lt;/code&gt;, which puts the key into &lt;strong&gt;their&lt;/strong&gt; session keyring, and at open time the
module calls &lt;code&gt;request_key()&lt;/code&gt; for &lt;code&gt;cryptofs:&amp;lt;key-id&amp;gt;&lt;/code&gt;. If the key is not in that process&amp;#8217;s
keyring, the open fails with &lt;code&gt;ENOKEY&lt;/code&gt;. It is the closest thing in the project to &quot;only Alice
can read Alice&amp;#8217;s files, even though root mounted the volume&quot; — with the usual caveat that
root on a live system can still reach almost anything.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The daemon talks to keys through a &lt;code&gt;KeyProvider&lt;/code&gt; trait. Only a local provider exists today;
the trait is there so an external KMS — HashiCorp Vault&amp;#8217;s transit engine, AWS KMS, or my own
&lt;a href=&quot;/projects/securevault/&quot;&gt;SecureVault&lt;/a&gt; — can be slotted in later. That integration does
not exist yet.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;what-i-would-change-the-nonce&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-i-would-change-the-nonce&quot;&gt;&lt;/a&gt;What I would change: the nonce&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Writing this up made me look hard at the one decision I am least comfortable with.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Deriving nonces from the file key, inode number, and extent index means nothing needs storing
and two extents never share a nonce. But an extent that is &lt;strong&gt;rewritten&lt;/strong&gt; keeps its nonce. If it
is rewritten with different data, AES-GCM is used twice with the same key and nonce — the
cardinal sin of GCM. An attacker who captures the lower file before and after the rewrite
learns the XOR of the two plaintexts for that extent, and nonce reuse also weakens GCM&amp;#8217;s
authentication. The README&amp;#8217;s note that &quot;the same data produces the same ciphertext&quot; is true,
but it understates the problem: the risk is different data under the same nonce.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The inode number in the derivation has a second consequence: the ciphertext is bound to the
lower file&amp;#8217;s inode. Copying the lower files to another filesystem — a backup restored with
&lt;code&gt;cp&lt;/code&gt; or &lt;code&gt;rsync&lt;/code&gt; — gives them new inode numbers, and their extents no longer decrypt.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The fix for both is well understood: store a random nonce per extent (12 more bytes per 4 KB),
or keep a write counter per extent and mix it in, or move to a construction designed for this —
an XTS-style scheme for confidentiality plus separate integrity, or AES-GCM-SIV, which
tolerates nonce reuse. That is the next change I would make, and it is the kind of flaw that
only shows up when you are forced to explain your design to someone else.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;how-it-is-tested&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#how-it-is-tested&quot;&gt;&lt;/a&gt;How it is tested&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Kernel code is unforgiving, so most of the repository is tests: shell suites for basic file
operations, random access and extent boundaries, appends, concurrency, UID policies, guarded
keys, and Docker containers (an authorised UID 1000 container sees plaintext; an
unauthorised UID 65534 one sees ciphertext). A stress test runs graduated file sizes up to
500 MB, patterned and random data, overwrites, random-offset patches, concurrent workers,
file churn, and unmount/remount persistence, checking everything with SHA-256. fio
benchmarks compare the encrypted mount with a plain baseline, and &lt;code&gt;build_and_test.sh&lt;/code&gt; runs
the whole pipeline end to end.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;There is also a demo of Apache Solr 9 running in Docker with its index on a CryptoFS mount —
no changes to Solr at all, which is the whole point of doing this in the kernel.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;trying-it-in-a-vm&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#trying-it-in-a-vm&quot;&gt;&lt;/a&gt;Trying it — in a VM&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Do not load an experimental kernel module on a machine you care about. On a Mac, the
repository includes a Vagrant definition for an ARM64 Ubuntu 24.04 VM under QEMU with every
dependency installed:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-console hljs&quot; data-lang=&quot;console&quot;&gt;brew install vagrant qemu &amp;amp;&amp;amp; vagrant plugin install vagrant-qemu
make vm-up &amp;amp;&amp;amp; make vm-ssh

# inside the VM
make all
sudo insmod kernel/cryptofs.ko
sudo daemon/target/release/cryptofs-keyd --foreground &amp;amp;
cryptofs-admin key generate --label demo     # then key unlock / key activate &amp;lt;id&amp;gt;
mkdir -p /tmp/lower /tmp/mnt &amp;amp;&amp;amp; sudo mount -t cryptofs /tmp/lower /tmp/mnt
echo &quot;secret data&quot; &amp;gt; /tmp/mnt/hello.txt
cat /tmp/mnt/hello.txt      # secret data
xxd /tmp/lower/hello.txt    # CRYPTOFS header, then ciphertext&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It needs Linux 6.1 or later with &lt;code&gt;CONFIG_CRYPTO_AES&lt;/code&gt; and &lt;code&gt;CONFIG_CRYPTO_GCM&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;an-honest-security-note&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#an-honest-security-note&quot;&gt;&lt;/a&gt;An honest security note&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;CryptoFS is a learning project and a working prototype. It has not been audited, the nonce
scheme above is a real weakness, and it protects data at rest only: it does nothing against
a compromised kernel or root on a running system with keys loaded. If you need this in
production, use dm-crypt or LUKS for whole volumes, fscrypt for per-directory encryption
built into ext4 and f2fs, or a commercial transparent-encryption product.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;What it did give me is a working model of how those products are built: a stacked VFS layer,
a policy decision made from the calling task, a key hierarchy that keeps master keys out of
the kernel until they are needed, and an on-disk format that can be read at any offset. The
source is on &lt;a href=&quot;https://github.com/geekychris/cryptfs&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Writing 3DO games in 2026: a 1993 console, a 1990s compiler, and a debuggable emulator</title>
    <link href="https://geekychris.github.io/articles/writing-3do-games-in-2026/"/>
    <id>https://geekychris.github.io/articles/writing-3do-games-in-2026/</id>
    <updated>2026-10-10T00:00:00Z</updated>
    <summary>How I built a modern dev kit for the 3DO Interactive Multiplayer — Norcroft ARM C in Docker, a patched Opera emulator with a GDB server and a JSON control port, and an MCP server — and used it to ship a demo and 21 games.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The 3DO Interactive Multiplayer came out in 1993 and asked for $699. It had an ARM CPU when
everyone else was on 68000s and MIPS, a sprite engine that thought in scaled and rotated
quadrilaterals, and a real preemptive multitasking OS underneath every game. It is a lovely
machine to program, and in 2026 it is surprisingly hard to get started on.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So I built &lt;a href=&quot;/projects/3do-dev/&quot;&gt;3do-dev&lt;/a&gt;: a dev kit that turns &quot;write a 3DO program&quot; into
a modern loop of &lt;strong&gt;edit → build → boot → observe → test → debug&lt;/strong&gt;, which works the same whether
the thing at the keyboard is me, a script, or Claude Code. This is how it works and what it
took.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;demo.png&quot; alt=&quot;The CLAUDE x 3DO demo running in the emulator: plasma&quot; width=&quot;starfield&quot; height=&quot;a rotating cel vortex&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 1. The demo: plasma, starfield, a rotating cel vortex, a bouncing title, and a sine scroller, at 60 fps on an emulated 12.5 MHz ARM60.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;why-the-3do-is-an-interesting-target&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#why-the-3do-is-an-interesting-target&quot;&gt;&lt;/a&gt;Why the 3DO is an interesting target&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The hardware in one paragraph: an &lt;strong&gt;ARM60 at 12.5 MHz&lt;/strong&gt; (big-endian ARMv3), 2 MB of DRAM plus
1 MB of VRAM, and two custom chips. &lt;strong&gt;MADAM&lt;/strong&gt; drives the &lt;strong&gt;cel engine&lt;/strong&gt;, which draws
hardware-scaled, rotated, and blended sprites (&quot;cels&quot;) and does matrix maths; &lt;strong&gt;CLIO&lt;/strong&gt; handles
video, the audio DSP, timers, and I/O. Games don&amp;#8217;t bang the hardware directly. They run on
&lt;strong&gt;Portfolio&lt;/strong&gt;, a preemptive multitasking OS with tasks, message ports, semaphores, and shared
libraries called &lt;strong&gt;folios&lt;/strong&gt; (graphics, audio, maths, filesystem). Programs are AIF executables
on a CD whose boot code is RSA-signed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;That combination — a RISC CPU, a quad-drawing GPU ancestor, and a proper OS — makes it feel
much closer to modern programming than its contemporaries, while still being small enough to
understand completely.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;why-it-is-hard-in-2026&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#why-it-is-hard-in-2026&quot;&gt;&lt;/a&gt;Why it is hard in 2026&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Three things stand in the way.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;The compiler.&lt;/strong&gt; The only complete toolchain is ARM&amp;#8217;s own SDT 2.51 — the &lt;strong&gt;Norcroft&lt;/strong&gt; C
compiler that 3DO developers actually used. trapexit&amp;#8217;s
&lt;a href=&quot;https://github.com/trapexit/3do-devkit&quot;&gt;3do-devkit&lt;/a&gt; packages it with the Portfolio 2.5 SDK and
modern disc tools, which is a huge head start. But the compilers are &lt;strong&gt;32-bit i386 Linux&lt;/strong&gt;
binaries and the disc tools are x86-64 Linux binaries. Nothing runs them natively on a Mac,
and Rosetta doesn&amp;#8217;t do 32-bit.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;The feedback loop.&lt;/strong&gt; Emulators exist — Opera, the maintained descendant of FreeDO and 4DO, is
excellent — but an emulator built for playing games gives you a window and nothing else. No
debug console, no breakpoints, no way for a script to press a button and check what happened.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;The debug information.&lt;/strong&gt; Norcroft emits DWARF 2, but in ways modern gdb quietly rejects.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;how-the-kit-solves-it&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#how-the-kit-solves-it&quot;&gt;&lt;/a&gt;How the kit solves it&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;a-1990s-compiler-in-a-container&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#a-1990s-compiler-in-a-container&quot;&gt;&lt;/a&gt;A 1990s compiler in a container&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;code&gt;bin/3do-make&lt;/code&gt; runs &lt;code&gt;make&lt;/code&gt; inside a tiny amd64 Debian image with the i386 C library, with the
repo mounted and running as my own user ID, so generated files belong to me. On Apple Silicon
the build script registers &lt;strong&gt;QEMU user-mode emulation&lt;/strong&gt; for i386 and x86-64 with the kernel&amp;#8217;s
&lt;code&gt;binfmt_misc&lt;/code&gt;, so the container&amp;#8217;s x86 binaries just run. A build still takes about two
seconds.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The shared rules in &lt;code&gt;sdk/project.mk&lt;/code&gt; compile with
&lt;code&gt;armcc -g -O2 -bigend -apcs 3/32/nofp/swst&lt;/code&gt;, link a self-relocating AIF with &lt;code&gt;armlink&lt;/code&gt;, stamp
the 3DO header with &lt;code&gt;modbin&lt;/code&gt;, copy in the base filesystem, and run &lt;code&gt;3dt pack&lt;/code&gt; to build an ISO
and &lt;strong&gt;RSA-sign it&lt;/strong&gt;, so the retail BIOS boots it. Then the same objects are linked a second time
as an &lt;strong&gt;ELF with DWARF at address 0x80&lt;/strong&gt;, so ELF addresses equal offsets into the loaded AIF.
That second link is what makes symbols and gdb possible.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;an-emulator-you-can-drive&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#an-emulator-you-can-drive&quot;&gt;&lt;/a&gt;An emulator you can drive&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The emulator is libretro Opera, compiled natively with a patch of roughly 50 lines that adds
hooks for the debug console, breakpoints, watchpoints, single-step, system-call tracing, and
memory access. The hooks only cost anything while they are armed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The part I&amp;#8217;m proudest of: &lt;strong&gt;stopping the CPU never disturbs emulated time.&lt;/strong&gt; Opera emulates a
frame by running the CPU in slices interleaved with scanline processing. I made that frame
function &lt;strong&gt;resumable&lt;/strong&gt; — when the debugger says stop, it saves its scanline and cycle position
and returns early, and the next call continues the same frame exactly where it left off. From
the 3DO&amp;#8217;s point of view, no time passes while you sit at a breakpoint.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;A small Python libretro frontend (&lt;code&gt;ctypes&lt;/code&gt;, no C glue) loads the core in-process. All the
heavy work stays in C; Python only runs between frames, so headless emulation still reaches
about 900 frames a second — fifteen times real time. Each emulator lives in its own &lt;strong&gt;session&lt;/strong&gt;
process with a JSON control port, a GDB remote-protocol server, an optional window, and a
registry entry so other tools can find it.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;seeing-inside-portfolio&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#seeing-inside-portfolio&quot;&gt;&lt;/a&gt;Seeing inside Portfolio&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Because Portfolio loads your program wherever it finds memory, the kit finds it by searching
emulated DRAM for the program&amp;#8217;s code (AIF self-relocation replaces a &lt;code&gt;BL&lt;/code&gt; in the header with a
&lt;code&gt;NOP&lt;/code&gt;, which confirms the match). Runtime address = load base + ELF address, and suddenly
&lt;code&gt;break update_plasma&lt;/code&gt;, &lt;code&gt;read_u32 addr=s_palette&lt;/code&gt;, and gdb&amp;#8217;s &lt;code&gt;qOffsets&lt;/code&gt; all just work.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It goes further and reads the OS&amp;#8217;s own data structures. Every Portfolio object is an &lt;strong&gt;item&lt;/strong&gt;;
walking the kernel&amp;#8217;s item table gives tasks, message ports, semaphores, devices, I/O requests,
screens, and a memory map of every DRAM and VRAM page by owner. The structure offsets come from
a tiny 3DO program that prints &lt;code&gt;offsetof()&lt;/code&gt; for the SDK structs, so they are right by
construction. None of this needs any code on the 3DO, and it works on a crashed program.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;devbench-tasks.png&quot; alt=&quot;DevBench&amp;#8217;s Portfolio OS inspector listing tasks with state&quot; width=&quot;priority&quot; height=&quot;signals&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 2. DevBench&amp;#8217;s OS inspector: the Portfolio task list, read straight out of emulated RAM.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;All of that is exposed three ways: the &lt;code&gt;./3do ctl&lt;/code&gt; command line (about 70 commands), a
browser &lt;strong&gt;DevBench&lt;/strong&gt; with a REST API, and an &lt;strong&gt;MCP server&lt;/strong&gt; with about 50 tools so Claude Code can
build, boot, press buttons, look at the screen, read logs, set breakpoints, and run gdb on its
own.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;devbench-debug.png&quot; alt=&quot;DevBench debugging Nova Defense: stopped at a breakpoint with a live system-call trace and profile&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 3. Debugging Nova Defense in DevBench: stopped at a breakpoint, with the live system-call trace and profiler.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a-walkthrough&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#a-walkthrough&quot;&gt;&lt;/a&gt;A walkthrough&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Installing is one line — it checks prerequisites, clones the repo, fetches the toolchain,
builds the emulator, runs the test suite, and opens the demo:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-console hljs&quot; data-lang=&quot;console&quot;&gt;curl -fsSL https://raw.githubusercontent.com/geekychris/3do-dev/main/scripts/install.sh | bash&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;From there, a new game is a template away:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-console hljs&quot; data-lang=&quot;console&quot;&gt;./3do new mygame               # copy the template into projects/mygame
$EDITOR projects/mygame/src/main.c
./3do run mygame               # rebuild, boot, open a window (control :7330, gdb :2330)
./3do test mygame              # headless smoke test
./3do gdb mygame               # source-level debugging&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;While it runs, everything is scriptable over the control port:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-console hljs&quot; data-lang=&quot;console&quot;&gt;./3do ctl press buttons=A
./3do ctl screenshot path=shot.png
./3do ctl break addr=update_plasma
./3do ctl watch addr=s_palette     # stop when a global changes
./3do ctl save_state name=before-boss&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;And gdb behaves like gdb:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;listingblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre class=&quot;highlightjs highlight&quot;&gt;&lt;code class=&quot;language-text hljs&quot; data-lang=&quot;text&quot;&gt;(gdb) break main.c:396
(gdb) continue
Breakpoint 1, update_text (t_=1284) at src/main.c:396
(gdb) p *s_squares[0]
$1 = {ccb_Flags = 1063666704, ccb_NextPtr = 0x84670, ccb_SourcePtr = 0x88a44, ...}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The programs themselves are C89 for Norcroft: no FPU (16.16 fixed point and the operamath
folio), big-endian, and graphics as a linked list of cels drawn with one &lt;code&gt;DrawCels()&lt;/code&gt; call per
frame. The demo draws about 110 cels per frame at 60 fps alongside a 64×48 per-pixel software
plasma.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-hardest-problems&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-hardest-problems&quot;&gt;&lt;/a&gt;The hardest problems&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Making gdb believe Norcroft.&lt;/strong&gt; The DWARF was valid-ish but rejected in three silent ways, which
&lt;code&gt;tdo.elffix&lt;/code&gt; repairs after linking:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;armlink names every loadable section after the output file, so gdb relocated code but not
data — and globals were read from, and &lt;strong&gt;written to&lt;/strong&gt;, the wrong addresses. Fix: rename
sections by their flags to &lt;code&gt;.text&lt;/code&gt;, &lt;code&gt;.data&lt;/code&gt;, &lt;code&gt;.bss&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;DW_AT_stmt_list&lt;/code&gt; used &lt;code&gt;DW_FORM_addr&lt;/code&gt; instead of an offset form, so gdb ignored &lt;strong&gt;all&lt;/strong&gt;
line-number information. Fix: patch the abbreviation to &lt;code&gt;DW_FORM_data4&lt;/code&gt; — same size, so
nothing else moves.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;.debug_frame&lt;/code&gt; call-frame info used conventions gdb misreads, giving one-frame
backtraces. Fix: hide it; gdb&amp;#8217;s ARM prologue analyser unwinds this code correctly on its own.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Debug output that lies.&lt;/strong&gt; The kernel writes debug text a byte at a time to a MADAM register,
which the harness captures. But &lt;code&gt;kprintf()&lt;/code&gt; only formats its &lt;strong&gt;first three&lt;/strong&gt; arguments
correctly: it&amp;#8217;s a SWI, so arguments four and up land on the user stack, where the kernel never
looks. I spent a while doubting my own arithmetic before working that out. The kit&amp;#8217;s
&lt;code&gt;tdo_log()&lt;/code&gt; formats with &lt;code&gt;vsprintf&lt;/code&gt; and passes a single string.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;strong&gt;Cel engine details.&lt;/strong&gt; Clear &lt;code&gt;CCB_LAST&lt;/code&gt; on every cel except the last or the engine stops early;
&lt;code&gt;HDX/HDY&lt;/code&gt; are 12.20 fixed point while the rest are 16.16; a pixel value of 0 is transparent, so
opaque black has to be &lt;code&gt;0x0001&lt;/code&gt;. All of these are now written down in the repo&amp;#8217;s notes, verified
in the emulator.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;results&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#results&quot;&gt;&lt;/a&gt;Results&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The demo was written, built, tested, and debugged entirely through the kit. Then it got
ambitious: an &lt;strong&gt;Amiga compatibility layer&lt;/strong&gt; — a Paula mixer streaming to the 3DO audio folio, a C
ProTracker player, and fast paths for RectFill, polygon fills, and run-length sprites — let me
port &lt;strong&gt;eighteen Amiga games&lt;/strong&gt;, and three Unity games were rebuilt for the cel engine:
Planet Chomp, Spectral Keep, and Rolling Steel. Each has its own disc, and all of them sit on
an &lt;strong&gt;arcade disc&lt;/strong&gt; behind a menu.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;arcade-menu.png&quot; alt=&quot;The arcade disc&amp;#8217;s game menu&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 4. The arcade disc: every game behind one menu.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;spectral-keep.png&quot; alt=&quot;Spectral Keep&quot; width=&quot;an isometric adventure rebuilt from Unity for the 3DO cel engine&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 5. Spectral Keep — an isometric Unity adventure, rebuilt for the 3DO&amp;#8217;s cel engine.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;planet-chomp.png&quot; alt=&quot;Planet Chomp&quot; width=&quot;a sphere-maze game rebuilt from Unity for the 3DO&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 6. Planet Chomp, the Unity sphere-maze game on the 3DO.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;canyon_rescue.png&quot; alt=&quot;Canyon Rescue running on the 3DO through the Amiga compatibility layer&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Figure 7. Canyon Rescue, ported through the Amiga layer.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The repository&amp;#8217;s games gallery measures each one: many run their game logic at the Amiga&amp;#8217;s 50
steps a second, with drawing rates from about 9 fps for the most demanding to around 49 for the
simplest.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;next-steps&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#next-steps&quot;&gt;&lt;/a&gt;Next steps&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The kit is new — the first commits landed this month — and the obvious next steps are about
speed and polish: more fast paths in the Amiga layer for the slower ports, and more of the hardware
notes turned into tests. The bigger
lesson is the one I keep relearning on retro platforms: the machine is rarely the hard part.
The hard part is the loop around it, and once that loop is fast and scriptable, both people
and agents get a lot braver.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The code, docs, and every game are at &lt;a href=&quot;https://github.com/geekychris/3do-dev&quot;&gt;github.com/geekychris/3do-dev&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>First build of the new site</title>
    <link href="https://geekychris.github.io/projects/gitwebsite/first-build/"/>
    <id>https://geekychris.github.io/projects/gitwebsite/first-build/</id>
    <updated>2026-10-09T00:00:00Z</updated>
    <summary>The first end-to-end build — content, areas, templates, and publishing to GitHub Pages.</summary>
    <content type="html">&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The first version builds every page from AsciiDoc in a single Asciidoctor process, caches the
rendered HTML by content hash, and publishes by copying only the files it owns into the
GitHub Pages repository — so the books and other projects that already live there are left
alone.&lt;/p&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Building a full social network from scratch: feed, friends, groups, DMs, search</title>
    <link href="https://geekychris.github.io/articles/building-a-full-social-network-from-scratch-feed-friends-groups-dms-search/"/>
    <id>https://geekychris.github.io/articles/building-a-full-social-network-from-scratch-feed-friends-groups-dms-search/</id>
    <updated>2026-05-12T00:00:00Z</updated>
    <summary>What it takes to build a complete social network as one coherent app — feed ranking, social graph, search, messaging — with Spring Boot, React, PostgreSQL, OpenSearch, and a Rust graph cache.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Most modern social platforms aren’t a single thing — they’re a stack of intersecting systems (feed ranking, social graph, search, messaging, moderation, analytics) that have grown together over years. I wanted to know what it actually takes to build all of that as one coherent app, end-to-end, with no shortcuts. The result is the &lt;strong&gt;Social Enterprise Platform&lt;/strong&gt;: a full-featured social network built with Spring Boot, React, PostgreSQL, OpenSearch, and a custom Rust social-graph cache.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;02-home-feed.png&quot; alt=&quot;02 home feed&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;whats-in-it&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#whats-in-it&quot;&gt;&lt;/a&gt;What’s in it&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Far more than I expected when I started. The app ships with:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Personalized feed&lt;/strong&gt; with 80% organic &amp;#43; 20% recommended content, six reaction types, nested comments, link previews, cursor-based infinite scroll&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;User profiles&lt;/strong&gt; with follow/unfollow, follower lists, activity history&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Friend requests&lt;/strong&gt; with mutual-follow semantics&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Groups&lt;/strong&gt; — public or restricted, role-based (Owner / Admin / Member), pending approval flow, pinned posts&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pages&lt;/strong&gt; for organizations or topics, with their own feeds&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Direct messages&lt;/strong&gt; with real-time conversations and file attachments&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt; across users, groups, and pages via OpenSearch (with PostgreSQL fallback)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Admin dashboard&lt;/strong&gt; with DAU/WAU/MAU, engagement analytics, moderation tools&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AI support&lt;/strong&gt; with Lucene KNN vectors, answer caching, knowledge-gap analysis, and feedback loops&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;04-profile.png&quot; alt=&quot;04 profile&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;the-interesting-parts&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-interesting-parts&quot;&gt;&lt;/a&gt;The interesting parts&lt;/h3&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;aoee-a-rust-social-graph-cache&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#aoee-a-rust-social-graph-cache&quot;&gt;&lt;/a&gt;AOEE: a Rust social-graph cache&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Postgres handles the durable social graph, but reading “who follows whom” on every feed request is too slow at any reasonable scale. AOEE is a small Rust service that holds the graph in memory with efficient bitset operations and answers reachability &amp;#43; mutual-friend queries in microseconds. Updates flow in via Postgres LISTEN/NOTIFY and a write-behind queue.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;feed-ranking-as-a-pluggable-mix&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#feed-ranking-as-a-pluggable-mix&quot;&gt;&lt;/a&gt;Feed ranking as a pluggable mix&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The 80/20 organic-to-recommended split isn’t hardcoded; it’s a ranking pipeline with explicit stages (candidate generation, scoring, diversification, blending). Each stage is testable in isolation. The recommended path uses Lucene KNN vectors over a content-embedding index.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;ai-support-that-improves-over-time&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#ai-support-that-improves-over-time&quot;&gt;&lt;/a&gt;AI support that improves over time&lt;/h4&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;03-search.png&quot; alt=&quot;03 search&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The latest iteration adds a full AI support system: an intent classifier routes user questions, a retrieval layer pulls relevant context from the platform’s knowledge base, an LLM answers, and every interaction feeds back into a knowledge-gap dashboard. When too many users hit the same gap, an admin sees it surface in the metrics view.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-i-learned&quot;&gt;&lt;/a&gt;What I learned&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Social-app surfaces are deceptively simple. A “comment” looks like a row in a table; in practice it’s three database rows, a search-index update, an OpenSearch refresh, a webhook fan-out, a moderation check, and a feed-cache invalidation. Building all of that yourself is humbling. But you understand the system in a way no managed SaaS ever lets you.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;source&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#source&quot;&gt;&lt;/a&gt;Source&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/geekychris/enterprise_social_platform&quot;&gt;github.com/geekychris/enterprise_social_platform&lt;/a&gt; — 34 commits in, with full screenshots, architecture docs, and a Docker Compose stack that brings up the entire system locally.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;architecture-diagrams&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#architecture-diagrams&quot;&gt;&lt;/a&gt;Architecture diagrams&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Rendered from the Mermaid blocks in the project README so the post stays in sync with the source of truth.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;social_enterprise_1.png&quot; alt=&quot;Architecture diagram&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;social_enterprise_2.png&quot; alt=&quot;Architecture diagram&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>ViewScene: a desktop Luke alternative for inspecting Lucene indexes</title>
    <link href="https://geekychris.github.io/articles/viewscene-a-desktop-luke-alternative-for-inspecting-lucene-indexes/"/>
    <id>https://geekychris.github.io/articles/viewscene-a-desktop-luke-alternative-for-inspecting-lucene-indexes/</id>
    <updated>2026-05-11T00:00:00Z</updated>
    <summary>A standalone JavaFX desktop app for inspecting and diagnosing Lucene indexes — documents, analyzers, and vector fields — as a desktop alternative to Luke.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you’ve spent any time with &lt;a href=&quot;https://lucene.apache.org/&quot;&gt;Apache Lucene&lt;/a&gt; as a developer, you’ve probably wanted to peek inside an index. Maybe a query isn’t matching what you expected. Maybe the analyzer is doing something surprising. Maybe a vector field has the wrong dimensionality. The classic answer is &lt;a href=&quot;https://github.com/DmitryKey/luke&quot;&gt;Luke&lt;/a&gt;, the Lucene Inspector — but Luke is a web app these days, and I missed having a real desktop tool.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So I built &lt;strong&gt;ViewScene&lt;/strong&gt;: a standalone JavaFX desktop application for inspecting and diagnosing Lucene indexes. Self-contained fat JAR. Native desktop UI. No web server, no browser, no external dependencies beyond a JDK.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;viewscene.png&quot; alt=&quot;viewscene&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;what-it-shows-you&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-it-shows-you&quot;&gt;&lt;/a&gt;What it shows you&lt;/h3&gt;
&lt;div class=&quot;ulist&quot;&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fields browser&lt;/strong&gt; — every field across the index, with type, doc count, indexed/stored flags, vector dimensions if any&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Document viewer&lt;/strong&gt; — pick any doc by ID, see every field’s stored value, indexed terms, and term positions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Term dictionary&lt;/strong&gt; — the actual postings list per field, browsable and searchable, with document frequencies&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt; — run real Lucene queries against the open index, see results ranked with explanations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Vector search&lt;/strong&gt; — for KNN-indexed fields, run a vector query (with explicit embedding input or text-to-embedding via a configured analyzer)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Analysis pipeline&lt;/strong&gt; — type in text, pick a field, see exactly what tokens come out at every analyzer stage. The most useful tab when you’re trying to understand “why doesn’t this match?”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Index tools&lt;/strong&gt; — segment listings, commit history, optimize / force-merge / delete operations (read-write mode is opt-in)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;05-vector-search.png&quot; alt=&quot;05 vector search&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;the-interesting-parts&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-interesting-parts&quot;&gt;&lt;/a&gt;The interesting parts&lt;/h3&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;fat-jar-with-no-install&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#fat-jar-with-no-install&quot;&gt;&lt;/a&gt;Fat JAR with no install&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Download one file, double-click, you’re inspecting an index. No Maven repo to bootstrap, no Node modules, no server to run alongside. The whole thing — UI, Lucene core, vector support — gets packed into a single executable JAR via the Maven Shade plugin with merged service files. Total weight is around 50 MB, which is mostly the Lucene &amp;#43; JavaFX runtime.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;direct-api-access-not-a-query-layer&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#direct-api-access-not-a-query-layer&quot;&gt;&lt;/a&gt;Direct API access, not a query layer&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Many index inspectors are wrappers around a query DSL. ViewScene uses the Lucene API directly: &lt;code&gt;IndexReader.leaves()&lt;/code&gt;, &lt;code&gt;Terms.iterator()&lt;/code&gt;, &lt;code&gt;PostingsEnum&lt;/code&gt;, &lt;code&gt;FloatVectorValues&lt;/code&gt;. The UI panels are thin views over those APIs. That keeps the tool honest — if Lucene shows it, ViewScene can show it. If Lucene doesn’t, neither does ViewScene.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;vector-search-with-explainability&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#vector-search-with-explainability&quot;&gt;&lt;/a&gt;Vector search with explainability&lt;/h4&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;06-analysis.png&quot; alt=&quot;06 analysis&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The vector-search panel does what Luke historically hasn’t: lets you submit a query vector (either as numbers or as text routed through an analyzer &amp;#43; embedding hook), runs a real &lt;code&gt;knnFloatVectorQuery&lt;/code&gt;, and shows the actual distance scores per hit. Combined with the analysis pipeline tab, this turned out to be invaluable for debugging KNN-based “AI search” features: you can see why doc 47 ranked above doc 12 in actual numbers.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;origin-hitorro&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#origin-hitorro&quot;&gt;&lt;/a&gt;Origin: hitorro&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The core viewer module was originally part of the &lt;a href=&quot;https://github.com/hitorro&quot;&gt;Hitorro&lt;/a&gt; project. I extracted it into a standalone, fully independent tool so it could be used outside that context. The base was clean; the standalone work was mostly stripping coupling and packaging.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;why-this-matters&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#why-this-matters&quot;&gt;&lt;/a&gt;Why this matters&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;If you build anything on Lucene, you eventually need to look inside an index. ViewScene exists so that “look inside an index” is one double-click away — same as opening a database GUI. The web-based alternatives are great when you have infrastructure for them; for the case where you just want to drag-drop an index path and look at fields, a desktop tool wins.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;source&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#source&quot;&gt;&lt;/a&gt;Source&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/geekychris/lucene_viewer&quot;&gt;github.com/geekychris/lucene_viewer&lt;/a&gt;. JavaFX, fat JAR, MIT license.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Building SolarSage: a local-first solar monitoring tool</title>
    <link href="https://geekychris.github.io/articles/building-solarsage-a-local-first-solar-monitor-that-forecasts-the-ac/"/>
    <id>https://geekychris.github.io/articles/building-solarsage-a-local-first-solar-monitor-that-forecasts-the-ac/</id>
    <updated>2026-05-10T00:00:00Z</updated>
    <summary>A local-first monitor for an off-grid 15 kW solar system that forecasts surplus power from the weather and tells you when to run heavy loads.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I have an &lt;strong&gt;off-grid in San Felipe, Mexico&lt;/strong&gt;, with a 15 kW EG4 solar system and a wall of lithium batteries. When the grid isn’t there, “do I have enough power to run the AC tonight?” stops being abstract. The vendor dashboard at &lt;a href=&quot;https://monitor.eg4electronics.com&quot;&gt;monitor.eg4electronics.com&lt;/a&gt; shows live numbers and a few canned charts, but it doesn’t &lt;em&gt;predict&lt;/em&gt; anything. I wanted a tool that did.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;So I built &lt;strong&gt;SolarSage&lt;/strong&gt; — a local monitor that polls my system every minute, captures everything in SQLite, layers a weather-aware forecast on top, and turns “you’ll have surplus solar” into “run the water heater between 12 and 3 PM tomorrow.” It runs entirely on my laptop, with no cloud component and nothing leaving my network.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The bigger goal is intelligence: predicting how much energy I’ll produce, how much the AC will draw at any given outdoor temperature, and where the headroom is. Off-grid living rewards that kind of forecasting in a way grid-tied living doesn’t — if you guess wrong, you run out, or you run a generator.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;dashboard_top.annotated.png&quot; alt=&quot;dashboard top.annotated&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#what-it-does&quot;&gt;&lt;/a&gt;What it does&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The architecture is intentionally boring: a FastAPI backend polls the EG4 portal every 60 seconds and writes every numeric field to a SQLite database. A small React UI reads from the backend. Forecasts and analytics layer on top. Everything runs on &lt;code&gt;127.0.0.1&lt;/code&gt;; no cloud, no telemetry, no signup. Credentials live in a &lt;code&gt;0600&lt;/code&gt; file on my laptop and that’s it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The interesting parts are the things that surprised me on the way.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;1-eg4-has-no-public-api-but-it-has-chart-endpoints&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#1-eg4-has-no-public-api-but-it-has-chart-endpoints&quot;&gt;&lt;/a&gt;1. EG4 has no public API — but it has chart endpoints&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The official “API documentation” page is marketing. What the portal actually has is a set of undocumented JSON endpoints that power the web UI. After some reverse-engineering (and reading &lt;a href=&quot;https://github.com/joyfulhouse/pylxpweb&quot;&gt;other people’s work&lt;/a&gt;), I found that &lt;code&gt;/WManage/api/analyze/chart/dayLine&lt;/code&gt; with &lt;code&gt;attr=ppv1&lt;/code&gt; returns full-day timeseries for any attribute — but only &lt;em&gt;some&lt;/em&gt; attributes work, and they vary by firmware. On my SNA-US 15K, &lt;code&gt;ppv&lt;/code&gt; as a chart attr returns empty; I have to sum &lt;code&gt;ppv1&amp;#43;ppv2&amp;#43;ppv3&lt;/code&gt; myself. The “all channels at once” endpoint other libraries use (&lt;code&gt;dayMultiLineParallel&lt;/code&gt;) returns &lt;code&gt;data: []&lt;/code&gt; entirely on this firmware.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The real gotcha: the chart endpoint’s &lt;code&gt;month&lt;/code&gt; field is &lt;strong&gt;Java zero-indexed&lt;/strong&gt;. May = 4. I caught it because timestamps came out exactly one month off — a great way to lose an afternoon if you trust the field names.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;2-free-weather-is-finally-great-for-solar&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#2-free-weather-is-finally-great-for-solar&quot;&gt;&lt;/a&gt;2. Free weather is finally great for solar&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://open-meteo.com&quot;&gt;Open-Meteo&lt;/a&gt; is the only free, keyless weather service I’ve found that returns &lt;strong&gt;forecasted shortwave irradiance (GHI)&lt;/strong&gt;, not just cloud cover. That changes the modeling. Instead of estimating PV from “30% clouds”, I multiply the forecast GHI by my system’s measured W-per-W/m² calibration (about 9.08 for my install) and get tomorrow’s predicted PV curve hour by hour. The historical archive endpoint lets me back-fit other models too.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;weather.annotated.png&quot; alt=&quot;weather.annotated&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;3-the-ac-model-is-one-line-of-math&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#3-the-ac-model-is-one-line-of-math&quot;&gt;&lt;/a&gt;3. The AC model is one line of math&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;My biggest variable load is air conditioning, and AC tracks outdoor temperature almost linearly above the comfort threshold. SolarSage fits a cooling-degree model from joint Open-Meteo &amp;#43; load history:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;load(hour) = base(hour) + slope_W_per_°F · max(0, °F − threshold)&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Grid-search the threshold from 50°F to 90°F, OLS-fit the slope on the residual. After 15 days of data my model lands at threshold=71°F, slope=78 W/°F, R²=0.43. Not yet impressive — but better than the constant-load assumption every other tool uses, and it sharpens automatically as more days accumulate.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;4-the-dashboard-already-caught-something&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#4-the-dashboard-already-caught-something&quot;&gt;&lt;/a&gt;4. The dashboard already caught something&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The string-health panel watches each PV string’s daily ratio against the strongest. Within hours of finishing it, the panel flagged &lt;strong&gt;ppv3 = 0% across every day&lt;/strong&gt;. Either I have an unused third string (likely), or a fuse blew, or there’s a wiring fault. Either way, the alert came from the system itself — exactly the kind of slow drift the EG4 portal hides in plain sight.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect3&quot;&gt;
&lt;h4 id=&quot;5-forecasts-concrete-recommendations&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#5-forecasts-concrete-recommendations&quot;&gt;&lt;/a&gt;5. Forecasts → concrete recommendations&lt;/h4&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The “smart load scheduler” turns the surplus forecast into actionable windows: for each enabled appliance, scan the next 48 hours for a slot where &lt;em&gt;every hour&lt;/em&gt; clears the appliance’s watts — not just the average. Right now it’s telling me to run the water heater &amp;#43; EV charger &amp;#43; dishwasher all in the same 12-15:00 block tomorrow, because that’s where 7.7 kW of sustained surplus lives. Pool pump is disabled because I don’t have one; electric dryer is disabled because mine’s gas. The scheduler respects what I told it I actually have.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;scheduler.annotated.png&quot; alt=&quot;scheduler.annotated&quot;&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect2&quot;&gt;
&lt;h3 id=&quot;try-it&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#try-it&quot;&gt;&lt;/a&gt;Try it&lt;/h3&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Source is at &lt;a href=&quot;https://github.com/geekychris/solarsage&quot;&gt;github.com/geekychris/solarsage&lt;/a&gt;. The one-liner install is:&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;literalblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;pre&gt;curl -fsSL https://raw.githubusercontent.com/geekychris/solarsage/main/install.sh | bash&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It runs on macOS, Linux, and Windows. The architecture doc has Mermaid diagrams of the data flow; the installation doc has manual steps if you’d rather not &lt;code&gt;curl | bash&lt;/code&gt;. The Q.Cells adapter is stubbed pending information on which monitoring portal Q.Cells installs actually talk to — drop me a note if you have one.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
  <entry>
    <title>Building the Amiga 40th: a carousel of progress</title>
    <link href="https://geekychris.github.io/projects/the-amiga-40th-exhibit/building-the-amiga-40th-a-carousel-of-progress/"/>
    <id>https://geekychris.github.io/projects/the-amiga-40th-exhibit/building-the-amiga-40th-a-carousel-of-progress/</id>
    <updated>2025-08-01T00:00:00Z</updated>
    <summary>How a conversation with Bill Borsari became a walk-through history of the Amiga at the Vintage Computer Festival. It was built almost entirely at my home, some 2,500 people came through, and we finished the day with the Amies.</summary>
    <content type="html">&lt;div id=&quot;preamble&quot;&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The Amiga turned 40, and we wanted to do something special about it.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Bill Borsari and I had been talking informally about doing something for a while, but the real
start was the last day of AmiWest 2024. On the way back from the show, Bill and I had a
multi-hour conversation and started to flesh out the idea. When I got home I wrote up a
&quot;treatment&quot; of the show, and over the next few weeks I sketched out my thoughts while Bill started
sketching the floor layout.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We talked to Zach Hardesty from the Vintage Computer Festival, and he was fully supportive of helping us
celebrate the 40th at the Vintage Computer Festival at the Computer History Museum. Over the
following months we talked with the VCF staff, who at first had a hard time understanding what we
were trying to do. Perhaps that&amp;#8217;s because, up to then, most VCF exhibits had been a few treasured machines on a
table. A fully immersive exhibit, more like a pop-up museum, took a little more imagination. :-)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;a-carousel-of-progress-for-the-amiga&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#a-carousel-of-progress-for-the-amiga&quot;&gt;&lt;/a&gt;A carousel of progress, for the Amiga&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We wanted to tell the story of the Amiga, and I proposed modelling it on Disney&amp;#8217;s Carousel of
Progress: walk through a series of scenes, each one a moment in time.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It starts at the first CES that Amiga attended. Out front, Amiga showed off joysticks and games
for 8-bit computers. Behind a curtain, by invitation only, was the Amiga computer itself, still
in its TTL wire-wrap prototype form. So our exhibit began with a facade of the CES 1984 booth,
and a curtain took you through to the wire-wraps.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-g7xqmks/0/LfVRsbWV4JRFs673ZtV8hChqG7k6CpjCDgVkZLZRJ/X3/IMG_3718-X3.jpg&quot; alt=&quot;The recreated CES 1984 booth&quot; width=&quot;with a red carpet&quot; height=&quot;an AMIGA header and a video game cabinet&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The CES exhibit, complete with video games.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-sVVvchd/0/NL2p2Nd3zWqvSfZTMSX69frQNvB6Lw5CXB7KW7T5J/X3/IMG_3712-X3.jpg&quot; alt=&quot;Several racks of wire-wrapped prototype circuit boards on a table in front of a curtain&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The original Amiga TTL wire-wrap prototype boards.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-3mCDPMt/0/LFS6qTM94JPVkQxN9MLfB3Zg3kV8pW6Nqzt6CkjRh/X3/IMG_3709-X3.jpg&quot; alt=&quot;A display of early computers&quot; width=&quot;keyboards and equipment on black tables&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Some of the black devbox tooling.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-jWf3CSK/0/KqTRVp5zS6CbPkXf9bTNNcDdQPMBdsx6TVvKnPPxm/X3/IMG_3711-X3.jpg&quot; alt=&quot;Vintage computers and equipment on a table&quot; width=&quot;with a small screen on the curtain behind&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Part of the prototype demo.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-k4ngrJb/0/Lg6tKbftRwVkHgWQxJ5pLvkxj8SnLDs7KbkV5Z3Bf/X3/IMG_3705-X3.jpg&quot; alt=&quot;Two room sets in the hall: a bedroom with a rug and a desk&quot; width=&quot;and a video office&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The bedroom and the NewTek video scenes, side by side.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Alongside them was the tooling it took to run the Amiga, including a Sage computer, and some of
the black devbox work, in operation.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;From there you walked into a reenactment of a typical bedroom, where so many kids had an Amiga
500 to play games and &quot;help do homework&quot;. Next door we paid homage to the Video Toaster with a
recreation of a video booth and office.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-svz7X7m/0/Kn5BNJG7CqbKXHD9L5T7Ns4Hbx5dtHFQJtbccPgPZ/X3/IMG_3727-X3.jpg&quot; alt=&quot;The NewTek video office scene with monitors and equipment&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The NewTek booth.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Then the Future Wall: lots of new Amigas, all stripped bare and mounted on a single wall, and
synchronised so they worked as one big display. We also showed the industrial design work of Rob
Gemmell. Each scene had a kiosk playing a video that explained it. Bill produced the videos, with
some help from AI; the scripts and ideas were all his. I built the kiosks and some of the software
that drove the videos.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;building-it-at-home&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#building-it-at-home&quot;&gt;&lt;/a&gt;Building it at home&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Almost all of the exhibit was made at my home, with help from Winnie, my son Kyle and Alan
Swithenbank. The planning started after AmiWest in October, but nearly all of the real making
didn&amp;#8217;t begin until July.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The hardest part was the trial and error of working out how to put it all together quickly and on
budget. I did a lot of prototyping for things like the CES facade and how we would construct the
bedroom. The art for the large prints in the bedroom and the video room was done by Evangelos
Goulas.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-VVF7RCP/0/NPBGsxR7XNmC5HcMRKmxD4z69jBzKkPtD4GNQ5BwD/X3/IMG_3356-X3.jpg&quot; alt=&quot;Alan cutting timber for a tall wooden frame on a driveway&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Alan building the frame that housed the demo monitor in the secret prototype room.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-M2Hcz4W/0/LpRd6KRXZtsCnML8PZMk29mgbm5ZJVXp78jzXbqhW/X3/IMG_3376-X3.jpg&quot; alt=&quot;A long red&quot; width=&quot;white and blue painted panel strapped onto a pickup truck&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Part of the CES facade, loaded onto Dale&amp;#8217;s truck.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-amies-trophies&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-amies-trophies&quot;&gt;&lt;/a&gt;The Amies trophies&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the evening we held the Amies, giving out awards to people who have contributed to the Amiga.
There were lots of small awards for different kinds of contribution, some very small ones to give
away, and one very large one that served as a display piece and for people to hold in photographs.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The trophy is a lemming. It was designed in LightWave by an Amiga fan in eastern Europe, because
at the time we couldn&amp;#8217;t get AI to produce a model that was printable. I printed, cleaned and
painted them.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-rCCdgB3/0/KgHBrmFmKcj4987vwdRTTFQDqjx6rzD2hZrsKNTW3/X3/IMG_3350-X3.jpg&quot; alt=&quot;A small gold lemming trophy in front of a large grey 3D-printed lemming&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;A giveaway lemming beside the full-size one we used for award-ceremony photos and promotion.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-MM3bRZc/0/Mt8ddDR3RpcX9qWdjPXk9t79pmMRDhXCwPbp52JCV/X3/IMG_3364-X3.jpg&quot; alt=&quot;A large gold lemming figure waving&quot; width=&quot;on a red and white boing-ball base&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The finished full-size lemming.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-66Pt2hz/0/LDS3htNr9pwZtfXNbzJWq3jKvb7knGwRfVD9NxKVb/X3/IMG_3387-X3.jpg&quot; alt=&quot;Close-up of the red and white checked boing-ball base under the gold figure&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Getting the boing-ball detail right.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-n3XtjJw/0/KL82cqpQBSX2dkVqh2fJXtm77tshm5mfstJBsPPfW/X3/IMG_3444-X3.jpg&quot; alt=&quot;Winnie holding up a tray of small gold lemming figures in a kitchen&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Winnie with a tray of giveaway lemmings, after a lot of post-print cleanup and gluing.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Each award carried a chip that said what it was for, so of course we made a giant one too.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-wKh4nMZ/0/NRLmrKpSKfWjq2dPKdxQgSfx5nGHGx8nNp3mZxg4b/X3/IMG_3532-X3.jpg&quot; alt=&quot;Winnie painting a red and white boing-ball on a large black chip&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Winnie putting the finishing touches on our massive chip.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-jLm3zms/0/NHxdkN45J7SxGJp9HwnM2C5J5HWNWHFnzmQ8sGwxM/X3/IMG_3533-X3.jpg&quot; alt=&quot;Me holding up a large black chip with a boing ball&quot; width=&quot;labelled AMIGA/040&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The super-sized chip. Smaller ones on each award said what it was for; this one went with Bill&amp;#8217;s lemming outfit to promote the evening.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-fV3bzvF/0/LFJj4GKgLNRQ3mqhVFt93VCzc8XrQQn4Qr33wdJGg/X3/IMG_3677-X3.jpg&quot; alt=&quot;A table full of small gold lemming awards with their chips&quot; width=&quot;lit in blue&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The Amies.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-2mSFNzP/0/Mv6NgnM7s5SNhktbfRHw4TWHCPv8FnzK5Zv6hQx6W/X3/IMG_3752-X3.jpg&quot; alt=&quot;The large gold lemming on display at the exhibit&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Mama Lemming.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-future-wall&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-future-wall&quot;&gt;&lt;/a&gt;The Future Wall&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The Future Wall needed lots of Amigas to behave as one display. Chris Hopper and his son designed
a device that plugged into every Amiga through its joystick port and could strobe through each
one in turn. An ARexx script on each Amiga reacted to the trigger and changed what it showed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-S4h6hSh/0/M864JTgVxfx488rH9QGrfvQXnvBBGS94n675fR3ZH/X3/IMG_3450-X3.jpg&quot; alt=&quot;Black screens and red boards laid out on a wooden panel marked with blue tape&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Laying out the Future Wall.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-4rWrjgN/0/NV42ZCJ8zLQFnqTjnxQ2wcdCKRGrtsgNVqmFbGSG7/X3/IMG_3724-X3.jpg&quot; alt=&quot;The finished Future Wall: screens and bare Amiga boards on a wall under a FUTURE WALL sign&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The Future Wall.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;moving-in&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#moving-in&quot;&gt;&lt;/a&gt;Moving in&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-LqP9jLB/0/MvPpsRPn6sGQSjrS2PhbgrL7MwrJrRp7DT6kkr4Mx/X3/IMG_3528-X3.jpg&quot; alt=&quot;A large&quot; width=&quot;empty exhibition hall with windows along one side&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The empty floor, just before we turned it into CES 1984.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-hd72h7n/0/LqRzqXMQ5DLXN8w2cwtgTtbGL2PNCrG74nXhp9XdC/X3/IMG_3540-X3.jpg&quot; alt=&quot;Two people hanging a large black curtain on a frame&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Michael Slifak and Trevor Dickinson helping to set up the exhibit.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-GFBn3Cz/0/Ljvk4ST55np5k9F26QvfpZd2TLgwJ5jJrC9CNRfhS/X3/IMG_3547-X3.jpg&quot; alt=&quot;Three people lifting a long AMIGA sign onto the booth on a red carpet&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Michael, Don and Dale putting up the official Amiga sign.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-DsPb9qx/0/NNJKDcSDwXVSFhSBb5FFRjrJwdcnvR4bhM6NfXShp/X3/IMG_3550-X3.jpg&quot; alt=&quot;Three people working around a table in the exhibit&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Michael, Hedley and Jeff adding the finishing touches.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-sZfLSpx/0/MGfs3vM3xRpDqppjMg2XZJrzhS8pJVwRXw2FfsV7S/X3/IMG_3556-X3.jpg&quot; alt=&quot;Tim Jenison signing a banner that reads 1985 History was written&quot; width=&quot;2025 Write yours&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Tim Jenison, founder of NewTek, signing our visitor banner.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-29bQ2HH/0/Kjh6N5gDq9smVHfjcWWshCggTJW2wRKzD5pdPZsps/X3/IMG_3562-X3.jpg&quot; alt=&quot;Jeff Bruette in a cap signing the banner&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Jeff Bruette signing the banner. Jeff worked at Amiga and taught Andy Warhol to use the Amiga for the launch.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-RgMpH77/0/KWnk4C8MZh9CGHPdnkBvQ4x4zzBZtjCh926dhDNkz/X3/IMG_3567-X3.jpg&quot; alt=&quot;Colin Proudfoot standing beside the signed banner&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Colin Proudfoot adds his name.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We also put up a banner for visitors to sign, and some of the first signatures came from people
who made the Amiga&amp;#8217;s history.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;We did drop a few things at the last minute. We had planned more parts to the exhibit, but time and
space ran out.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-day&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-day&quot;&gt;&lt;/a&gt;The day&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-Gfnhb63/0/Kk25kwh3fp3NHWBnSz7j4QHhbwHQ3rZZdp4TtwxGF/X3/IMG_3716-X3.jpg&quot; alt=&quot;The full exhibit: the CES facade with AMIGA header&quot; width=&quot;red carpet and an arcade machine&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The CES 1984 facade: the entrance to the Amiga 40th.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;People were amazed. Some 2,500 came through.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-JGPgzNN/0/MXqKnWCfLhJPM3VJRRNBzdMmHQv7Vh3Q2Ww6ZDSQc/X3/IMG_3743-X3.jpg&quot; alt=&quot;Visitors walking through the curtain into the exhibit&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Some 2,500 people came through.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-tFZr7cn/0/MxgGgsjXzPZGvkf96hZSKrf78R3PTg7nQwcVXkbC5/X3/IMG_3748-X3.jpg&quot; alt=&quot;A visitor&amp;#8217;s feet on a joypad on the red carpet&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Playing at the CES exhibit with an Amiga joypad.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;videoblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Amy Herndon enjoying our fluffy red carpet. It was a bit like dancing on a polar bear.&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;video src=&quot;https://photos.smugmug.com/Amiga-40th/i-VNdrqQx/0/LKSQn6j26bVKvqmWd3ksB4gBdkHmgxfmXFpQVJTWN/1920/IMG_3613-1920.mp4&quot; controls&gt;
Your browser does not support the video tag.
&lt;/video&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-tVHpZGq/0/NB496BF6FKNpzWr2sbZmGc4qzR7SnsMxBKhnmGRTB/X3/IMG_3614-X3.jpg&quot; alt=&quot;Jeri Ellsworth at an Amiga 500 in the bedroom scene&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Jeri Ellsworth living out her childhood in our bedroom scene.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-7VtgBHC/0/Mw9zhhJfxjx7hX6gQqKSqrtw772cmG9GjqmdbnVnd/X3/IMG_3725-X3.jpg&quot; alt=&quot;A visitor playing an arcade cabinet&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;An Amiga-based arcade machine from Dale Luck&amp;#8217;s collection.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;I showed Leonard Tramiel and Joe Decuir around personally. They both responded like kids at
Christmas. Their eyes lit up, and they shared story after story.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-wfCRRxk/0/NKmTcS7gfK37GCvDpJJ8jg9XjfrMGPr9fNM9wrCGm/X3/IMG_3753-X3.jpg&quot; alt=&quot;Me with Joe Decuir in front of the red curtain&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;With Joe Decuir, Amiga&amp;#8217;s original systems architect (Dave Needle later took over the role), talking about the early days. He was like a kid in a candy shop.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-S9kTXRg/0/MZc93Szcmj7GNjgPnsn7JcPBT4vxPWjscXJhGMRCP/X3/IMG_3596-X3.jpg&quot; alt=&quot;Me with Rob Gemmell beside a display of his design drawings&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;With Rob Gemmell, who did the industrial design for the Apple IIc and went on to draw the first designs of what became the Amiga 2000 (Ranger, which was about packaging, not a new chipset!) and the Amiga 500.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-kKzXTw8/0/LBt5gTFjzjKQKG5NnsSGcKZ2ZfqZws9Wpz8CM5L6b/X3/IMG_3750-X3.jpg&quot; alt=&quot;Me with Paul Lassa&quot; width=&quot;who is holding a small lemming award&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;With Paul Lassa, who designed the Amiga 4000T and the C65.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-RMQLS5G/0/LDJVfpFZKd9F3mD4BmkpgqtgmCTLWrPR7ddMNZthT/X3/IMG_3601-X3.jpg&quot; alt=&quot;Two people working on the Video Toaster setup in the video scene&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Tim Jenison and Bill repairing a NewTek Video Toaster.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;One visitor got so excited that he started &quot;panic explaining&quot; to his girlfriend what the Amiga
meant to him. He mentioned playing on his 500 in his bedroom. At that point I said &quot;hold on&quot; and
walked him over to the bedroom scene, where he actually started crying.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The special thing for me was all the original Amiga and Commodore employees who helped. It was
also the last time I got to see Hedley Davis, who sadly passed away not long after. Hedley spent
most of the day explaining the TTL hardware to visitors.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;the-evening&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#the-evening&quot;&gt;&lt;/a&gt;The evening&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;videoblock&quot;&gt;
&lt;div class=&quot;title&quot;&gt;Bill drumming up business for the evening event.&lt;/div&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;video src=&quot;https://photos.smugmug.com/Amiga-40th/i-pDXp2Tb/0/NKVDD3p2dFrt32MNSsPfs2xvwgKT4L2ZFnb8xtx6W/1920/IMG_3621-1920.mp4&quot; controls&gt;
Your browser does not support the video tag.
&lt;/video&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;In the evening we paid homage to the 1985 Amiga launch, and held the Amies.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;openblock gallery&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-kLjkGts/0/L35PhLXfxHs49ZLwPKnjMgvRF3kfD2szBwnt7p6XH/X3/IMG_3642-X3.jpg&quot; alt=&quot;A packed auditorium facing the stage&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The evening: our homage to the 1985 Amiga launch, and the Amies award ceremony.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-Ntz9sjs/0/KgfSKJGmtSqhb79mqQqJqMHmn5D3HQZLC9kTNMNV7/X3/IMG_3684-X3.jpg&quot; alt=&quot;On stage under a screen reading The winner is Trevor Dickinson&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Amiga Bill, Bill Borsari and Trevor Dickinson.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-NPCGgDf/0/Nd7gM5NJKGxHfdbww5tLwdkB8LnG9NQM9BLR7v2NC/X3/IMG_3698-X3.jpg&quot; alt=&quot;Kiki Stockhammer holding a lemming award&quot; width=&quot;with Tim Jenison beside her&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Tim Jenison and Kiki Stockhammer of NewTek at the Amies, with Kiki playing the role of Debbie Harry.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;Not everything went to plan. For the 1985 launch homage we were going to use Amiga speech to talk
back, but we had technical issues, and problems with the video setup and feed caused a long delay
in the evening event.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-fgzqkrq/0/MqtshGN58nQkzMqzxfTgT9ncTPtNmxQ2kG4sZ4rPp/X3/8de51f29-3e7d-4f3d-90ad-bbf508a94db3-X3.jpg&quot; alt=&quot;The team in tuxedos holding the large lemming in front of a press backdrop&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;The Amiga 40th team after the Amies, in tuxedos as a nod to the original 1985 launch at Lincoln Center.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;teardown&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#teardown&quot;&gt;&lt;/a&gt;Teardown&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;imageblock&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;img src=&quot;https://photos.smugmug.com/Amiga-40th/i-ZF97fQb/0/NdKKnr9R8RmB4hBg5gWLn7KPnXs7GmQDkrdgBnqZc/X3/IMG_3757-X3.jpg&quot; alt=&quot;Ron Nicholson taking down a curtain&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;title&quot;&gt;Ron Nicholson helping to tear down the exhibit.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;It was amazing to see my personal idols taking part. We did this to celebrate them, and at the
same time they all rolled up their sleeves and made the event fantastic.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;sect1&quot;&gt;
&lt;h2 id=&quot;thanks&quot;&gt;&lt;a class=&quot;anchor&quot; href=&quot;#thanks&quot;&gt;&lt;/a&gt;Thanks&lt;/h2&gt;
&lt;div class=&quot;sectionbody&quot;&gt;
&lt;div class=&quot;paragraph&quot;&gt;
&lt;p&gt;The biggest thanks goes to &lt;strong&gt;Bill Borsari&lt;/strong&gt;, who was an amazing partner to work with: from the
conversation that started it all to the kiosk videos. Everyone else who made it happen is on the
&lt;a href=&quot;/projects/the-amiga-40th-exhibit/&quot;&gt;project page&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  
</feed>
