<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Game-Engine on Max Bonnefin</title><link>https://bonnef.in/tags/game-engine/</link><description>Recent content in Game-Engine on Max Bonnefin</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>max@bonnef.in (Max Bonnefin)</managingEditor><webMaster>max@bonnef.in (Max Bonnefin)</webMaster><copyright>Max Bonnefin</copyright><lastBuildDate>Thu, 22 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://bonnef.in/tags/game-engine/index.xml" rel="self" type="application/rss+xml"/><item><title>Getting the Memory Back</title><link>https://bonnef.in/posts/getting-the-memory-back/</link><pubDate>Thu, 22 Oct 2026 00:00:00 +0000</pubDate><author>max@bonnef.in (Max Bonnefin)</author><guid>https://bonnef.in/posts/getting-the-memory-back/</guid><description>&lt;p&gt;Almost every line of code I&amp;rsquo;ve written in my career has run on top of a garbage collector. Java, TypeScript, Python, Go. Languages where memory is something the runtime worries about so I don&amp;rsquo;t have to, and where for years I was happy to let it. My general day to day backend work doesn&amp;rsquo;t punish me for not knowing where my bytes live.&lt;/p&gt;
&lt;p&gt;I read a book on data-oriented design a few years ago and it stuck with me, but I never had a project where putting it into practice made sense. So I set about building a small game engine as an excuse to manage my own memory for once, and picked &lt;a href="https://odin-lang.org/" class="external-link" target="_blank" rel="noopener"&gt;Odin&lt;/a&gt; to do it in because it stays out of the way while you work at that level. The language is the vehicle here, not the destination. What I actually wanted was to find out what all those years of letting the runtime decide had stopped me noticing.&lt;/p&gt;
&lt;p&gt;The engine itself is just the test rig. It&amp;rsquo;s a 2D thing with &lt;a href="https://www.libsdl.org/" class="external-link" target="_blank" rel="noopener"&gt;SDL3&lt;/a&gt; for the window and input, a software renderer and a shadow-casting mechanic I&amp;rsquo;m still playing around with. It is currently unfinished and may well stay that way as it&amp;rsquo;s just a prototype.&lt;/p&gt;
&lt;h2 id="why-not-go"&gt;
Why not Go
&lt;a class="heading-link" href="#why-not-go"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Go is probably my favourite language at the moment and the obvious place to start from, so it&amp;rsquo;s the one worth measuring against. I like it a lot, reach for it constantly, and I&amp;rsquo;d defend it in most of the contexts I use it. This isn&amp;rsquo;t a breakup.&lt;/p&gt;
&lt;p&gt;The whole exercise was about owning something Go deliberately withholds: control over when and where memory moves. Go&amp;rsquo;s garbage collector is a feature almost everywhere else, a decision the language makes so I don&amp;rsquo;t have to, and for most use cases I am glad to let it. In a frame budget of 16 milliseconds I wanted to make that decision myself for once. This is a preference, not a horror story. I didn&amp;rsquo;t profile a stuttering collector and storm off. You can actually configure and customise the Go GC pretty extensively&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; but I just wanted the allocation to be mine, and Go is designed so that it mostly isn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a smaller thing too, the kind of papercut you only notice writing tight code all day. Odin has a ternary and Go doesn&amp;rsquo;t. Switching level on a door is one line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-odin" data-lang="odin"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;state.level&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;state.level&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;==&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;.Hub&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#f85149"&gt;?&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;.World&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;.Hub&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Go has no ternary at all, so the same thing becomes a multi-line &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;else&lt;/code&gt; assigning to a variable, every time, forever. It&amp;rsquo;s a tiny ergonomic win and I&amp;rsquo;m not going to pretend it&amp;rsquo;s the reason I chose the language. It makes me wince every time I write the Go version, though. Being able to stop fighting the grammar for something this simple is just pleasant.&lt;/p&gt;
&lt;p&gt;The honest counterweight: if I were starting a project with junior engineers on the team, I&amp;rsquo;d pick Go without hesitating. Its refusal to let you manage memory is exactly what you want when you can&amp;rsquo;t assume everyone has internalised the consequences of getting it wrong. The things that make Go a slightly blunt instrument for a game loop are the same things that make it a safe one for a team.&lt;/p&gt;
&lt;h2 id="why-not-cc"&gt;
Why not C/C++
&lt;a class="heading-link" href="#why-not-cc"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Game code closer to the metal usually means C++. Decades of accumulated features and compile times that climb for little benefit have made it entirely unappealing to me.&lt;/p&gt;
&lt;p&gt;A better option is the OG C. I like C in the way you like an old tool that has clearly earned its place. Philosophically it&amp;rsquo;s hard to argue with; the C spec is still the gold standard of language specs. I&amp;rsquo;ve written a modest amount of C in my life, only ever hobby stuff, mainly homebrew Game Boy games with GBDK, but writing it in 2026 feels archaic.&lt;/p&gt;
&lt;p&gt;Header files that make you declare everything twice. Strings that are a pointer and a prayer, null-terminated and happy to walk off the end of their buffer. No &lt;code&gt;defer&lt;/code&gt;, so cleanup is a manual discipline you get wrong under pressure. A preprocessor doing text substitution underneath the language rather than anything the compiler understands. I know there is &amp;ldquo;modern C&amp;rdquo; that makes it more usable now, but the foundations haven&amp;rsquo;t moved.&lt;/p&gt;
&lt;p&gt;Odin keeps the part of C I actually wanted (direct memory, no runtime between me and the machine) and drops the 1970s ergonomics. Proper slices and strings that know their own length. &lt;code&gt;defer&lt;/code&gt; built in. No separate declaration ritual. No single feature made me sit up. It was the accumulated relief of a language that does the obvious thing without ceremony.&lt;/p&gt;
&lt;h2 id="owning-the-memory"&gt;
Owning the memory
&lt;a class="heading-link" href="#owning-the-memory"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The thing managed languages had quietly taken from me was the habit of thinking about memory at all. The first lesson of doing it by hand was how little of it you actually need to do if you plan the lifetimes up front. Everything in the engine comes back to one decision made at startup: the platform layer reserves two large blocks of memory and hands them to the game, and nothing else allocates from the operating system during normal play.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-odin" data-lang="odin"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;permanent,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;permanent_err&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;virtual.reserve_and_commit(64&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;1024&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;1024)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;transient,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;transient_err&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;virtual.reserve_and_commit(64&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;1024&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;1024)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;memory&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;game.Memory&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;permanent&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;permanent,&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;transient&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;transient,&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;read_entire_file&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;platform_read_entire_file,&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure class="inline-svg" role="img" aria-label="Diagram of the engine&amp;#39;s memory layout: a 64MB permanent block with the State struct at its top and a bump arena filling the rest, a separate 64MB transient block cleared every frame, and a game loop that allocates nothing from the OS" style="max-width: min(100%, 640px); margin: 0 auto;"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 460" width="640" height="460" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" role="img" aria-labelledby="svgtitle svgdesc"&gt;
&lt;title id="svgtitle"&gt;Odin game engine memory layout&lt;/title&gt;
&lt;desc id="svgdesc"&gt;At startup the platform reserves two 64MB blocks. The permanent block holds the State struct at its top with a bump arena filling the rest. The transient block is cleared every frame. The game loop allocates nothing from the operating system.&lt;/desc&gt;
&lt;style&gt;
.odin-mem-panel { fill: #ffffff; }
.odin-mem-ink { fill: #1b1f24; }
.odin-mem-sub { fill: #57606a; font-size: 12px; }
.odin-mem-accent { fill: #0077cc; }
.odin-mem-box { fill: none; stroke: #1b1f24; stroke-width: 1.5; }
.odin-mem-box-accent { fill: none; stroke: #0077cc; stroke-width: 1.5; }
.odin-mem-faint { fill: #e6f0f8; }
.odin-mem-divider { stroke: #1b1f24; stroke-width: 1; stroke-dasharray: 4 4; opacity: 0.4; }
.odin-mem-arrow { stroke: #0077cc; stroke-width: 1.5; }
.odin-mem-label { font-size: 15px; }
.odin-mem-tag { font-size: 12px; font-weight: 600; }
.odin-mem-title { font-size: 16px; font-weight: 600; }
/* Dark mode: toggled via body class or OS preference. Light ink on a dark panel. */
body.colorscheme-dark .odin-mem-panel { fill: #1c2128; }
body.colorscheme-dark .odin-mem-ink { fill: #dadada; }
body.colorscheme-dark .odin-mem-sub { fill: #9aa4af; }
body.colorscheme-dark .odin-mem-box { stroke: #dadada; }
body.colorscheme-dark .odin-mem-divider { stroke: #dadada; }
body.colorscheme-dark .odin-mem-faint { fill: #16324a; }
body.colorscheme-dark .odin-mem-accent,
body.colorscheme-dark .odin-mem-box-accent { stroke: #4aa3e0; }
body.colorscheme-dark .odin-mem-accent { fill: #4aa3e0; }
body.colorscheme-dark .odin-mem-arrow { stroke: #4aa3e0; }
@media (prefers-color-scheme: dark) {
body.colorscheme-auto .odin-mem-panel { fill: #1c2128; }
body.colorscheme-auto .odin-mem-ink { fill: #dadada; }
body.colorscheme-auto .odin-mem-sub { fill: #9aa4af; }
body.colorscheme-auto .odin-mem-box { stroke: #dadada; }
body.colorscheme-auto .odin-mem-divider { stroke: #dadada; }
body.colorscheme-auto .odin-mem-faint { fill: #16324a; }
body.colorscheme-auto .odin-mem-accent,
body.colorscheme-auto .odin-mem-box-accent { stroke: #4aa3e0; }
body.colorscheme-auto .odin-mem-accent { fill: #4aa3e0; }
body.colorscheme-auto .odin-mem-arrow { stroke: #4aa3e0; }
}
&lt;/style&gt;
&lt;!-- Background panel --&gt;
&lt;rect class="odin-mem-panel" x="0" y="0" width="640" height="460" rx="6"/&gt;
&lt;!-- Startup caption --&gt;
&lt;text x="20" y="34" class="odin-mem-sub"&gt;At startup: platform reserves two blocks, nothing else touches the OS during play.&lt;/text&gt;
&lt;!-- Permanent block --&gt;
&lt;text x="20" y="70" class="odin-mem-title odin-mem-ink"&gt;permanent — 64 MB&lt;/text&gt;
&lt;rect x="20" y="80" width="280" height="300" rx="4" class="odin-mem-box"/&gt;
&lt;!-- State struct at top of permanent --&gt;
&lt;rect x="20" y="80" width="280" height="70" rx="4" class="odin-mem-faint"/&gt;
&lt;rect x="20" y="80" width="280" height="70" rx="4" class="odin-mem-box-accent"/&gt;
&lt;text x="160" y="110" text-anchor="middle" class="odin-mem-tag odin-mem-accent"&gt;State struct&lt;/text&gt;
&lt;text x="160" y="130" text-anchor="middle" class="odin-mem-sub"&gt;reinterpreted from raw bytes on frame 1&lt;/text&gt;
&lt;text x="160" y="145" text-anchor="middle" class="odin-mem-sub"&gt;cast(^State)raw_data(permanent)&lt;/text&gt;
&lt;!-- divider --&gt;
&lt;line x1="20" y1="150" x2="300" y2="150" class="odin-mem-divider"/&gt;
&lt;!-- Bump arena --&gt;
&lt;text x="160" y="188" text-anchor="middle" class="odin-mem-label odin-mem-ink"&gt;bump arena&lt;/text&gt;
&lt;text x="160" y="208" text-anchor="middle" class="odin-mem-sub"&gt;everything loaded once and kept&lt;/text&gt;
&lt;!-- arena cursor arrow --&gt;
&lt;line x1="60" y1="228" x2="60" y2="358" class="odin-mem-arrow"/&gt;
&lt;polygon points="55,352 65,352 60,364" class="odin-mem-accent"/&gt;
&lt;text x="76" y="298" class="odin-mem-sub odin-mem-accent"&gt;used grows down&lt;/text&gt;
&lt;text x="76" y="316" class="odin-mem-sub"&gt;free single alloc = no-op&lt;/text&gt;
&lt;text x="76" y="334" class="odin-mem-sub"&gt;reset frees everything at once&lt;/text&gt;
&lt;!-- Transient block --&gt;
&lt;text x="340" y="70" class="odin-mem-title odin-mem-ink"&gt;transient — 64 MB&lt;/text&gt;
&lt;rect x="340" y="80" width="280" height="300" rx="4" class="odin-mem-box"/&gt;
&lt;text x="480" y="208" text-anchor="middle" class="odin-mem-label odin-mem-ink"&gt;scratch space&lt;/text&gt;
&lt;text x="480" y="230" text-anchor="middle" class="odin-mem-sub"&gt;cleared every frame&lt;/text&gt;
&lt;text x="480" y="248" text-anchor="middle" class="odin-mem-sub"&gt;nothing here outlives the frame&lt;/text&gt;
&lt;!-- Game loop caption --&gt;
&lt;text x="320" y="420" text-anchor="middle" class="odin-mem-label odin-mem-ink"&gt;The game loop allocates nothing from the OS. It mutates State in place and&lt;/text&gt;
&lt;text x="320" y="440" text-anchor="middle" class="odin-mem-label odin-mem-ink"&gt;writes pixels into a buffer created once.&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;
&lt;p&gt;Sixty-four megabytes each. The game&amp;rsquo;s entire persistent state is a single struct living at the top of the permanent block, and on the first frame the game takes that memory and reinterprets it as its &lt;code&gt;State&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-odin" data-lang="odin"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;state&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;cast&lt;/span&gt;(&lt;span style="color:#ff7b72;font-weight:bold"&gt;^&lt;/span&gt;State)raw_data(memory.permanent)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No allocation. The bytes already exist. The game just decides what they mean. That one line did more to break my managed-language instincts than anything else in the project. In Go I&amp;rsquo;d reach for a constructor and let the runtime find room for the object. Here the room is already there, I&amp;rsquo;m simply pointing a type at it, and the distinction between &amp;ldquo;make me a thing&amp;rdquo; and &amp;ldquo;treat these bytes as a thing&amp;rdquo; is suddenly load-bearing.&lt;/p&gt;
&lt;p&gt;Whatever&amp;rsquo;s left of the permanent block becomes a bump allocator for everything loaded once and kept. The allocator is about thirty lines, and the part that matters is tiny:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-odin" data-lang="odin"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;case&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;.Alloc,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;.Alloc_Non_Zeroed&lt;span style="color:#ff7b72;font-weight:bold"&gt;:&lt;/span&gt;&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aligned&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;(arena.used&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;alignment&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;1)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#f85149"&gt;~&lt;/span&gt;(alignment&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;-&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;1)&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aligned&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;size&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;gt;&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;len&lt;/span&gt;(arena.data)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#79c0ff"&gt;nil&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;.Out_Of_Memory&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;result&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;arena.data[aligned&lt;span style="color:#ff7b72;font-weight:bold"&gt;:&lt;/span&gt;aligned&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;size]&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;arena.used&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;aligned&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;+&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;size&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Move a cursor forward, hand back the slice. Freeing a single allocation does nothing on purpose; the only way to reclaim space is to reset the whole arena at once. That sounds reckless until you notice it maps exactly onto how a game actually uses memory. Assets loaded for a level live as long as the level. When you&amp;rsquo;re done, you don&amp;rsquo;t free them one by one, you throw the whole region away and start again. The allocation strategy and the lifetime of the data are the same shape.&lt;/p&gt;
&lt;p&gt;The game loop allocates nothing. Every frame reads input, mutates the &lt;code&gt;State&lt;/code&gt; blob in place, and writes pixels into a buffer that was created once. A garbage-collected language doesn&amp;rsquo;t stop you from working this way, but it spends a lot of effort inviting you not to. Odin assumes you&amp;rsquo;d rather know.&lt;/p&gt;
&lt;h2 id="data-oriented-design-honestly"&gt;
Data-oriented design, honestly
&lt;a class="heading-link" href="#data-oriented-design-honestly"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The ideas I&amp;rsquo;d been sitting on came from &lt;a href="https://www.amazon.co.uk/Data-oriented-design-engineering-resources-schedules/dp/1916478700/" class="external-link" target="_blank" rel="noopener"&gt;Richard Fabian&amp;rsquo;s book on data-oriented design&lt;/a&gt;. There&amp;rsquo;s a &lt;a href="https://www.dataorienteddesign.com/dodbook/" class="external-link" target="_blank" rel="noopener"&gt;free abridged version online&lt;/a&gt;. I read it a few years back, nodded along, and then went back to writing the same object-heavy code I always had, because nothing in my day job rewarded doing otherwise. The core idea is that you design around how data is laid out and accessed, because the cache is the thing that actually determines whether your program is fast, not how elegant your object model looks.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll be straight about how far I took it. Odin has first-class structure-of-arrays support, the &lt;a href="https://odin-lang.org/docs/overview/#soa-data-types" class="external-link" target="_blank" rel="noopener"&gt;&lt;code&gt;#soa&lt;/code&gt;&lt;/a&gt; keyword that transposes an array of structs into a struct of arrays for better cache behaviour.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; I didn&amp;rsquo;t use it. I wasn&amp;rsquo;t actually aware of this language feature at the time but I don&amp;rsquo;t think the engine is pushing enough entities for that transformation to earn its place yet.&lt;/p&gt;
&lt;p&gt;What I did take is the mindset, and it shows up in the layout. The shadow system is a fixed array sized to the map, not a dynamic list that grows and shrinks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-odin" data-lang="odin"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;shadows&lt;span style="color:#ff7b72;font-weight:bold"&gt;:&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;[MAP_ROWS&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;MAP_COLS]Convex,&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;shadow_polys&lt;span style="color:#ff7b72;font-weight:bold"&gt;:&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;[MAP_ROWS&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;MAP_COLS]Shadow_Poly,&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;shadow_count&lt;span style="color:#ff7b72;font-weight:bold"&gt;:&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;int&lt;/span&gt;,&lt;span style="color:#6e7681"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are 32 by 8 tiles, so the worst case is 256 shadow-casting shapes, so I reserve 256 and track how many are live with a count. No allocation when the light moves, no resizing, no pointer-chasing through a linked structure. The collision code works the same way, projecting polygon points onto candidate axes in tight loops over contiguous arrays. The data sits where the next access expects it to be.&lt;/p&gt;
&lt;p&gt;The habit the book was trying to teach me, and that only stuck once I actually built something, is to think about a piece of data&amp;rsquo;s shape and lifetime before I think about its behaviour. Odin makes that the path of least resistance. Go actively hides it from you, which is usually a kindness and occasionally the exact thing you&amp;rsquo;re trying to escape.&lt;/p&gt;
&lt;h2 id="hot-reload-falls-out-of-the-architecture"&gt;
Hot reload falls out of the architecture
&lt;a class="heading-link" href="#hot-reload-falls-out-of-the-architecture"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Owning the memory bought me something I didn&amp;rsquo;t plan for. The game logic compiles to a shared library and the main loop watches it for changes. Rebuild while the program is running and it loads the new code mid-frame, with the player still walking around, their position intact. I can change how jumping feels, rebuild and watch the character respond differently mid-leap.&lt;/p&gt;
&lt;p&gt;State survives the swap because state was never in the library. The code lives in the shared object; the data lives in those two memory blocks the platform owns, and they&amp;rsquo;re handed straight back into the fresh code on the next frame. This is the &lt;a href="https://handmadehero.org/" class="external-link" target="_blank" rel="noopener"&gt;Handmade Hero&lt;/a&gt; architecture and I invented none of it.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt; The point worth making is that it falls out of the memory decision for free. Once the data lives outside the code, hot reload stops being a trick you bolt on and becomes a thing that simply works. In a managed language the state would be tangled up in objects the runtime owns, and this would be far harder to pull off. Doing the memory myself made it almost incidental.&lt;/p&gt;
&lt;h2 id="what-annoys-me"&gt;
What annoys me
&lt;a class="heading-link" href="#what-annoys-me"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;A post that&amp;rsquo;s all praise is just a sales pitch, so here&amp;rsquo;s the other side, kept proportional to how much it actually bothers me, which is not much.&lt;/p&gt;
&lt;p&gt;Odin dropped the increment and decrement operators.&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt; No &lt;code&gt;i++&lt;/code&gt;. You write &lt;code&gt;i += 1&lt;/code&gt;, which is three extra keystrokes and a flicker of irritation every time my fingers reach for the shorter form out of twenty years of habit. That&amp;rsquo;s the complaint. It&amp;rsquo;s a small language with a small standard library and a small community, and when something isn&amp;rsquo;t in the vendor collection you&amp;rsquo;re writing it yourself or binding to C.&lt;/p&gt;
&lt;p&gt;The one corner where that thinness actually slowed me down was &lt;a href="https://pkg.odin-lang.org/core/testing/" class="external-link" target="_blank" rel="noopener"&gt;&lt;code&gt;core:testing&lt;/code&gt;&lt;/a&gt;. I lean on tests, so I noticed its edges quickly. The matchers are minimal, really just &lt;code&gt;expect&lt;/code&gt; and a couple of variants, so you hand-write the failure message every time rather than getting a &amp;ldquo;wanted X, got Y&amp;rdquo; for free. There&amp;rsquo;s no setup or teardown hook, so shared fixtures get rebuilt by hand in each test. No built-in coverage report. The runner is bare. There&amp;rsquo;s no mocking or spy machinery either, which is a deliberate choice I mostly agree with; a language without pervasive runtime reflection is a language where that machinery can&amp;rsquo;t quietly take over your test suite. I say mostly, because I don&amp;rsquo;t think reflection is the absolute devil, and once in a while I did miss being able to stub a dependency without restructuring the code to allow it. None of this stopped me writing the tests. It just meant doing a bit more of the scaffolding myself, which, given everything else about the language, I should probably have expected.&lt;/p&gt;
&lt;p&gt;I should be clear about the limits of my opinion and experience here. So far I have only used Odin for hobby projects. I have no idea what it&amp;rsquo;s like to hire for, to onboard a team onto, or to run in production under real load, and I&amp;rsquo;m not going to pretend the view from a handful of weekend projects tells me any of that. What I can say is that for one person building a game engine for the pleasure of it, the tooling has been good enough, which is quite a high compliment.&lt;/p&gt;
&lt;h2 id="where-this-leaves-me"&gt;
Where this leaves me
&lt;a class="heading-link" href="#where-this-leaves-me"&gt;
&lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"&gt;&lt;/i&gt;
&lt;span class="sr-only"&gt;Link to heading&lt;/span&gt;
&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Odin hasn&amp;rsquo;t replaced Go for me and I never wanted it to. The two solve different problems and I&amp;rsquo;ll keep using both. What the experiment actually gave me wasn&amp;rsquo;t a new favourite language, it was the memory back. After a career of letting the runtime decide where my bytes live, spending a few months deciding for myself has changed how I read every program, managed or not. Odin was the right vehicle for that: close enough to the metal to force the lessons, modern enough not to make me suffer for them. The nearest alternative is still behind a closed beta, so we&amp;rsquo;ll see if this holds once the long-awaited Jai reaches public release.&lt;/p&gt;
&lt;p&gt;The engine will likely never ship a finished game, which was never the goal. I&amp;rsquo;ve learned more about how computers actually spend their time from building it than from any amount of backend work, which tells you something about what you give up when the runtime handles everything for you.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;The &lt;a href="https://tip.golang.org/doc/gc-guide" class="external-link" target="_blank" rel="noopener"&gt;Go GC guide&lt;/a&gt; covers the knobs. &lt;code&gt;GOGC&lt;/code&gt; sets the target heap growth between collections (default 100, meaning the heap can double before a cycle), &lt;code&gt;GOGC=off&lt;/code&gt; disables the collector entirely, and &lt;code&gt;GOMEMLIMIT&lt;/code&gt; (Go 1.19+) sets a soft memory ceiling the runtime tries to stay under. You can change the percentage at runtime with &lt;code&gt;runtime/debug.SetGCPercent&lt;/code&gt;. Plenty of control, just not control over individual allocations.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;Odin stores &lt;code&gt;#soa&lt;/code&gt; arrays as a struct of parallel arrays internally while letting you index them as if they were an ordinary array of structs. See the &lt;a href="https://odin-lang.org/docs/overview/#soa-data-types" class="external-link" target="_blank" rel="noopener"&gt;&lt;code&gt;#soa&lt;/code&gt; section&lt;/a&gt; of the language overview, and the &lt;a href="https://forum.odin-lang.org/t/how-soa-works-under-the-hood/383" class="external-link" target="_blank" rel="noopener"&gt;forum thread&lt;/a&gt; for the tradeoffs it makes under the hood.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://handmadehero.org/" class="external-link" target="_blank" rel="noopener"&gt;Handmade Hero&lt;/a&gt; is Casey Muratori&amp;rsquo;s series building a complete game from scratch in C with no libraries. The platform-owns-the-memory split and the hot-reloadable game DLL both come from his early episodes.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;The reasoning is in the &lt;a href="https://odin-lang.org/docs/faq/#why-are-there-no--or----operators" class="external-link" target="_blank" rel="noopener"&gt;Odin FAQ&lt;/a&gt;: &lt;code&gt;++&lt;/code&gt; and &lt;code&gt;--&lt;/code&gt; look simple but depend on evaluation order, which can lead to bugs. &lt;code&gt;i += 1&lt;/code&gt; is slightly longer but unambiguous. I agree with the principle.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item></channel></rss>