<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>A816 on Man-You</title><link>https://man-you.ringum.net/tags/a816/</link><description>Recent content in A816 on Man-You</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 04 May 2026 00:08:13 +0200</lastBuildDate><atom:link href="https://man-you.ringum.net/tags/a816/index.xml" rel="self" type="application/rss+xml"/><item><title>25 years to a complete machine</title><link>https://man-you.ringum.net/posts/25-year-quest/</link><pubDate>Mon, 04 May 2026 00:08:13 +0200</pubDate><guid>https://man-you.ringum.net/posts/25-year-quest/</guid><description>&lt;p&gt;Since around the year 2000 I have been piling features into a French Final Fantasy IV translation patch. Variable-width fonts in dialog, in battle messages, in menu descriptions. Redrawn battle, load/save, main, and two-column magic menus. An expanded inventory of &lt;code&gt;0x48&lt;/code&gt; slots rendered through a paged window instead of the static grid the original engine assumed. Most of these were features the SNES ROM hacking scene of the era said could not be done on real hardware. Other hackers eventually pulled off equivalents on other titles, fair enough; modifying a compiled ROM is more convoluted than editing source code, but not impossible. Twenty-five years on, &amp;ldquo;impossible&amp;rdquo; reads as a fair approximation of how hard, not of whether.&lt;/p&gt;
&lt;figure&gt;&lt;img src="https://man-you.ringum.net/posts/25-year-quest/ff4-battle-french.png"
alt="Battle screen with French enemy names" width="400"&gt;
&lt;/figure&gt;
&lt;p&gt;The bugs that come with hard features are also recent, introduced by my own patches. The field-menu item list eventually crashed: open inventory, swap items around, and after enough operations the stack pointer drifted until the next interrupt landed on a &lt;code&gt;brk&lt;/code&gt;&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; instruction and the emulator stopped cold.&lt;/p&gt;
&lt;p&gt;The cause was one wrong return mnemonic&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;. &lt;code&gt;swap_redraw_trampoline&lt;/code&gt; lives in bank &lt;code&gt;$01&lt;/code&gt;&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;. Its real body, &lt;code&gt;swap_redraw_hook_impl&lt;/code&gt;, lives in bank &lt;code&gt;$21&lt;/code&gt;. The field menu calls the trampoline with &lt;code&gt;jsr.l&lt;/code&gt;&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;, which pushes a 24-bit return. The bank-$21 body returns via &lt;code&gt;rtl&lt;/code&gt;, which is correct. The trampoline itself ended in &lt;code&gt;rts&lt;/code&gt;, which is not. One byte of the long-return address stayed on the stack every time an item was swapped. Run the swap path enough times and the underflow cascaded, the next NMI&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt; hit garbage, and the BRK trap I had wired into the vector table&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt; caught the offending PC.&lt;/p&gt;
&lt;p&gt;The fix is one mnemonic. Getting to the point where I could see the wrong mnemonic took infrastructure.&lt;/p&gt;
&lt;h2 id="why-a-rolling-inventory-at-all"&gt;Why a rolling inventory at all&lt;/h2&gt;
&lt;p&gt;The rolling inventory was not strictly necessary. Two columns of items were getting tight on screen as the inventory grew, and I knew FF6 had solved a similar layout problem more elegantly with a paged renderer. FF4 and FF6 share engine roots, which made porting the technique tractable instead of ground-up work. So I lifted the approach.&lt;/p&gt;
&lt;p&gt;The memory side effect is real but secondary. The rolling render only keeps the visible slice in RAM and VRAM&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;, paging new ones in as the cursor scrolls. &lt;code&gt;0x48&lt;/code&gt; slots at roughly &lt;code&gt;0x40&lt;/code&gt; bytes of tile data each would have cost &lt;code&gt;0x48 * 0x40 = 0x1200&lt;/code&gt; bytes per buffer in a static layout. The rolling layout drops that to one window&amp;rsquo;s worth. If saving memory had been the only goal there were cheaper fixes than this one.&lt;/p&gt;
&lt;p&gt;The bug above was the price of pulling FF6 ergonomics into FF4: the trampoline that moves tile data between RAM and VRAM forgot to return with the right mnemonic, leaked one byte per swap, and the stack caught up with the player when they were having too much fun rearranging.&lt;/p&gt;
&lt;h2 id="what-made-the-bug-reachable"&gt;What made the bug reachable&lt;/h2&gt;
&lt;p&gt;For most of the 25 years I have been doing this, I would not have caught it.&lt;/p&gt;
&lt;p&gt;The original ff4 source, like every long-running ROM hack, was a single textual &lt;code&gt;.include&lt;/code&gt; chain compiled in one pass. Every label was global. There were no module boundaries. There was no way to ask the assembler &amp;ldquo;where does this symbol come from&amp;rdquo; except by grepping the file system. The trampoline-with-the-wrong-rts looked exactly like the trampoline-with-the-right-rtl two screens up. Lost in the noise.&lt;/p&gt;
&lt;p&gt;The ff4 source recently finished its migration to a816&amp;rsquo;s &lt;code&gt;.import&lt;/code&gt; system. Each module declares its own externs, compiles to its own &lt;code&gt;.o&lt;/code&gt;, links through relocations. The LSP earns its keep on top of that. When you suspect &lt;code&gt;swap_redraw_trampoline&lt;/code&gt; ends in &lt;code&gt;rts&lt;/code&gt; where it should end in &lt;code&gt;rtl&lt;/code&gt;, you do not grep for &amp;ldquo;swap_redraw&amp;rdquo; across hundreds of files. You click. The symbol resolves through the import graph, the trampoline opens in the right module, and the wrong mnemonic is on the screen.&lt;/p&gt;
&lt;p&gt;Without modules the bug was unreachable. Without the BRK trap in the vector table the symptom was &amp;ldquo;crashes sometimes&amp;rdquo; instead of a PC value. The rest of the chain (LSP for navigation, kintsuki for deterministic reproduction) only mattered once those two existed.&lt;/p&gt;
&lt;h2 id="why-these-tools-exist"&gt;Why these tools exist&lt;/h2&gt;
&lt;p&gt;I have been hacking on Final Fantasy IV, on and off, since around the year 2000. The earliest public trace is the &lt;a href="https://github.com/manz/ff4fr"&gt;ff4fr repo&lt;/a&gt; from 2011, itself a snapshot of older work. The reason the project keeps surviving long gaps is simple: it interests me. I come back, I poke at it for a few weekends, I drift, I come back. A hobby that happened to last.&lt;/p&gt;
&lt;p&gt;None of the tools were built as long-game preparation for the rolling inventory bug.&lt;/p&gt;
&lt;p&gt;a816 replaced x816.exe, the DOS assembler the FF4 scene had used for years. x816 is painful on Windows, dead on Linux and macOS. Building the ROM on GitHub Actions through dosbox was not going to happen. asar exists and is a fine alternative (and may not have been around when I started; I do not remember the timeline clearly). I went my own way because I wanted tight integration with the ff4 source: modules I controlled, a formatter I could shape, an LSP that understood bank annotations and docstring conventions, an object format that handled multi-region modules cleanly. Easier to build that end-to-end than to lobby someone else&amp;rsquo;s project for ergonomics specific to one ROM hack. The GitHub Actions side became its own win. Messing with workflows on a personal repo translated directly into knowledge I now apply at work.&lt;/p&gt;
&lt;p&gt;kintsuki exists because I like automated tests. Running the ROM deterministically against pinned inputs needs primitives ares does not expose by default: mid-frame yield, PC override across scheduler entries, &lt;code&gt;stp&lt;/code&gt; and &lt;code&gt;wai&lt;/code&gt; flag preservation through external state-set. kintsuki adds the shim around the ares core and ships it as a Python wheel.&lt;/p&gt;
&lt;p&gt;The honest reason underneath both: knowing enough to be dangerous has been the constant of my career. At fifteen, in 2000, I was dangerous with a hex editor and someone else&amp;rsquo;s assembler. That was already enough to start a translation patch. The version writing now is dangerous with a homemade assembler, an LSP, an emulator harness wrapped as a Python wheel, and a multi-region object format. The disposition has not moved. The capability has. Every year of doing software for a living added new ways of being dangerous, and the FF4 patch is the one personal project that stuck around long enough to absorb all of them. The tools and the patch grew up together. The features the fifteen-year-old wanted are now sitting on infrastructure he could not have built, but would absolutely have used.&lt;/p&gt;
&lt;p&gt;The thing actually pacing the work right now is a self-imposed release date: December 26, 2026, for a packaged build of the patch. That deadline turned the loose interest into a backlog. The deadline is recent. The hobby is old.&lt;/p&gt;
&lt;p&gt;Seven and a bit months until the release date. The patch will ship with bugs that came after it on top of an assembler and a harness that came before. The ROM hack will still have open issues, untranslated NPCs, and design decisions I will second-guess. None of that bothers me. It is the project I keep coming back to because it keeps being interesting, and right now the interesting thing is finishing it.&lt;/p&gt;
&lt;p&gt;Not everything I write is meant to stick it up to the man. This one is not. Hobby code, on a console nobody ships for, against a deadline I made up because I felt like one. No thesis. A release on December 26.&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;&lt;code&gt;brk&lt;/code&gt; is a one-byte instruction that triggers a software interrupt. The CPU pushes its state and jumps to a fixed handler address. Useful as a trap when that handler is wired to halt the emulator on an unexpected fall-through.&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;A mnemonic is the short text name for a CPU instruction (&lt;code&gt;rts&lt;/code&gt;, &lt;code&gt;jsr&lt;/code&gt;, &lt;code&gt;lda&lt;/code&gt;). The assembler turns each one into the matching machine-code byte.&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;The 65C816 addresses 24-bit memory split into 256 banks of 64KB. &lt;code&gt;$01:FF34&lt;/code&gt; reads &amp;ldquo;address &lt;code&gt;$FF34&lt;/code&gt; in bank &lt;code&gt;$01&lt;/code&gt;&amp;rdquo;. Code in different banks is reached with long-call instructions instead of short ones.&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;&lt;code&gt;jsr.l&lt;/code&gt; is a long jump to subroutine: it pushes a 24-bit return address (bank + 16-bit) before jumping. The matching return is &lt;code&gt;rtl&lt;/code&gt;. The short pair is &lt;code&gt;jsr&lt;/code&gt; + &lt;code&gt;rts&lt;/code&gt; (16-bit return). Mismatching them leaks or eats bytes off the stack.&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;li id="fn:5"&gt;
&lt;p&gt;Non-maskable interrupt. On the SNES it fires once per video frame, in the brief moment when the screen is between drawing two frames, and runs whatever handler the CPU is told to run for it.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;The vector table is a fixed block at &lt;code&gt;$00FFE0&lt;/code&gt;-&lt;code&gt;$00FFFF&lt;/code&gt; listing handler addresses for reset, NMI, BRK, and others. Wiring a BRK trap means setting the BRK handler to halt the emulator and record where the bad instruction came from, so an unexpected &lt;code&gt;brk&lt;/code&gt; produces a debuggable crash instead of silent corruption.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;The SNES CPU&amp;rsquo;s main RAM is 128KB. VRAM is the graphics chip&amp;rsquo;s video RAM (64KB), holding the tile data and the screen layout. Anything visible has to be in VRAM. Tile data is usually staged in main RAM and copied across in bulk between two drawn frames.&amp;#160;&lt;a href="#fnref:7" 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>