<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>thomblog</title><link href="https://thomaspurnell.com/" rel="alternate"/><link href="https://thomaspurnell.com/atom.xml" rel="self"/><id>https://thomaspurnell.com/</id><updated>2026-05-04T00:00:00+10:00</updated><subtitle>Tom 'voxel' Purnell's notes</subtitle><entry><title>Using nginx as a forward proxy for gemini</title><link href="https://thomaspurnell.com/020_AmiGemini_nginx_forward_proxy/gemini_nginx_forward_proxy.html" rel="alternate"/><published>2026-05-04T00:00:00+10:00</published><updated>2026-05-04T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2026-05-04:/020_AmiGemini_nginx_forward_proxy/gemini_nginx_forward_proxy.html</id><summary type="html">Using nginx as a forward proxy for gemini</summary><content type="html">&lt;p&gt;&lt;img alt="AmiGemini browsing a gemini capsule" src="amigemini.png"&gt;&lt;/p&gt;
&lt;h2&gt;Accessing gemini without SSL support&lt;/h2&gt;
&lt;p&gt;The gemini &amp;lsquo;smallweb&amp;rsquo; protocol relies on SSL support for security, encryption, etc. The content of these gemini capsules is almost entirely text-only, ideal for browsing on limited or old hardware, such as my Amiga. Although SSL support is possible on more powerful Amigas, mine can&amp;rsquo;t actually load the SSL library &lt;em&gt;and&lt;/em&gt; use a network connection simultaneously, so I wanted a solution to get gemini capsules displaying without SSL. My solution is to use nginx as a forward proxy. The Amiga (or other device) sends an unencrypted request to the proxy, which then wraps it in a warm security blanket and posts it out onto the internet, and then unpackages the secured return traffic for the Amiga too.&lt;/p&gt;
&lt;h2&gt;Stream&lt;/h2&gt;
&lt;p&gt;nginx doesn&amp;rsquo;t really &amp;lsquo;support&amp;rsquo; gemini, but it has a generic &amp;lsquo;stream&amp;rsquo; module that can handle weird traffic. My local &lt;code&gt;nginx.conf&lt;/code&gt; now includes this&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# DNS resolver&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;resolver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;.1.1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Import the JS module&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;js_import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;gemini&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;gemini_proxy.js&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;#gemini_host variable will contain the name of the host we want to connect to&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;js_set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$gemini_host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;gemini.get_host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Old machines connect on this port&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Use the imported javascript to sniff the hostname from the incoming request&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;js_preread&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;gemini.preread_gemini&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Proxy the request to the host on the standard gemini port&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_pass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$gemini_host:1965&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_ssl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_ssl_server_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_ssl_protocols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;TLSv1.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;proxy_ssl_verify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And the helper js file &lt;code&gt;gemini_proxy.js&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;preread_gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;upload&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// Find the Gemini URL in the raw text payload&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// Matches &amp;#39;gemini://domain.com&amp;#39; or just &amp;#39;domain.com&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/gemini:\/\/([^\/\:\r\n]+)/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^([^\/\:\r\n]+)/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Extracted Gemini Host: &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Stop prereading and move to next phase&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;get_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;preread_gemini&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;get_host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Since the stream module can&amp;rsquo;t tell where the traffic is supposed to go (it&amp;rsquo;s just a random stream after all), the javascript scans the incoming stream and tries to locate the destination url. This is used to populate the &lt;code&gt;$gemini_host&lt;/code&gt; variable in nginx, which then allows the &lt;code&gt;proxy_pass&lt;/code&gt; directive to.. proxy.&lt;/p&gt;
&lt;h2&gt;Security and safety considerations&lt;/h2&gt;
&lt;p&gt;Lol no. Don&amp;rsquo;t host open proxies unless you understand all the implications, &lt;em&gt;even&lt;/em&gt; if it&amp;rsquo;s just for gemini.&lt;/p&gt;
&lt;h2&gt;Unneccessary?&lt;/h2&gt;
&lt;p&gt;Karl Jeacle, the author of AmiGemini hosts a public proxy free for everyone to use. He even supplies a python script to run your own if you don&amp;rsquo;t want to connect to his instance. However, I already have an nginx instance running on my network and thought this would be an ideal use for it. But if you&amp;rsquo;re in the highly normal situation of running an Amiga without SSL and using it to browse Gemini capsules, and you &lt;em&gt;don&amp;rsquo;t&lt;/em&gt; have an nginx instance, his proxy is probably the best bet.&lt;/p&gt;
&lt;h2&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;I needed assistance extracting the server name from the stream, and got help from Sid (why do you not have a blog?). Then while writing up this blog post he informed me that the javascript was AI generated. Amazing. Now I&amp;rsquo;m sullied and impure. I don&amp;rsquo;t like to use AI, and now I&amp;rsquo;ve used it by proxy.. to build a proxy?&lt;/p&gt;</content></entry><entry><title>January 2026</title><link href="https://thomaspurnell.com/019_January_2026/january_2026.html" rel="alternate"/><published>2026-02-01T00:00:00+10:00</published><updated>2026-02-01T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2026-02-01:/019_January_2026/january_2026.html</id><summary type="html">January 2026</summary><content type="html">&lt;h2&gt;Untitled project&lt;/h2&gt;
&lt;iframe width="640" height="360" src="https://www.youtube.com/embed/3c18lxu63Oc" title="Devlog 000: Untitled HOTAS/HOSAS game prototype" frameborder="0" allow="encrypted-media; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;

&lt;p&gt;Started the year by producing a devlog for my ongoing untitled project. I&amp;rsquo;ve decided to reduce the focus on flying since making this devlog, I think precision flying is too inaccessible for the other planned gameplay aspects. I still want to do something specifically for my custom HOSAS (Hands On Stick and Stick) dual joystick setup .. but I&amp;rsquo;d like to release something that other people can actually play first ^_^.&lt;/p&gt;
&lt;h2&gt;Monster Summoner&lt;/h2&gt;
&lt;p&gt;Despite my promise to avoid gamejams, I made a &amp;lsquo;small game&amp;rsquo; for CGA Jam. There&amp;rsquo;s a &amp;lsquo;high resolution&amp;rsquo; graphics mode &amp;lsquo;0x06&amp;rsquo; supported by the CGA hardware, 640x200 pixels. It&amp;rsquo;s only 1 bit, so you&amp;rsquo;re limited to a fixed black background and one foreground colour. The resolution makes drawing a challenge - the pixels are 2.4 times taller than they are wide. Just trying to get aseprite to display anything in this pixel ratio is a challenge, and all kinds of strange errors occurred with visual garbage and glitches being left when I adjusted zoom levels, or even pixels appearing in the wrong place when drawing. So I mostly stuck to the reasonably functional &amp;lsquo;double height pixel&amp;rsquo; mode, which isn&amp;rsquo;t quite as stretched but comes close. Cloanto Personal Paint on the Amiga actually supports a 640:240 pixel mode that comes close, but the mouse I have for the Amiga isn&amp;rsquo;t great, so I mostly stuck to Aseprite. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Cloanto Personal Paint on an Amiga 1200" src="amiga_ppaint.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I failed to get a working gcc cross compiler for ia16 (16 bit 8086 family CPUs), so I went with OpenWatcomV2. This worked out pretty well once I managed to find some reasonably up to date documentation. It was my first time writing 16bit C in a while, and I repeatedly made the mistake of assuming 32bit sizes for integers and getting confused when numbers over 32767 (maximum value for a signed 16bit integer) would act strangely. Drawing to the screen in mode 0x06 is fairly simple, every pixel has one corresponding bit, but the memory area is divided into two sections, one for even scanlines and another for oddscanlines. So it was interesting enough without being a challenge to wrangle like some of the more complicated planar graphics modes out there.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Monster Summoner screenshot" src="msummon_1.png"&gt;&lt;/p&gt;
&lt;p&gt;Fairly pleased with how the game turned out. If I&amp;rsquo;m honest it was entirely a vehicle to draw some 1bit pixelart, the game is just a rationalisation. &lt;a href="https://voxel.itch.io/monster-summoner"&gt;You can play the game here in your browser, but I recommend downloading it and playing in dosbox&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Amiga&lt;/h2&gt;
&lt;p&gt;When I was a child, I wrote a lottery number generator in Amiga Basic for our Amiga 500. My parents won some money with the numbers it output. I&amp;rsquo;ve a significant amount of time and a not insignificant amount of money fixing up my Amiga 1200, so I figured it was time to cash in on karma and wrote a lottery number generator in 68000 assembly, so my new Amiga can tell me the winning numbers and make me rich enough to be able to maintain the machine in the future. Also I really wanted a &amp;lsquo;small&amp;rsquo; project to get familiar with developing on the Amiga. Nowadays most people write code on their modern PCs and then &amp;lsquo;cross compile&amp;rsquo; to the Amiga, testing their code on a software emulator. This lets them use all the futuristic conveniences of the 2020s. But I wanted to do as much as possible on the Amiga itself. There&amp;rsquo;s plenty of options for developing &lt;em&gt;on&lt;/em&gt; the Amiga, but after a little research I went with the highly recommended Asm Pro.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Asm Pro IDE" src="scr_asmpro.png"&gt; &lt;/p&gt;
&lt;p&gt;Opening a window and writing text to it is weirdly easy. You just &amp;lsquo;open a file&amp;rsquo; with a filename like &amp;lsquo;RAW:0/0/280/200/Lottery&amp;rdquo;, and a window is created at the top left of the screen, 280 pixels wide and 200 tall, titled &amp;lsquo;Lottery&amp;rsquo;. Then any characters you write to the file appear in the window. Easy, though somewhere I&amp;rsquo;m occasionally outputting the wrong number of linebreaks. My Amiga has a realtime clock on an expansion board, so I read the current date from that and use it to seed my Random Number Generator. Then I read out six numbers, check there are no duplicates, and print them to the screen. The actual RNG code could really be run anywhere - it&amp;rsquo;s completely deterministic and would give the same result if you went through all of the steps manually with pen and paper - but where&amp;rsquo;s the fun in that? So far this lottery generator has generated a line with three correct numbers and won me $9.15, nearly enough to cover the cost of the tickets I&amp;rsquo;ve bought so far, haha. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Lottery number generator" src="scr_lotto.png"&gt;&lt;/p&gt;
&lt;p&gt;Obviously it could be made to look a lot nicer, but it served as a &amp;lsquo;first project&amp;rsquo;, much better than the typical &amp;lsquo;hello world&amp;rsquo;. Sometime in the future I&amp;rsquo;d like to write a game or three for the Amiga, but that&amp;rsquo;ll have to wait awhile.&lt;/p&gt;
&lt;h2&gt;Reading&lt;/h2&gt;
&lt;p&gt;Over January I read a whole bunch of the &amp;lsquo;Dungeon Crawler Carl&amp;rsquo; series. I&amp;rsquo;m conflicted about these books. Some snobby part of me wants to look down on their &amp;lsquo;game inspired&amp;rsquo; elements. Carl and his sentient talking cat Donut are forced to fight for their lives in a dungeon world filled with ridiculous pop culture references, memes and puns. They have a video-game like interface for their inventory, levelling up, boosting stats and so on. So it reads somewhere between a &amp;lsquo;gameplay walkthrough&amp;rsquo; and a typical adventure novel. There&amp;rsquo;s an underlying current of subverting the system and rebellion, hinting at the possibility of overthrowing the powers that are forcing them to participate in the story - I think this is the main motivation keeping me reading. &lt;/p&gt;
&lt;h2&gt;Music&lt;/h2&gt;
&lt;p&gt;Album of the month is &lt;a href="https://spacetak.bandcamp.com/album/pleasure-seekers"&gt;Takuya Nakamura&amp;rsquo;s &amp;lsquo;Pleasure Seekers&amp;rsquo;&lt;/a&gt;. He does a lot of jungle/garage adjacent stuff with a good amount of jazz mixed in. I love it, especially his live sets where he pulls out a trumpet and improvises over his live mix.&lt;/p&gt;</content></entry><entry><title>Setting up a 16 bit DOS development environment in Arch Linux</title><link href="https://thomaspurnell.com/018_16bit_DOS_environment/16bit_DOS_environment.html" rel="alternate"/><published>2026-01-19T00:00:00+10:00</published><updated>2026-01-19T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2026-01-19:/018_16bit_DOS_environment/16bit_DOS_environment.html</id><summary type="html">Setting up a 16 bit DOS development environment in Arch Linux</summary><content type="html">&lt;p&gt;This is a guide on one way of setting up a modern development system for 16 bit MS-DOS systems. It will support 32 bit protected mode too, but there&amp;rsquo;s many other options for that.&lt;/p&gt;
&lt;h2&gt;My environment&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m setting this environment up from a 64bit x86_64 Arch linux machine.&lt;/p&gt;
&lt;h2&gt;Open Watcom v2&lt;/h2&gt;
&lt;p&gt;Current available scripts for configuring a gcc toolchain to target 16 bit DOS wouldn&amp;rsquo;t work for me. Some seemed to have dependencies on older versions of gcc on the host. I spent an evening failing to make any of them work. &lt;/p&gt;
&lt;p&gt;Instead, I&amp;rsquo;m now using &lt;a href="https://github.com/open-watcom/open-watcom-v2"&gt;open watcom v2&lt;/a&gt;, an open source fork of the venerable watcom tools. OW2 supports a cross compiling to a range of targets, including 16 bit real-mode MSDOS.&lt;/p&gt;
&lt;h2&gt;Build process&lt;/h2&gt;
&lt;p&gt;This largely follows the &lt;a href="https://github.com/open-watcom/open-watcom-v2/wiki/Build"&gt;open watcom official guide&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Clone the OW2 repository&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/open-watcom/open-watcom-v2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Copy and modify the supplied setenv script&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;open-watcom-v2/setenv.sh&lt;span class="w"&gt; &lt;/span&gt;./
$&lt;span class="w"&gt; &lt;/span&gt;vim&lt;span class="w"&gt; &lt;/span&gt;setenv.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Modify the &lt;code&gt;OWROOT&lt;/code&gt; variable to point to the root of the local copy of the open-watcom repository, i.e:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;OWROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/voxel/Src/Extern/open-watcom-v2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Despite having dosbox installed and on the default path, the open-watcom build wouldn&amp;rsquo;t succeed unless I also set &lt;code&gt;OWNOWGML=1&lt;/code&gt; to disable using dosbox to build the documentation.&lt;/p&gt;
&lt;p&gt;Additionally I set the following:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;OWGUINOBUILD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;OWDISTRBUILD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, run the build script, and have it populate a &lt;code&gt;rel&lt;/code&gt; directory with the built assets.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;rel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This took about 20 minutes to run, and resulted in the subdirectory &lt;code&gt;rel&lt;/code&gt; being created with the compiled watcom system.&lt;/p&gt;
&lt;p&gt;To &amp;lsquo;install&amp;rsquo; watcom, I copied rel to where I keep such toolchains.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;rel&lt;span class="w"&gt; &lt;/span&gt;/opt/watcom
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, I created a setvars script to configure environment variables when I&amp;rsquo;m working with watcom.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;vim&lt;span class="w"&gt; &lt;/span&gt;/opt/watcom/setvars.sh

&lt;span class="c1"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;:/opt/watcom/binl:/opt/watcom/binl64
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;WATCOM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/watcom
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;INCLUDE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/watcom/h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Using watcom to build MSDOS compatible COM files&lt;/h2&gt;
&lt;p&gt;Create a test file &lt;code&gt;hell_world.c&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#include &amp;lt;stdio.h&amp;gt;&lt;/span&gt;

int&lt;span class="w"&gt; &lt;/span&gt;main&lt;span class="o"&gt;(){&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;printf&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;HELL WORLD\n&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, ensuring the environment variable script has been activated, compile and link a com file in one step.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;/opt/watcom/setvars.sh
$&lt;span class="w"&gt; &lt;/span&gt;wcl&lt;span class="w"&gt; &lt;/span&gt;-bcl&lt;span class="o"&gt;=&lt;/span&gt;com&lt;span class="w"&gt; &lt;/span&gt;hell_world.c
Open&lt;span class="w"&gt; &lt;/span&gt;Watcom&lt;span class="w"&gt; &lt;/span&gt;C/C++&lt;span class="w"&gt; &lt;/span&gt;x86&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;-bit&lt;span class="w"&gt; &lt;/span&gt;Compile&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;Link&lt;span class="w"&gt; &lt;/span&gt;Utility
...
&lt;span class="w"&gt;    &lt;/span&gt;wcc&lt;span class="w"&gt; &lt;/span&gt;hell_world.c&lt;span class="w"&gt;  &lt;/span&gt;-bt&lt;span class="o"&gt;=&lt;/span&gt;com
Open&lt;span class="w"&gt; &lt;/span&gt;Watcom&lt;span class="w"&gt; &lt;/span&gt;C&lt;span class="w"&gt; &lt;/span&gt;x86&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;-bit&lt;span class="w"&gt; &lt;/span&gt;Optimizing&lt;span class="w"&gt; &lt;/span&gt;Compiler
...
hell_world.c:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;lines,&lt;span class="w"&gt; &lt;/span&gt;included&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;810&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;warnings,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;errors
Code&lt;span class="w"&gt; &lt;/span&gt;size:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;19&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;wlink&lt;span class="w"&gt; &lt;/span&gt;@__wcl_00.lnk
Open&lt;span class="w"&gt; &lt;/span&gt;Watcom&lt;span class="w"&gt; &lt;/span&gt;Linker&lt;span class="w"&gt; &lt;/span&gt;Version&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;.0&lt;span class="w"&gt; &lt;/span&gt;beta&lt;span class="w"&gt; &lt;/span&gt;Jan&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;:01:50&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;-bit&lt;span class="o"&gt;)&lt;/span&gt;
...
loading&lt;span class="w"&gt; &lt;/span&gt;object&lt;span class="w"&gt; &lt;/span&gt;files
searching&lt;span class="w"&gt; &lt;/span&gt;libraries
creating&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;DOS&lt;span class="w"&gt; &lt;/span&gt;.COM&lt;span class="w"&gt; &lt;/span&gt;executable
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To test, the file can be ran in dosbox.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Dosbox screenshot" src="dosbox_screenshot.png"&gt;&lt;/p&gt;</content></entry><entry><title>Amiga restoration project</title><link href="https://thomaspurnell.com/017_Amiga1200/amiga1200.html" rel="alternate"/><published>2025-11-28T00:00:00+10:00</published><updated>2025-11-28T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2025-11-28:/017_Amiga1200/amiga1200.html</id><summary type="html">Amiga restoration project</summary><content type="html">&lt;p&gt;As mentioned back in
&lt;a href="https://www.thomaspurnell.com/015_September_2025/september_2025.html"&gt;September&lt;/a&gt;,
I bought an Amiga 1200 in unknown condition from ebay. This page simply
documents the steps I took to get it working. I also have an
&lt;a href="https://merveilles.town/@voxel/115262076334565495"&gt;ongoing thread&lt;/a&gt;
on the fediverse about this process.
There&amp;rsquo;s also a much more nicely written and entertaining Amiga restoration on
&lt;a href="https://amiga.console.cc"&gt;Console&amp;rsquo;s blog&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Amiga 1200&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Amiga 1200" src="a1200.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I received an Amiga 1200 rev 1D.4, with an installed spinning-disk
harddrive, and a MBX-1200z expansion card, which has a floating point
processor, realtime clock, and 2MB of RAM fitted. No power supply,
peripherals or cables were included.&lt;/p&gt;
&lt;p&gt;For power, I ordered a
&lt;a href="https://www.retrokit.com.au/product/commodore-amiga-psu-boost-edition-for-a500-a500-a600-a1200-or-cd32/"&gt;modern replacement from Retrokit.com.au&lt;/a&gt;.
I&amp;rsquo;ve seen some reports since that this design of power supply is unreliable,
or might even damage the computer in the long term, but many other people
seem to be using them without issue.&lt;/p&gt;
&lt;h2&gt;Video signal problems&lt;/h2&gt;
&lt;p&gt;Once I had the Amiga and a power supply, I was ready to start testing.
Power LED lit up when I supplied power, but I couldn&amp;rsquo;t get a video
signal on the first screen I tried. I only have cables for testing the
composite &amp;lsquo;tv&amp;rsquo; video output, and there&amp;rsquo;s not many screens around that
still have sockets for such a connection. I managed to temporarily get
an old portable flat panel display to show .. something:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Luma problem" src="luma.gif"&gt;&lt;/p&gt;
&lt;p&gt;First thing I did was replace all of the barrel capacitors on the board.
This is a strongly recommended step for Amigas of this era; the capacitors
are old enough that they&amp;rsquo;re basically guaranteed to leak. If not already,
then soon.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Motherboard wrapped up in Kapton tape" src="kapton.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Sensitive areas of the board near the surgery sites were decorated with 
flimsy filmy Kapton tape, which didn&amp;rsquo;t feel like it would offer much in
the way of protection, but apparently it works because it did a fine job
of stopping surrounding components from melting under the deadly breath
of my heat gun. The old capacitors did not surrender easily. I heated them
to melt the solder holding them in place, but the same heat also caused these
leaky bois to hiss, spit and in a couple of cases loudly pop at me.
Eye protection mandatory. Ventilation also recommended, the leaking
electrolyte has a horrible fish smell and is probably not recommended for
breathing. Replacement capacitors, which I&amp;rsquo;m assured should
never leak, went on fairly easily.&lt;/p&gt;
&lt;p&gt;Unfortunately the recap didn&amp;rsquo;t make any difference to the composite video
problem, but I&amp;rsquo;m actually glad it didn&amp;rsquo;t work immediately. I wanted a
hardware project and expected this kind of problem when I placed the order.
This was an opportunity to use my oscilloscope to explore the board,
measuring each point of the video system that I could get to with the probes.
The excellent &lt;a href="www.amigapcb.org"&gt;amigapcb.org&lt;/a&gt; has interactive diagrams of
the circuit board, making tracing the routes between pads and components
fast and easy.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Scope output" src="scope.png"&gt;&lt;/p&gt;
&lt;p&gt;Eventually I found that the &amp;lsquo;LUMA IN&amp;rsquo; signal needed by the composite video
encoder chip was not being received. There&amp;rsquo;s a &amp;lsquo;delay line&amp;rsquo; designed to
generate the signal based on the the &amp;lsquo;LUMA OUT&amp;rsquo; signal from the same chip,
and nothing was coming back from the delay line.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Cropped view of Composite Video encoder circuit" src="luma_schematic.png"&gt;&lt;/p&gt;
&lt;p&gt;I experimented with shorting the LUMA_OUT to LUMA_IN pins on the encoder to
bypass the delay line and see if I could get something recognisable on the
screen..&lt;/p&gt;
&lt;iframe src="https://merveilles.town/@voxel/115282065697711122/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"&gt;&lt;/iframe&gt;
&lt;script src="https://merveilles.town/embed.js" async="async"&gt;&lt;/script&gt;

&lt;p&gt;The dead delay line components are basically unobtainium, but some research
revealed
&lt;a href="https://www.ikod.se/cxa2075m/"&gt;wise masters have devised an upgrade&lt;/a&gt;
that replaces the current encoder and delay line with a single (available!)
encoder that has its own internal delay. I ordered a replacement from
&lt;a href="https://utsource.net/itm/p/1332076.html"&gt;utsource.net&lt;/a&gt;.&lt;/p&gt;
&lt;video width="360" height="640" controls&gt;
&lt;source src="encoder_removal.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;More surgery later, and I now have working composite video output :)&lt;/p&gt;
&lt;p&gt;&lt;img alt="Working composite" src="new_encoder.jpg"&gt;&lt;/p&gt;
&lt;h2&gt;Booting Workbench&lt;/h2&gt;
&lt;p&gt;When I bought my PSU I also included a Compact Flash to IDE adapter, letting
me use a CF Card as an internal harddrive on the Amiga. Plugging the CF card
into my desktop PC, I was able to install a copy of the Workbench 3.0
operating system and a few pieces of software, directly from an emulator.
Plugging this into the Amiga seemed to work first time.&lt;/p&gt;
&lt;p&gt;&lt;img alt="First boot" src="first_boot.jpg"&gt;&lt;/p&gt;
&lt;h2&gt;Kybrd Prblm&lt;/h2&gt;
&lt;p&gt;But almost no keys on the keyboard were registering. Removing the
connector, cleaning it and replacing it didn&amp;rsquo;t help. The end of the keyboard
ribbon connector looked like the conductor had turned black with corrosion.
Later I learned that this is actually a layer of conductive carbon that&amp;rsquo;s
supposed to be there. I trimmed back the ribbon and managed to get about 2/3
of the keys working.&lt;/p&gt;
&lt;p&gt;Rather than attempt to fix broken traces on a flimsy membrane, I opted to
buy a replacement from a
&lt;a href="https://www.ebay.com.au/str/retrofuzion"&gt;local distributor&lt;/a&gt;.
It arrived quickly and installation was quick and easy.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Keyboard membrane installation" src="membrane.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Now all keys are working, except for the Right &amp;lsquo;A&amp;rsquo; button, which acts as
a modifier key like alt or shift. Haven&amp;rsquo;t worked out the problem yet.&lt;/p&gt;
&lt;h2&gt;Audio&lt;/h2&gt;
&lt;p&gt;Audio output was functional at this point, but the volume seemed to wander
up and down (mostly down), sometimes barely audible. I reflowed the solder
of the replacement surface mount capacitor I had added nearest to the audio
outputs, and the problem was immediately fixed. Hooray for easy fixes!&lt;/p&gt;
&lt;h2&gt;File system corruption&lt;/h2&gt;
&lt;p&gt;About this time I started noticing that large files seemed broken somehow.
Any large program seemed to not work. Some would crash, others just error
out. It took me a while but I found a
&lt;a href="https://johan.driessen.se/posts/Setting-up-a-Compact-Flash-card-with-Classic-Workbench-and-WHDLoad-for-Amiga-600-1200/"&gt;more comprehensive guide&lt;/a&gt;
which recommends limiting the maximum transfer size per write to a partition.
An arcane setting easily overlooked. I guess there&amp;rsquo;s some kind of buffer
issue or bug that this works around.&lt;/p&gt;
&lt;p&gt;&lt;img alt="MaxTransfer partition setting" src="hdtoolbox-5-change-file-system.png"&gt;&lt;/p&gt;
&lt;p&gt;Since setting this parameter and (re)installing workbench 3.1, I&amp;rsquo;ve had
zero of these mysterious corruption issues.&lt;/p&gt;
&lt;h2&gt;Networking&lt;/h2&gt;
&lt;p&gt;Next up I wanted to get the Amiga on the network. While it&amp;rsquo;s decidedly
underpowered for browsing modern websites, having networking makes moving
files on and off of the machine much easier and quicker.&lt;/p&gt;
&lt;p&gt;The A1200 has a PCMIA slot, a cross-platform standard for small expansion
cards. The most popular compatible networking cards are fairly expensive
due to demand, but during my research I found a
&lt;a href="https://aminet.net/driver/net/xircomce2.readme"&gt;newly released driver&lt;/a&gt;
for the xircom range of PCMIA network devices. These are much less in demand
and I was able to buy one for about $10.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Xircom PCMIA NIC" src="xircom_clothed.jpg"&gt;&lt;/p&gt;
&lt;p&gt;One issue I didn&amp;rsquo;t anticipate is that some PCMIA devices are actually
&amp;lsquo;two units tall&amp;rsquo;. The plastic shell of the device made it too large to
physically fit into the slot in the Amiga, despite the actual connection
fitting when the case was removed from the Amiga.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Xircom PCMIA NAKED" src="xircom_naked.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t want to cut a hole in the Amiga to accomodate the card, so instead
I trimmed the shell of the network card.
I told the driver author about this and I think he was somewhat horrified:
&amp;lsquo;I just bought a pcmia extender on ebay&amp;hellip;&amp;rsquo;. Sorry Neil.&lt;/p&gt;
&lt;p&gt;&lt;img alt="AmiIRC session" src="irc_session.png"&gt;&lt;/p&gt;
&lt;p&gt;Finally I can log into IRC and hassle my friends in #DosGameClub and let
them know how much better IRC is on the Amiga (:. File transfer also works.&lt;/p&gt;
&lt;h2&gt;Future&lt;/h2&gt;
&lt;p&gt;Currently I&amp;rsquo;m awaiting an adapter to let me plug a vga cable into the
23 pin Video output port. It might allow me to get a better quality picture
than composite, but relies on being connected to a compatible monitor.
I don&amp;rsquo;t have high hopes that my monitors will support the 15Khz signal,
but I could get lucky. The alternative is to try and find an older
supported monitor, or use some kind of signal processor to transform the
signal into something my monitors do support. Those are expensive and I
really don&amp;rsquo;t want More Devices being plugged in if I can help it.&lt;/p&gt;
&lt;p&gt;I also have an 8MB stick of RAM in the post to slot into the expansion port.
Currently there&amp;rsquo;s only 2MB of expansion RAM, and I&amp;rsquo;d like more for games
and graphics creation later on.&lt;/p&gt;</content></entry><entry><title>October and November 2025</title><link href="https://thomaspurnell.com/016_OctoberNovember_2025/october_november_2025.html" rel="alternate"/><published>2025-11-27T00:00:00+10:00</published><updated>2025-11-27T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2025-11-27:/016_OctoberNovember_2025/october_november_2025.html</id><summary type="html">October and November 2025</summary><content type="html">&lt;p&gt;Been busy, been tired.&lt;/p&gt;
&lt;h2&gt;Trip&lt;/h2&gt;
&lt;p&gt;The latter half of October was spent on a multi-stop trip: Turkiye, Germany, Greece. I don&amp;rsquo;t think anyone enjoys looking at other people&amp;rsquo;s holiday snaps, but I&amp;rsquo;m going to post a few anyway :p.&lt;/p&gt;
&lt;h3&gt;Turkiye&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Outside the Hagia Sophia" src="hagia_sophia_exterior.jpg"&gt;
&lt;img alt="Lamp post in the palace gardens" src="lampost.jpg"&gt;&lt;/p&gt;
&lt;p&gt;We stayed a few days in the Faith district of Istanbul, dodging crowds, cats, trams and highly motivated hawkers. Incredible historical architecture and food. The Blue Mosque interior is pretty amazing (and impossible to photograph when you&amp;rsquo;re in a throng of tourists).&lt;/p&gt;
&lt;p&gt;&lt;img alt="Blue Mosque interior ceilings" src="blue_mosque.jpg"&gt;&lt;/p&gt;
&lt;p&gt;We ducked down into the Basilica Cisterns. Some of the pillars are made of repurposed Roman material, like this Gorgon&amp;rsquo;s head.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Gorgon's head" src="gorgon.jpg"&gt;
&lt;img alt="Down in the cisterns" src="cistern.jpg"&gt;&lt;/p&gt;
&lt;h3&gt;Germany&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Marktkirch Halle" src="marktkirche.jpg"&gt;&lt;/p&gt;
&lt;p&gt;A quick trip to Leipzig and Halle to visit some &lt;a href="https://ratking.de"&gt;good friends&lt;/a&gt;. Probably a strange observation but I was really surprised by the intensity of economic activity, there are so many shops and businesses operating. Also some great historical architecture. Leipzig zoo was fun, and a lot of good chocolates were eaten.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A street" src="street.jpg"&gt;
&lt;img alt="Walking with a friend" src="coolfriends.jpg"&gt;&lt;/p&gt;
&lt;h3&gt;Greece&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Corfiot sunrise" src="corfu_sunrise.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The primary purpose of the trip was to attend a family gathering on the isle of Corfu. I saw relatives I haven&amp;rsquo;t seen in over ten years, and we had a memorial for my grandmother who died not so long ago.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Greek payphone" src="phone.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Listen, I have my interests and old payphones are among them, don&amp;rsquo;t judge me.&lt;/p&gt;
&lt;h2&gt;Gamedev&lt;/h2&gt;
&lt;p&gt;I do love game jams, probably too much. It&amp;rsquo;s appealing to start afresh, work maniacally for an intense period, and then effectively abandon the game at the end of the jam. But this isn&amp;rsquo;t conducive to creating quality work, especially not of a commercial standard. As of November I&amp;rsquo;ve resolved to temporarily retire from jams and focus on a longer term project. It has been difficult. I&amp;rsquo;ll have a dedicated update later, but so far I&amp;rsquo;ve managed to force myself to spend time only on gameplay programming and avoid any work on art assets, which has resulted in some very professional looking placeholder textures, like this one for a climbable chainlink fence.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Programmer art" src="programmer_art.png"&gt;&lt;/p&gt;
&lt;h2&gt;Reading&lt;/h2&gt;
&lt;p&gt;My trip included plenty of opportunities for reading. Everything I read was great.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Tanya_Huff_bibliography#Valor_Confederation_series"&gt;Valor&amp;rsquo;s Choice - Tanya Huff&lt;/a&gt;. Not too ashamed to admit that I&amp;rsquo;ve read this more than once previously and I didn&amp;rsquo;t even realise until I got a few chapters in. Solid military scifi.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/The_Stainless_Steel_Rat"&gt;Stainless Steel Rat - Harry Harrison&lt;/a&gt;. Some classic 50s scifi. Pretty predictable but highly enjoyable cheesy space-rogue adventures.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Blindness_(novel)"&gt;Blindness - José Saramago&lt;/a&gt;. This one was great. Well outside of my usual reading zone, I really enjoyed the unusual writing and tone. A somewhat hopeful post-apocalyptic survival story.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/The_Light_Brigade_(novel)"&gt;The Light Brigade - Kameron Hurley&lt;/a&gt;. A time hopping military scifi story that could have come straight from Philip K Dick. Read through it in two solid blocks. A little grim but I couldn&amp;rsquo;t put it down. &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Fails&lt;/h2&gt;
&lt;p&gt;The TTGO I mentioned previously died an ignoble death and no longer powers up. It seems the usb connector used for power was particularly fragile. Not sure I can salvage it, the connector has inaccessible solder pads underneath, which means heatguns in confined spaces.&lt;/p&gt;</content></entry><entry><title>September 2025</title><link href="https://thomaspurnell.com/015_September_2025/september_2025.html" rel="alternate"/><published>2025-10-01T00:00:00+10:00</published><updated>2025-10-01T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2025-10-01:/015_September_2025/september_2025.html</id><summary type="html">September 2025</summary><content type="html">&lt;p&gt;Another monthly update!&lt;/p&gt;
&lt;h2&gt;Game Dev&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been working on a &amp;lsquo;shoot-em-up&amp;rsquo; editor and engine for &lt;a href="https://itch.io/jam/dosember-game-jam"&gt;Dosember Game Jam&lt;/a&gt;.
The editor runs in DOS, which has made it a little more interesting to work on,
but also means progress has been slower than it might have been if I&amp;rsquo;d targeted
contemporary operating systems only. Not much to show yet but I uploaded a
demonstration video to mastodon:&lt;/p&gt;
&lt;iframe src="https://merveilles.town/@voxel/115194628952703107/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"&gt;&lt;/iframe&gt;
&lt;script src="https://merveilles.town/embed.js" async="async"&gt;If you had javascript enabled you'd see &lt;a href="https://merveilles.town/@voxel/115194628952703107"&gt;a mastodon post embedded here showing a video of my editor&lt;/a&gt;&lt;/script&gt;

&lt;p&gt;There won&amp;rsquo;t be too much time to work on this over the next few weeks, but the
jam runs all the way through to the end of November, so there should be a
shooty-dossy game before the end of the year.&lt;/p&gt;
&lt;p&gt;&lt;img alt="68000 Assembly Programming book cover" src="68000_assembly.jpg"&gt;&lt;/p&gt;
&lt;p&gt;After some months presumed lost in the post,
&amp;lsquo;68000 ASSEMBLY LANGUAGE PROGRAMMING&amp;rsquo; arrived in my mail box.
This is fortunate timing because this month I also finally
gave in to the urge to buy a:&lt;/p&gt;
&lt;h2&gt;Broken Amiga 1200&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Amiga 1200 on a desk" src="a1200.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Yeah! I&amp;rsquo;ve wanted a real Amiga (which uses a 68000 family processor) for ages.
But I&amp;rsquo;ve had perfectly good emulation - especially on the MisterFPGA. However,
the USB controller on my Mister board died recently and I&amp;rsquo;ve been unable to
repair it. So finally I gave in and placed a bid on an &amp;lsquo;untested&amp;rsquo; A1200 on
ebay. &amp;lsquo;Untested&amp;rsquo; can mean a lot of different things on online auctions, but
it&amp;rsquo;s best to assume that the item definitely does not work. I won the auction
and received an Amiga that turns on, but the composite and RF video sockets
do not put out a valid signal:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Amiga video output" src="luma.gif"&gt;&lt;/p&gt;
&lt;p&gt;Instead there&amp;rsquo;s a rapidly rolling out-of-sync signal with the wrong colours.
This is OK though, I wanted a hardware project, and repairing the Amiga is
already underway. I&amp;rsquo;ll document my repair progress in another post soon.&lt;/p&gt;
&lt;h2&gt;TTGO VGA&lt;/h2&gt;
&lt;p&gt;&lt;img alt="TTGO VGA dev board" src="ttgo_1.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Another delayed mail item, a little ESP32 devboard arrived. It&amp;rsquo;s unusual for
having a VGA port for video output, and two PS/2 ports for old style keyboard
and mouse connections. I set mine up as a very low power DOS emulator. It
draws almost no power, and can only run very old software with modest
requirements. It also only cost about AU $15, which is great for a tiny
computer.&lt;/p&gt;
&lt;p&gt;&lt;img alt="TTGO VGA dev board in fancy pants case" src="ttgo_2.jpg"&gt;&lt;/p&gt;
&lt;p&gt;It needed a case to keep it safe so I took an existing model and designed
a deco-inspired grill for it. I&amp;rsquo;m pleased with how it turned out, but
unfortunately I managed to bend the micro-usb connector severely out of
shape when unplugging the power cable :(.&lt;/p&gt;
&lt;h2&gt;Played&lt;/h2&gt;
&lt;p&gt;This month I finished an ongoing campaign of Carrier Command 2 against some AI fleets.&lt;/p&gt;
&lt;h3&gt;Carrier Command 2&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Carrier command 2 montage" src="carrier_command_2.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Really this is a &amp;lsquo;multiplayer first&amp;rsquo; game, the single player experience
being initially overwhelming and then quickly becoming too easy.
The player commands a carrier (surprise!) with limited resources. An
archipelago of islands is populated with resources and factories that
can be captured to produce additional air, sea and land units with which
to combat ambient defences and opposing factions. Fuel and various types
of ammunition need to be produced too and delivered to where they&amp;rsquo;re
needed. Your logistics lines are at least as important as your combat units.
The land units are fairly redundant though, air units being more useful in
almost every situation. However, I found that it&amp;rsquo;s possible to start the
campaign with a limited loadout, where the carrier starts with no aircraft
available. This forces you to make use of the land units with naval
support for conquering the initial islands, making the starting phases
of the game more challenging. Unfortunately the AI players seem to suffer
no such limitations, and have access to aircraft and unlimited cruise
missiles from the outset. Still a great game regardless.&lt;/p&gt;
&lt;h3&gt;Beyond All Reason&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Beyond all Reason" src="bor.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Toward the end of the month one of my brothers organised a few rounds of
&amp;lsquo;Beyond All Reason&amp;rsquo;, an open source descendent of the real-time-strategy game
&amp;lsquo;Total Annihilation&amp;rsquo;. TA got a lot of play-time in our family home and is
definitely one of my preferred RTS - not a mad micromanagement rapid-fire
clickfest like Starcraft, but not so slow and long a campaign as to drag a
match out all day. It&amp;rsquo;s an impressive example of open source gaming, cross
platform online multiplayer all developed and hosted by the community. My
only real complaint has always been the ugly user-interface. Fun to play
against a sibling, but I don&amp;rsquo;t think I&amp;rsquo;d have much fun in single player.&lt;/p&gt;
&lt;h2&gt;Other Media&lt;/h2&gt;
&lt;p&gt;This month I bought &lt;a href="https://zodiacmusicofficial.bandcamp.com/album/book-of-madness"&gt;&amp;lsquo;Book of Madness (狂気)&amp;rsquo; by Onryō&lt;/a&gt;.
Dark Drum and Bass, a bit minimal but with really good production.
The label &amp;lsquo;Zodiac Music&amp;rsquo; has a pretty strong musical identity - by which I
really mean many of the tracks are very similar to eachother. Case in point:
this album initially released with track 3 accidentally replaced
by a duplicate of track 2, and I didn&amp;rsquo;t even notice until they fixed it.
I love it regardless, dark dnb is my &amp;lsquo;productivity&amp;rsquo; music. I get a lot done 
when the drums are drumming at 170bpm.&lt;/p&gt;</content></entry><entry><title>August 2025</title><link href="https://thomaspurnell.com/014_August_2025/august_2025.html" rel="alternate"/><published>2025-08-27T00:00:00+10:00</published><updated>2025-08-27T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2025-08-27:/014_August_2025/august_2025.html</id><summary type="html">August 2025</summary><content type="html">&lt;p&gt;Inspired by &lt;a href="https://fholio.de/log/"&gt;ratrogue&amp;rsquo;s dev log&lt;/a&gt;, I decided to have a go at logging some of the things I work on, play and watch. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Game Dev&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Watched &lt;a href="https://www.youtube.com/watch?v=Va0Qy9Oou_M"&gt;The Anti-Compulsion Loop Series: Gradius Origins Review&lt;/a&gt;. The first third talks in great detail about the design decisions and limitations of the original Gradius games. The video is targeted at STG/shmup fans and makes a lot of assumptions about familiarity with various mechanics and terminology, making it hard to recommend to a general audience.&lt;/p&gt;
&lt;video width="600" height="320" controls&gt;
&lt;source src="tachi_compressed.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;Wrote a partial NES / famicom game inspired by the Tachikoma robots from the Ghost In The Shell manga / movies. Thought this would be interesting because Tachikoma designs are more about their movement and personality than static visual appearance, so I put some effort into procedural animation. Not a total success and certainly not a complete game, but I&amp;rsquo;m pleased with how the player character moves and animates. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Played&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Touhou Luna Nights" src="luna_nights.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Bought and played through the excellent &lt;a href="https://store.steampowered.com/app/851100/Touhou_Luna_Nights/"&gt;Touhou Luna Nights&lt;/a&gt;, a pixel-art platforming metroidvania where the protagonist can slow and stop time and throw hundreds of knives. Time control is needed to solve a few puzzles, and normally passable water becomes completely solid when time is stopped, serving as a barrier or floor. As the game progresses you encounter enemies and puzzles that only move or attack while time is slowed. Elements of bullet hell reflect the touhou origins, and the humour is particularly good. I&amp;rsquo;d recommend this to anyone, though maybe it&amp;rsquo;s a little too difficult for some?&lt;/p&gt;
&lt;p&gt;&lt;img alt="Warzone 2100" src="warzone2100.png"&gt;&lt;/p&gt;
&lt;p&gt;I also reinstalled &lt;a href="https://wz2100.net/"&gt;Warzone 2100&lt;/a&gt;, an older RTS that is still kept updated as a community supported open source project. An absolutely ridiculous tech tree lets you research and design varied vehicles and weapon combinations that support many different play styles, many more than is typical for the genre. There&amp;rsquo;s a good mix of difficulty options too, though the AI is pretty poor in certain respects. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Other Media&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;&lt;img alt="Evangelion" src="evangelion.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Started watching &amp;lsquo;Neon Genesis: Evangelion&amp;rsquo; with my son. Not sure how appropriate it really is for a 10 year old child but so far we&amp;rsquo;ve both enjoyed it. I&amp;rsquo;ve started the series a few times previously but never made it beyond the third episode. &lt;/p&gt;
&lt;p&gt;Found a list of recommended sci-fi novels and grabbed a bunch at random on my ereader. For the first time in the five or so years I&amp;rsquo;ve had it, I have had cause to delete books. Some of the recommendations were clearly AI generated - meandering conversations that repeatedly loop back and make no sense whatsoever. The AI coverart should have served as a warning. Felt like I took some psychic damage just trying to read these. Of the &amp;lsquo;probably written by humans&amp;rsquo; books I tried, &amp;lsquo;Neural Wraith&amp;rsquo; by K.D. Robertson was best. An attempt at a detective noir story that had a small cast of human males and thousands of feminine robots whose descriptions largely centred on their breast size. For some reason that is never fully explained, all of the female robots are in the city are in love with the protagonist. But there was a passable story in there too. My main takeaway is that the list or recommendations is garbage.&lt;/p&gt;
&lt;p&gt;In music, I preordered &lt;a href="https://jazzsticks.bandcamp.com/album/smoke-and-fire-summer-of-love"&gt;&amp;lsquo;Smoke and Fire / Summer of Love&amp;rsquo; by Oversight&lt;/a&gt;, a jazzy drum and bass single that&amp;rsquo;s a bit more bright and cheery than my usual dnb listenings.&lt;/p&gt;</content></entry><entry><title>bloggpt, a rant</title><link href="https://thomaspurnell.com/013_bloggpt/bloggpt.html" rel="alternate"/><published>2025-03-24T00:00:00+10:00</published><updated>2025-03-24T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2025-03-24:/013_bloggpt/bloggpt.html</id><summary type="html">bloggpt, a rant</summary><content type="html">&lt;p&gt;It is not news to anyone that the web is filled to the eyeballs with shit. This may always been the case, but never before was the effluent so endless in quantity and miserably poor in quality. Image diffusers and Large Language Models - so called &amp;lsquo;AI&amp;rsquo; - have created a vast new source of meaningless fecal content to pollute the information streams. Every social media site is filled with images and text generated in exchange for not inconsiderable amounts of electricty - all to try and farm enough traffic to generate advertiser revenue. So pervasive is the rot that even my closest friends use these AI &amp;lsquo;tools&amp;rsquo;, and at least one family member goes to great lengths to try and discover ways of monetising them. In my immense privilege, I use a moderated, private mastodon server for most of my social media interaction. I see little AI content unless I go looking for it - besides a few petty fraudsters trying to pass off generated art as their own, using it to try and solicit donations from the unwary. But elsewhere: tumblr, instagram, search results - these places I see more AI content than human. &lt;/p&gt;
&lt;p&gt;A small grace is that as the web becomes increasingly polluted with machine generated feculence, the source from which the AIs are trained becomes increasingly corrupt. In the 1990s, beef farmers in the UK recycled some of the less desirable cow parts back into the food supply for their herds. This cannibalism resulted in &amp;lsquo;mad cow disease&amp;rsquo; infecting the food supply and potentially the people that ate it. Likewise with AI, as the corpus on which they feed fills with AI content, their ability to generate coherent content rapidly reduces. Image diffusers either generate garbage or pictures that congregate on specific styles despite user instructions. LLMs generate poorer quality text filled with &amp;lsquo;tells&amp;rsquo;, words increasingly common in AI generated content like &amp;lsquo;delve&amp;rsquo; and &amp;lsquo;hitch&amp;rsquo;. &amp;lsquo;Pre-AI&amp;rsquo; datasets, consisting of mostly human generated content free of generative taint, are valuable commodities. &lt;/p&gt;
&lt;p&gt;Why am I talking about this here? Blogs are one of the continuing sources of longer-form human generated content. Clearly there are blogs written by AI, or perhaps co-written, but there are still a good number of people continuing to blog their thoughts, notes, gamedev environment configuration instructions and other important human generated text. My own humble blog is free of AI content, and while it may not be the most important writing humanity has produced, it&amp;rsquo;s infinitely more valuable than anything ever output from chatgpt. &lt;/p&gt;
&lt;p&gt;A cursory glance at the website access logs for any public blog will show that automated scrapers account for much (if not most) of the traffic any given blog receives. &amp;lsquo;Bots&amp;rsquo; have always crawled sites - you wouldn&amp;rsquo;t have much in the way of search engines without this. But the search engine &amp;lsquo;web spiders&amp;rsquo; were primarily indexing the content of websites to see if it had anything relevant for web search results. The current generation of crawlers want only to copy every image and sentence on a website into a training set. Whether that is to sell to model trainers or to build their own AI, the result is the same. Your words are taken and put into a giant statistics table that someone hopes to sell access to, and to burn kilowatts of electricity to make it happen. These crawlers also seem to ignore requests to not index your site via the venerable &amp;lsquo;robots.txt&amp;rsquo; standard that was somewhat respected by the web spiders of yore. &lt;/p&gt;
&lt;p&gt;Over on mastodon, there&amp;rsquo;s a culture of providing comprehensive alt-text, written descriptions of any images or non text media uploaded to the platform. This helps improve accessibility by providing a non graphical alternative for blind users, or potentially a low-bandwidth option for users on connections too slow or unstable for transferring photos of your cat. Image diffuser training relies on exactly these types of descriptions. The more accurate, detailed and explicit the text description of an associated image, the more valuable it is for the purposes of training an AI to generate fake versions of those visual subjects.  &lt;/p&gt;
&lt;p&gt;To use the web in the last 20 years was to feed the advertising algorithms. Every link clicked, text entered, momentary hesitation when scrolling past a photo - every recordable interaction fed into a program designed to sell you to advertisers. Now every contribution you make online is potentially fed into a vast hellish data lasagne to be fed to coal burning demon AI. This is the new cost of interacting with online communities and you can&amp;rsquo;t avoid it. I won&amp;rsquo;t stop blogging because I&amp;rsquo;m afraid an AI will give inaccurate instructions on configuring a raspberry pi as a dreamcast development machine. I won&amp;rsquo;t make my mastodon photos inaccessible to a blind person just to spite the seller of a dataset. &lt;/p&gt;
&lt;p&gt;Inconveniencing real people for a small chance at causing a rounding-error of difference to an AI isn&amp;rsquo;t worth the cost to me.  &lt;/p&gt;</content></entry><entry><title>DARIUSBURST Chronicle Saviours Dual Screen on linux</title><link href="https://thomaspurnell.com/012_dariusburst_chronicles_linux/dariusburst.html" rel="alternate"/><published>2024-12-01T00:00:00+10:00</published><updated>2024-12-01T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-12-01:/012_dariusburst_chronicles_linux/dariusburst.html</id><summary type="html">DARIUSBURST Chronicle Saviours Dual Screen on linux</summary><content type="html">&lt;p&gt;&lt;img alt="Darius Tux" src="darius_tux.jpg"&gt;
(The author&amp;rsquo;s impression of Tux the penguin visiting Darius space).&lt;/p&gt;
&lt;h2&gt;DARIUSBURST CS&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been playing a lot of shoot-em-up games lately. One I tried in the past and bounced off of was the Darius series. But I revisited it this week with fresh eyes and am really enjoying it. Giant robo-aquatic bosses firing missiles and lasers at your space in 32:9 widescreen. The game is designed to work on a &amp;lsquo;dual screen&amp;rsquo; setup, two standard 16:9 monitors side by side. This works OK in Windows - assuming you have exactly two monitors. I have three and this causes the game to try and display the game in the wrong resolution. Easily fixed by going into the windows settings and completely disabling the third monitor. Not ideal.&lt;/p&gt;
&lt;h2&gt;Linux&lt;/h2&gt;
&lt;p&gt;But obviously I don&amp;rsquo;t want to boot windows if there&amp;rsquo;s any reasonable alternative. The game runs OK under linux - in single screen mode. Setting the game configurator to Dual Screen gives only a triple screen resolution&lt;/p&gt;
&lt;p&gt;&lt;img alt="Darius configurator showing only a 5760x1080 resolution option" src="5760.png"&gt;&lt;/p&gt;
&lt;p&gt;and for some unknown reason Window Mode offers a selection of lesser resolutions which do work - but don&amp;rsquo;t fill two screens.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Darius configurator showing smaller resolutions for two screens in window mode" src="window_resolutions.png"&gt;&lt;/p&gt;
&lt;p&gt;But no amount of tweaking the minimal exposed options can make the dual screen mode work fullscreen. Starting in &amp;lsquo;Full Screen - Dual Screen&amp;rsquo; mode just draws a double screen&amp;rsquo;s worth of pixels onto a single screen, and no amount of command line requests to resize the window to cover two screens had any effect.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot showing the game squished into too narrow a space" src="squashed.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m running the steam version of the game via proton, which is valve&amp;rsquo;s flavour of wine, a windows compatibility layer for linux. This means that we can make configuration changes to the wine environment used by a particular game, assuming we can work out where it is. Steam in linux installs wine environments to &lt;code&gt;~/.steam/steam/steamapps/compatdata/[steam_app_id]/pfx&lt;/code&gt;. One easy way to find the steam app id is to look at the url of a game in the steam store. In this case it is &lt;code&gt;377870&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Ensuring the game is not running, I launch &lt;code&gt;winecfg&lt;/code&gt; for Dariusburst with &lt;/p&gt;
&lt;p&gt;&lt;code&gt;WINEPREFIX=/home/voxel/.steam/steam/steamapps/compatdata/377870/pfx winecfg&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Navigating to the Graphics tab, we can choose to &amp;lsquo;Emulate a virtual desktop&amp;rsquo; and set the desktop size to 3840x1080, the resolution of two 1920x1080 monitors side by side.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Winecfg open to the Graphics tab" src="winecfg.png"&gt;&lt;/p&gt;
&lt;p&gt;Now when launching the game, the full screen resolution is detected as 3840x1080 - a double screen.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Darius configurator showing the wanted screen resolution options" src="correct_resolution.png"&gt;&lt;/p&gt;
&lt;p&gt;I can now launch the game and play across two monitors, without having to disable the third as I do in Windows, hooray. &lt;/p&gt;
&lt;p&gt;&lt;img alt="A photo of my workdesk playing the game across two monitors" src="desk_photo.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Maybe I should work on reorganising those wires next?&lt;/p&gt;</content></entry><entry><title>Dreamcast homebrew engine part 1</title><link href="https://thomaspurnell.com/011_dreamcast_homebrew_engine/dreamcast_homebrew_engine.html" rel="alternate"/><published>2024-10-22T00:00:00+10:00</published><updated>2024-10-22T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-10-22:/011_dreamcast_homebrew_engine/dreamcast_homebrew_engine.html</id><summary type="html">Dreamcast homebrew engine part 1</summary><content type="html">&lt;p&gt;&lt;img alt="Dreamcast and controller" src="dreamcast_and_controller.jpg"&gt;&lt;/p&gt;
&lt;h2&gt;Dreamcast&lt;/h2&gt;
&lt;p&gt;Released in Japan 1998 / elsewhere 1999, the Dreamcast was Sega&amp;rsquo;s last home game console. It was designed with 3D games in mind, excels in that regard, but was quickly eclipsed by the Playstation 2 and ultimately had a very short commercial lifespan. I love a doomed underdog, and when I first became aware of its homebrew scene a few years ago, I knew that Dreamcast development lay in my near future. I&amp;rsquo;ve made a &lt;a href="https://voxel.itch.io/dashy-blast"&gt;few prototypes already&lt;/a&gt;, but for various reasons they have been indefinitely postponed. There is an actively maintained open source &amp;lsquo;operating system&amp;rsquo; named &lt;a href="https://github.com/KallistiOS/KallistiOS"&gt;Kallistios&lt;/a&gt; that enables new software to be developed for the console without having to use the copyrighted official software development kit from Sega. Coupled with forums filled with technical discussions, a &lt;a href="https://dreamcast.wiki"&gt;recently revamped wiki&lt;/a&gt; and multiple busy Discord servers (boo), there&amp;rsquo;s a wealth of resources for understanding the system&amp;rsquo;s workings and how to develop for it. &lt;/p&gt;
&lt;h2&gt;Dreamdisc 24&lt;/h2&gt;
&lt;p&gt;This December will see the &lt;a href="https://itch.io/jam/dream-disc-24"&gt;&amp;lsquo;Dreamdisc 24&amp;rsquo; gamejam&lt;/a&gt;, a community event where all Dreamcast developers are invited to create a game, some of which will end up on a physical compilation disc. I have been known to participate in a gamejam or two (it&amp;rsquo;s basically my entire personality), so I&amp;rsquo;m excited to participate. In preparation, I&amp;rsquo;ve been implementing a game engine - a framework on which multiple different games coule be built.  &lt;/p&gt;
&lt;h2&gt;Homebrew engine&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Dashy no blast animated gameplay" src="/gallery/dashy_no_blast.gif"&gt;&lt;/p&gt;
&lt;p&gt;Though the engine I put together for &lt;a href="https://voxel.itch.io/dashy-blast"&gt;Dashy No Blast&lt;/a&gt; was reasonably sophisticated, I was using an opengl abstraction layer named GLDC rather than working with the lower level Power VR graphics api that Kallistios exposes. When I recently returned to Dreamcast development and tried to rebuild Dashy no Blast with the latest Kallistios and GLDC updates, I was met with only a black screen on the Dreamcast. Initially I assumed I was failing to initialise something properly, and despite a few hours of investigation was unable to draw anything to the display. Eventually I discovered that a recent Kallistios update made a breaking change that prevented GLDC from working - but before learning this I had already started exploring rendering 3D on the system without opengl.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Hello Triangle" src="hello_triangle.png"&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a mighty triangle running on an emulated dreamcast, my first baby. All 3D images on the Dreamcast are created from triangles - often hundreds or thousands of them - so being able to output a single triangle to the screen is an important step. This confirms that I&amp;rsquo;m able to draw in 3D without using OpenGL. In theory, I can draw more triangles and at a faster rate if I use the Kallistios PVR api directly, without the slightly more generalised OpenGL layer performing calculations in the middle. In practice there is probably quite a lot of optimisation needed to get from an initial naive approach to a system that parallels the performance of the dreamcast OpenGL implementation. But for now the next step was loading and displaying a more complicated mesh, with textures:&lt;/p&gt;
&lt;video width="640" height="480" loop autoplay muted&gt;&lt;source src="textured_room.mp4"&gt;(Embedded video of a room rotating not supported here)&lt;/video&gt;

&lt;h2&gt;Z Culling&lt;/h2&gt;
&lt;p&gt;If you look closely in the video above you may notice pieces of the floor turning black. Any geometry that extends offscreen needs to be &amp;lsquo;clipped&amp;rsquo; to prevent errors and chaotic triangle soup from being drawn all over the screen. Here&amp;rsquo;s an example of when culling isn&amp;rsquo;t working:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Broken rendering" src="broken.gif"&gt;&lt;/p&gt;
&lt;p&gt;&amp;lsquo;Illegal&amp;rsquo; triangle data in the stream sent to the graphics chip is discarded, leaving holes that invalidate the meaning of subsequent data, leading to unpredictable results. My &amp;lsquo;easy&amp;rsquo; solution for now is to cull any triangle that is has one or more corners with a Z coordinate behind the camera. Culled triangles are never sent to the PVR graphics chip, preventing this problem from occurring - but at the cost of not being able to draw geometry that can be behind the view. This can mean holes in floors, missing walls, and other incomplete geometry if the camera is too close.&lt;/p&gt;
&lt;p&gt;It is only possible to submit &amp;lsquo;so many&amp;rsquo; triangles to the PVR each frame. Exceeding this number can slow the framerate of the game if the scene is not ready to be drawn when the next frame must be drawn. If any of the submitted triangles are not visible, due to being offscreen to the left or right, or perhaps too far into the distance, then there&amp;rsquo;s no point in sending them to the PVR. Rendering a tall skyscraper 90&amp;rsquo; to the right of where the camera is looking is useless if it can&amp;rsquo;t be seen. Instead the triangle budget could be spent on more detail for the objects that are onscreen. To limit the number of wasted triangles sent to the PVR, another type of culling is needed.&lt;/p&gt;
&lt;h2&gt;Bounding sphere visibility culling&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Two low poly space carriers" src="banner.jpg"&gt;
Let&amp;rsquo;s say we want to draw this spaceship scene. The engine transforms the position of the ship and all of its triangles by the camera projection matrix. This operation converts from &amp;lsquo;3D space inside the game&amp;rsquo; to &amp;lsquo;2D space on the screen&amp;rsquo;. If the camera is at the coordinates 0,0,0 and the ship is located at 0,0,50 then if the camera is facing the right direction, the ship will be drawn at an appropriate scale for being &amp;lsquo;50 units&amp;rsquo; distant. If the ship were moved to 0,0,100 then the distance from the camera to ship is 100 and we would expect a reduction in the size of the ship drawn to the screen. Likewise adjusting the X and Y coordinates would move the ship around the screen horizontally and vertically respectively. Too far in either of these directions, and the ship would be &amp;lsquo;offscreen&amp;rsquo; and invisible to the viewer.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Ship encaged in a wireframe sphere" src="bounding_sphere.png"&gt;&lt;/p&gt;
&lt;p&gt;My engine creates an imaginary sphere containing each object to be drawn to the screen. Before the somewhat expensive operation of calculating the transformed position of every triangle, the engine calculates the screen position of where the sphere would be by transforming the position of the ship and calculating the screen size of the sphere. Then it is a simple matter of 2D maths to check if this position overlaps the visible area of the screen.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Example sphere visibility culling. A yellow rectangle represents visible screen space. A ship in a green sphere overlaps the screen rectangle and is labelled Onscreen Sphere. Another ship in a red sphere does not overlap the screen rectangle and is labelled Offscreen Sphere" src="sphere_culling.png"&gt;&lt;/p&gt;
&lt;p&gt;The yellow rectangle represents the visible screen space. Any spheres that are contained or overlap this rectangle will have their contents drawn to the screen, as is shown with the green onscreen sphere. The red offscreen sphere does not intersect the screen rectangle, and so is culled and not sent to the PVR graphics chip, saving triangles. &lt;/p&gt;
&lt;p&gt;This system is not perfect - in this example the ships are long but not tall. A sphere that slightly overlaps at the top or bottom of the screen will still send wasted triangles in this example, but it is a good approximation and cheap to test. If I write a game that would benefit from more precise culling, I could extend the spheres to support being squashed on some axes, or using multiple small spheres instead of a single large one.&lt;/p&gt;
&lt;h2&gt;To be continued&lt;/h2&gt;
&lt;p&gt;The engine contains a Level Of Detail system to save triangles by drawing simplified versions of meshes at long distances where details are not visible, limited gltf file format support, audio with dynamic pitch and volume, and a rudimentary &amp;lsquo;cel-shading&amp;rsquo; like style with model outlines. If there&amp;rsquo;s anything to be said about those or other features, I&amp;rsquo;ll add a part 2 in the near future. Until I decide on what game I&amp;rsquo;d like to make for the Dreamdisc-24 jam, I can&amp;rsquo;t be sure what else the engine will need.&lt;/p&gt;</content></entry><entry><title>Deathloop 256 NES/Famicom STG shooter</title><link href="https://thomaspurnell.com/010_deathloop_256/deathloop_256.html" rel="alternate"/><published>2024-09-18T00:00:00+10:00</published><updated>2024-09-18T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-09-18:/010_deathloop_256/deathloop_256.html</id><summary type="html">Deathloop 256 NES/Famicom STG shooter</summary><content type="html">&lt;h2&gt;Alakajam 20&lt;/h2&gt;
&lt;p&gt;This weekend was the &lt;a href="https://alakajam.com/20th-alakajam/games"&gt;20th tri-annual 48 hour Alakajam gamejam&lt;/a&gt;. Participants suggest themes, the community takes a vote, and the theme most (dis)liked by all is revealed at the start of the 48 hours. Games are written from scratch and submitted before the time is up - developers working alone for the solo category or in arbitrary sized teams for the team category. My entry (&lt;a href="https://voxel.itch.io/nes-deathloop-256"&gt;which you can play here&lt;/a&gt;) is a NES game.&lt;/p&gt;
&lt;h2&gt;NES Famicom&lt;/h2&gt;
&lt;p&gt;Released in 1983 in Japan, and other countries soon after, Nintendo&amp;rsquo;s &amp;lsquo;Family Computer&amp;rsquo; or &amp;lsquo;Entertainment System&amp;rsquo; is a limited machine with 2KB of work RAM, 2KB of video RAM, 54 colours, and a 6502 family processor clocked at a stately 1.79MHz, probably about 1 thousandth the speed of the device you&amp;rsquo;re using to read this. It&amp;rsquo;s awesome. I have an ongoing game project for this console, with a plan to produce a small run of physical cartridges - which means I&amp;rsquo;ve deliberately targeting the minimum spec for a game cartridge to keep cost and difficulty of hardware development to a minimum. But commercial releases for the console were not always so constrained - it&amp;rsquo;s possible to add special chips that allow for extra memory, super graphics capabilities and much more - all within the game cartridge. So for this gamejam I decided to give myself the luxury of Nintendo&amp;rsquo;s top of the line mapper - the extra control chip on the cart - and use the &amp;lsquo;MMC5&amp;rsquo;. Most notably, this chip lets the cart exceed the normal 8KB of stored graphics by many, many times.&lt;/p&gt;
&lt;h2&gt;Vertical scrolling&lt;/h2&gt;
&lt;video width="512" height="480" controls autoplay muted&gt;&lt;source src="windstorm_reef.mp4"&gt;(Embedded video not supported here)&lt;/video&gt;

&lt;p&gt;The NES can scroll the screen in any direction - but that moves the entire screen as a single chunk, as shown here in another of my NES jam games, Windstorm Reef. My interest for this jam was creating a vertical parallax effect, scrolling the screen vertically, but at different speeds to give an illusion of depth. While it&amp;rsquo;s possible to dynamically change the vertical scroll of screen regions in realtime, it&amp;rsquo;s very difficult to do accurately, especially if you have gameplay happening simultaneously and using the CPU&amp;rsquo;s very limited time. &lt;/p&gt;
&lt;p&gt;So instead of &amp;lsquo;actually&amp;rsquo; scrolling the screen, I leveraged the MMC5&amp;rsquo;s ability to access large amounts of graphical memory and drew versions of the background in each state of being scrolled.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Tiles viewed in NEXXT graphics editor" src="nexxt_tiles.png"&gt;&lt;/p&gt;
&lt;p&gt;Here are four &amp;lsquo;frames&amp;rsquo; of the background, stored in a 4KB &amp;lsquo;page&amp;rsquo; of CHR (CHaRacter) memory. Overall the vertical scrolling effect uses 64 frames, with each of the frames contains all of the graphics needed to make a frame of the background image when repeated:&lt;/p&gt;
&lt;p&gt;&lt;img alt="A NES nametable using the background tiles" src="nametable.png"&gt;&lt;/p&gt;
&lt;p&gt;The NES &amp;lsquo;thinks&amp;rsquo; that it&amp;rsquo;s drawing the same background every frame and has no concept that any &amp;lsquo;scrolling&amp;rsquo; is happening. Instead, the MMC5 mapper chip is instructed to page a section of video memory. When the graphics &amp;lsquo;PPU&amp;rsquo; processor is ready to draw the pixels for the background, it looks up the correct values to draw by first checking the &amp;lsquo;nametable&amp;rsquo; to get a tile index, and then grabs the pixels for that tile from character ROM. This lookup happens via the MMC5, which I&amp;rsquo;ve programmed to &amp;lsquo;point&amp;rsquo; to a different window of the background tile library each frame.&lt;/p&gt;
&lt;p&gt;We can examine what the console &amp;lsquo;sees&amp;rsquo; in video RAM in realtime using the Mesen emulator&amp;rsquo;s Tile Viewer debug tool:&lt;/p&gt;
&lt;video width="512" height="480" loop autoplay muted&gt;&lt;source src="tile_viewer.mp4"&gt;(Embedded video not supported here)&lt;/video&gt;

&lt;p&gt;By rapidly flicking through the different pages of character rom, the background tiles appear to animate. 
Putting it all together, the end result is pretty good:&lt;/p&gt;
&lt;video width="512" height="480" controls autoplay muted loop&gt;&lt;source src="scrolling.mp4"&gt;(Embedded video not supported here)&lt;/video&gt;

&lt;p&gt;I also tried to implement a small amount of horizontal perspective, but I&amp;rsquo;m unhappy with how it turned out. Not smooth enough and too subtle an effect. You can see this for yourself if you play the game and move left and right. This was a real time waster in the jam, I nudged the pixels by hand for 7 perspective angles, for a total of 448 frames. I didn&amp;rsquo;t expect it to take more than half an hour so didn&amp;rsquo;t really consider an automated solution. If I revisit this type of effect, scripting as much of the process as possible will be my first step.&lt;/p&gt;
&lt;h2&gt;Gameplay&lt;/h2&gt;
&lt;p&gt;A scrolling background does not a game make. The theme for the jam was &amp;lsquo;In a loop&amp;rsquo;, so my entry is a &amp;lsquo;shoot em up&amp;rsquo; or &amp;lsquo;STG&amp;rsquo; (ShooTing Game) where the play is stuck in a series of loops. Usually in STGs a viable strategy is to shoot a few enemies and dodge the rest. In this game, you must defeat every enemy that appears onscreen to progress. Missing one or more enemies causes the loop to repeat - and being destroyed by an enemy causes the player to &amp;lsquo;loop down&amp;rsquo; to the previous loop. It&amp;rsquo;s a simple mechanic and works well enough, but in the current implementation severely lacks variety. There&amp;rsquo;s only a single enemy type and only eight different enemy attack patterns. In order to allow for a gradual increase in difficulty, the enemies do not even fire at the player until more than halfway through the game&amp;rsquo;s 8 loop waves. But once they do, the difficulty climbs quite rapidly.&lt;/p&gt;
&lt;h2&gt;Firin mah&amp;rsquo; lazer&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Firing the laser in Deathloop 256" src="laser.png"&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m a big fan of &amp;lsquo;special attacks&amp;rsquo; in this genre of games. Choosing when to use your special weapon can add a lot of tactical depth to what might otherwise be a very shallow game. Time limited, my special laser is very overpowered and suffers some bugs, but I&amp;rsquo;m happy with how it looks. Unfortunately, when the lazer stretches the full screen height, it consumes all of the limited number of sprites the console is able to draw to the screen, but when it works correctly, it looks cool!&lt;/p&gt;
&lt;h2&gt;Assembly&lt;/h2&gt;
&lt;p&gt;I wrote this game using only assembly, the lowest level human readable representation of machine code. I really enjoy working at the bare metal level and giving the machine direct instructions, rather than writing in a higher level language and hoping the compiler produces something close enough to what I want. It&amp;rsquo;s not the fastest way to develop a game, but I&amp;rsquo;ve done a few NES assembly jam entries now and am pretty happy with how much I&amp;rsquo;m able to achieve in a short timespan. I could have produced a more fully featured game using a high level language - but would it have run on a console from 1983? &lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://voxel.itch.io/nes-deathloop-256"&gt;You can play Deathloop-256 in your browser or download it for use in an emulator here&lt;/a&gt;. It&amp;rsquo;s simple but quick to try out, and I hope you at least enjoy the cool scrolling background - I do :D.&lt;/p&gt;</content></entry><entry><title>Demonic Foundations, MSDOS puzzle/board game</title><link href="https://thomaspurnell.com/009_demonic_foundations/demonic_foundations.html" rel="alternate"/><published>2024-08-18T00:00:00+10:00</published><updated>2024-08-18T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-08-18:/009_demonic_foundations/demonic_foundations.html</id><summary type="html">Demonic Foundations, MSDOS puzzle/board game</summary><content type="html">&lt;h2&gt;Dos Game Jam&lt;/h2&gt;
&lt;p&gt;Last weekend I was feeling a bit despondent over not having made an entry for &lt;a href="https://itch.io/jam/dos-games-july-2024-jam"&gt;Dos Games July 2024 Jam&lt;/a&gt;, a periodically repeating jam with a nice community. The project I&amp;rsquo;d started for the jam had fizzled out, equally disintersting to develop and to play. So started drawing the game I&amp;rsquo;d like to make for the jam &amp;lsquo;if I had time&amp;rsquo;. A few hours later I had this mockup:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Demonic Foundations initial mockup" src="mockup.png"&gt;&lt;/p&gt;
&lt;p&gt;Someone once explained the boardgame of Santorini to me, and the mockup was my approximation of that concept, but set in some kind of pixellated hell. It&amp;rsquo;s a fairly simple concept - two players face off against eachother, each turn moving one demon, adding one tile to a stack. Demons can climb up to one step higher than their current position, and you win if your demon stands on top of a stack three tiles tall. Seems simple, enough so that I felt like I could manage to turn the game around within the week still remaining.&lt;/p&gt;
&lt;h2&gt;Development environment&lt;/h2&gt;
&lt;p&gt;I recently built a new desktop machine and so used it over my normal raspberry pi setup - but there&amp;rsquo;s nothing used during this jam that wouldn&amp;rsquo;t work perfectly well there. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Text editor: vim&lt;/li&gt;
&lt;li&gt;Graphics editor: krita (for sketching), aseprite&lt;/li&gt;
&lt;li&gt;Audio editor: audacity &lt;/li&gt;
&lt;li&gt;Test emulator: dosbox&lt;/li&gt;
&lt;li&gt;Cross compiler: i586-pc-msdosdjgpp-gcc&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Writing the game&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve written a few games for MSDOS both 16 and 32bit. Given that I wanted &amp;lsquo;nice graphics&amp;rsquo; this time, I didn&amp;rsquo;t choose to limit myself in terms of graphics hardware. The game requires VGA graphics, though targeting the limited palettes of CGA, EGA adapters would definitely increase the &amp;lsquo;dos-feel&amp;rsquo; of the game. I used the Allegro 4 library to handle all the low level details like initialising graphics modes, sound driver etc. Initially I was quite conservative with memory use and redrawing the screen - but once I decided I wanted a scrolling background I was having to redraw much of the screen every frame, so I just decided to redraw &lt;em&gt;everything&lt;/em&gt; every frame. This reduces performance on pre-pentium machines I&amp;rsquo;m sure - possibly into the realms of unacceptable framerates.&lt;/p&gt;
&lt;p&gt;But once I was committed to redrawing everything each frame, there was no reason to keep anything stationary. So I added &amp;lsquo;bounciness&amp;rsquo; to the stacks of tiles, animated the movement of the demons, and added a dynamic camera that scrolls the map a little as you move the mouse - which also allows me to make the playfield slightly too large to fit onto the screen.&lt;/p&gt;
&lt;video width="640" height="480" controls&gt;&lt;source src="gameplay.mp4"&gt;(Embedded video not supported here)&lt;/video&gt;

&lt;p&gt;Taking the time to configure a nice debugging setup wasn&amp;rsquo;t part of my schedule, instead I relied on the time honoured and ever hated method of literring problem areas with print statements to output important state information to the console. This bit me when it came time to writing the computer opponent. There&amp;rsquo;s still a couple of bugs in its behaviour that I haven&amp;rsquo;t taken the time to analyse - I&amp;rsquo;m fairly certain stepping through with a debugger would make short work of that. I&amp;rsquo;m not a fan of using gdb from the command line, I should take the time to learn one of the gui frontends for it. &lt;/p&gt;
&lt;h2&gt;Cut content&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;d originally planned on adding multiple computer personalities to play against, with differing strategies. The ingame portraits were designed with those in mind, and would definitely be near the top of my todo list if I return to expand upon this game later. A short &amp;lsquo;campaign&amp;rsquo; where you play against increasingly difficult opponents to reach the final boss of hell would be good too. Adding unique powers to each character is another option - I think some versions of Santorini do this as well - allowing for slightly asymetric play as each player has a different unique skill. &lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;You can download &lt;a href="https://voxel.itch.io/demonic-foundations"&gt;Demonic Foundations here&lt;/a&gt;. You&amp;rsquo;ll need a dos emulator, I didn&amp;rsquo;t take the time to make a web build that runs in the browser. People can download the game and play it on their own machines instead. I think it adds something to the DOS emulation experience, but it also reduces the number of people that will play my game - something I can live with.&lt;/p&gt;
&lt;p&gt;Doing a graphics-focussed jam was great. Drawing little pixellated demons and portraits was definitely the highlight for me - but losing to the AI for the first time was also a highlight. So proud of my stupid little machine demon. Let me know if you play it!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Little corner demons" src="corner_demons.png"&gt;&lt;/p&gt;</content></entry><entry><title>Quokka Wokka, a small GBA game</title><link href="https://thomaspurnell.com/008_quokka_wokka/quokka_wokka.html" rel="alternate"/><published>2024-08-04T00:00:00+10:00</published><updated>2024-08-04T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-08-04:/008_quokka_wokka/quokka_wokka.html</id><summary type="html">Quokka Wokka, a small GBA game</summary><content type="html">&lt;p&gt;Using the environment configured in the &lt;a href="/007_gba_raspi/gba_raspi.html"&gt;previous post&lt;/a&gt;, I&amp;rsquo;ve created a small game as an entry to the game boy advance gamejam &lt;a href="https://itch.io/jam/gbajam24"&gt;gbajam24&lt;/a&gt;, &lt;a href="https://voxel.itch.io/game-boy-advance-quokka-wokka"&gt;Quokka Wokka&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Quokka Wokka logo" src="quokka_logo.png"&gt;&lt;/p&gt;
&lt;p&gt;This was my first time writing a GBA game, and there was quite a lot to learn. I used the tonc library to do most of the heavy lifting, but I still needed to understand how to working with the GBA video memory, making sure data was in the right format and the right place in memory. I opted for a fairly simple, straightforward game to try and limit the development difficulty, not wanting to combine learning a new platform with trying to perform complex programming. So, Quokka Wokka is a 2D &amp;lsquo;single screen arcade platformer&amp;rsquo; where you play as a Quokka that needs to clear each screen, avoiding touching the enemies, throwing your babies as weapons, and eating as many leaves as possible.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Quokka throwing some babies" src="screenshot_0.png"&gt;&lt;/p&gt;
&lt;h2&gt;Gameplay&lt;/h2&gt;
&lt;p&gt;The goal is to clear each room of enemies. There are two ways to do this - throw your babies at an enemy to stun them, and then run into them to send them flying across the screen. When they land they&amp;rsquo;ll turn into delicious fruit. The other method is to collect all the small leaves on each map - which triggers the &amp;lsquo;Bonus Game&amp;rsquo; inspired by Rodland, turning all enemies into giant harmless leaves that can be touched to create bonus letter pickups - collect all six letters to get an extra life.&lt;/p&gt;
&lt;p&gt;The Quokka is an Australian marsupial largely known for being willing to sacrifice its children while fleeing from predators, physically dropping them as it runs away. I&amp;rsquo;m not sure that throwing one at a crab would stun it very effectively, but there&amp;rsquo;s &lt;em&gt;some&lt;/em&gt; basis in reality here. &lt;/p&gt;
&lt;h2&gt;Graphics&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Grafx2 image editor" src="grafx2.png"&gt;&lt;/p&gt;
&lt;p&gt;The graphics were drawn in Grafx2 and Aseprite. Aseprite is definitely a more fully featured editor, but Grafx2 is much snappier on limited hardware like my Raspberry Pi, as well as being much easier to install and get running. &lt;/p&gt;
&lt;p&gt;For simplicity, I kept most of the graphics in a single spritesheet. The GBA can hold more than this in graphics memory, especially when using &amp;lsquo;4bit&amp;rsquo; palettised graphics - but again to keep things simple for myself I used &amp;lsquo;8bit&amp;rsquo; colour support - all graphics take up twice as much memory but share a single large palette. If I were to develop this into a full game I&amp;rsquo;d start by switching to &amp;lsquo;4bit&amp;rsquo; graphics to allow for swapping palettes at runtime, and allowing for a greater variety of sprites to be visible onscreen at once.&lt;/p&gt;
&lt;h2&gt;Music&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Milkytracker music editor" src="milkytracker.png"&gt;&lt;/p&gt;
&lt;p&gt;I tried to find a musician to contribute music for the game, but the responses were from people that were unfamiliar with the formats the game would need. I can&amp;rsquo;t fault anyone, but I increasingly see musicians drop links to their portfolios in the discussion boards for popular gamejams, even when their music is produced entirely through a modern workflow incompatible with the target platform for the games. GBA music is bbest created as a &amp;lsquo;.mod&amp;rsquo; or similar format - small audio samples packaged with a midi like sequence of notes telling the machine when to play each sample, at what pitch and volume etc. It&amp;rsquo;s not trivial to go from a modern &amp;lsquo;Digital Audio Workstation&amp;rsquo; to the spartan limiations of a tracker. A musically talented family member did try, but it&amp;rsquo;s a lot to learn! So in the end I created the music using Milkytracker, and although it&amp;rsquo;s not a great soundtrack, it turned out better than I feared.&lt;/p&gt;
&lt;h2&gt;Programming&lt;/h2&gt;
&lt;p&gt;Writing in C feels like a holiday after being down in the 6502 assembly mines. Though it won&amp;rsquo;t net me the best possible performance for the machine, C gets pretty close, and the game is so simple that it&amp;rsquo;s not really a concern. If I wanted 3D, some very fast matrix maths or similar, then I&amp;rsquo;d look to peppering in some assembly. Libtonc is not just a library of useful functions, it&amp;rsquo;s also a well documented api with an extensive tutorial. I had to re-read some sections on how videoram is mapped and interpreted when things didn&amp;rsquo;t behave how I expected, and I had some interesting effects when I pushed the sprite hardware the wrong way. Definitely a library and workflow I&amp;rsquo;d like to use again :)&lt;/p&gt;
&lt;video width="320" height="240" controls&gt;&lt;source src="stress_test.mp4"&gt;(Embedded video not supported here)&lt;/video&gt;

&lt;h2&gt;Hardware&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Game boy advance and game cartridge" src="banner.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I grabbed some cheap &amp;lsquo;pirate carts&amp;rsquo; from aliexpress. It was a bit of a gamble as not every cart is compatible with my cartridge flasher (the device I use to copy my game from the development machine onto the cartridge, which can then be inserted into the game console). A &amp;lsquo;blank&amp;rsquo; cart sells for around $30, but these &amp;lsquo;game reproductions&amp;rsquo; only cost $4 each, and work great. The same can&amp;rsquo;t be said of my printer, which has decided to no longer output blue ink, causing the sticker label to be quite pink. I have modded this Game Boy Advance to use a backlit screen, which means I haven&amp;rsquo;t been able to check how readable the graphics are on the &amp;lsquo;unlit&amp;rsquo; display the original GBA shipped with. Hopefully someone will let me know if it&amp;rsquo;s unplayable. &lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Quokka fighting the boss" src="screenshot_3.png"&gt;&lt;/p&gt;
&lt;p&gt;Quokka Wokka is a short, easy game. As a &amp;lsquo;full game&amp;rsquo;, it would need more bosses, a lot more maps, and perhaps a few extra gameplay mechanics. I think it&amp;rsquo;s more suited to platforms a little older than the GBA, where less gameplay complexity is expected.&lt;/p&gt;
&lt;p&gt;I enjoyed making something simple while learning about the workings of the GBA. It&amp;rsquo;s a lot more powerful than I realised, and writing a game for it in C, rather than the assembly I work with for the original Game Boy and NES, made the process fast. &lt;/p&gt;</content></entry><entry><title>Developing GBA games on Raspberry Pi</title><link href="https://thomaspurnell.com/007_gba_raspi/gba_raspi.html" rel="alternate"/><published>2024-05-28T00:00:00+10:00</published><updated>2024-05-28T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-05-28:/007_gba_raspi/gba_raspi.html</id><summary type="html">Developing GBA games on Raspberry Pi</summary><content type="html">&lt;p&gt;This is a log of how I set up a GBA development environment on a raspberry pi. It&amp;rsquo;s not intended to teach game development, just the process of installing and configuring the tools so I can refer to it later.&lt;/p&gt;
&lt;h1&gt;&lt;a name="why"&gt;&lt;/a&gt;Why&lt;/h1&gt;
&lt;p&gt;At the time of writing, &lt;a href="https://itch.io/jam/gbajam24"&gt;&amp;lsquo;GBA JAM 24&amp;rsquo;&lt;/a&gt; is running. Entrants create a new game compatible with the handheld Game Boy Advance console from 2001. I&amp;rsquo;ve never made a game for this platform before, and I&amp;rsquo;d like to give it a try! Currently my only &amp;lsquo;desktop computer&amp;rsquo; is a raspberry pi 4, so that&amp;rsquo;s what I&amp;rsquo;ll be using. &lt;a href="/000_nes_raspi/nes_raspi.html"&gt;More information on my hardware setup is available here if you&amp;rsquo;re interested&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Game boy advance console" src="gba.jpg"&gt;&lt;/p&gt;
&lt;h1&gt;&lt;a name="devkitpro"&gt;&lt;/a&gt;DevKitPro&lt;/h1&gt;
&lt;p&gt;The very good people at &lt;a href="https://devkitpro.org"&gt;DevKitPro&lt;/a&gt; maintain a free toolchain of cross compilers and libraries targeting multiple platforms, including the Game Boy Advance. This handles most of the difficult configuration and setup for us, and reduces the scope of my own work to &amp;lsquo;making the game&amp;rsquo; rather than having to spend a long time creating special compilers and so on. They provide a fairly straightforward &lt;a href="https://devkitpro.org/wiki/Getting_Started"&gt;Getting started guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Though not available directly from the debian repositories that I&amp;rsquo;m using on my raspberry pi, devkitpro make their own debian compatible repository available. We just need to configure apt to use it.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# wget https://apt.devkitpro.org/install-devkitpro-pacman&lt;/span&gt;
&lt;span class="c1"&gt;# chmod +x ./install-devkitpro-pacman&lt;/span&gt;
&lt;span class="c1"&gt;# sudo ./install-devkitpro-pacman&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This configures the repository and installs devkitpro-pacman, the devkitpro package manager tool - which we can now use to automatically install everything we need for gba dev&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# sudo dkp-pacman -S gba-dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This installs gba compilers, libraries, examples, a debugger and probably more.&lt;/p&gt;
&lt;h1&gt;&lt;a name="helloworld"&gt;&lt;/a&gt;Hell(o) World&lt;/h1&gt;
&lt;p&gt;As a special treat we&amp;rsquo;re going to actually use the tools this time. In a new terminal window (to ensure the new devkitpro environment vars are set), create a folder for the project and add this &lt;a href="Makefile"&gt;Makefile&lt;/a&gt; (borrowed from the devkitpro examples). There&amp;rsquo;s quite a lot of configuration to the devkitpro environment and the Makefile handles it all on our behalf. Finally create a &lt;a href="main.c"&gt;c source file&lt;/a&gt;. &lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# mkdir hell_world&lt;/span&gt;
&lt;span class="c1"&gt;# cd hell_world&lt;/span&gt;
&lt;span class="c1"&gt;# vim main.c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Edit in your favourite editor:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;gba_console.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;gba_video.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;gba_interrupt.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;gba_systemcalls.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;irqInit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;irqEnable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IRQ_VBLANK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;consoleDemoInit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;iprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\x1b&lt;/span&gt;&lt;span class="s"&gt;[10;10HHell World!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;VBlankIntrWait&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, from within this project directory, run Make&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Make&lt;/span&gt;
main.c
linking&lt;span class="w"&gt; &lt;/span&gt;cartridge
built&lt;span class="w"&gt; &lt;/span&gt;...&lt;span class="w"&gt; &lt;/span&gt;hell_world.gba
ROM&lt;span class="w"&gt; &lt;/span&gt;fixed!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gives us &lt;a href="hell_world.gba"&gt;hell_world.gba&lt;/a&gt;, ready to run in an emulator or on a real Game Boy Advance if you have a method of putting it onto a cartridge.&lt;/p&gt;
&lt;p&gt;&lt;img alt="mesen.png" src="mesen.png"&gt;&lt;/p&gt;</content></entry><entry><title>Webgen blog generator</title><link href="https://thomaspurnell.com/006_Webgen/webgen.html" rel="alternate"/><published>2024-05-22T00:00:00+10:00</published><updated>2024-05-22T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-05-22:/006_Webgen/webgen.html</id><summary type="html">Webgen blog generator</summary><content type="html">&lt;h1&gt;Why&lt;/h1&gt;
&lt;p&gt;I have a tendency to &amp;lsquo;rebuild&amp;rsquo; my computing devices, sometimes the hardware, more often the software stack. A few times a year I&amp;rsquo;ll reinstall windows, linux, freebsd, depending on the mood and what I need to get done. This also involves reinstalling the various tools I need, graphics packages, editors and so on. &lt;/p&gt;
&lt;p&gt;Annoyingly, this also means rebuilding whatever tools I&amp;rsquo;m using for managing websites. In particular I find it frustrating to set up tooling for my own personal sites. Ideally publishing to the web shouldn&amp;rsquo;t involve anything more than the most basic text editor and a web browser, but there are usually reasons why this isn&amp;rsquo;t ideal. As soon as you want features like links between pages, tags, any kind of hierarchy or organisation, the workload multiplies. These are all possible manually, but better to be done quickly and accurately by the machines. Hopping around between processor architectures and operating systems, I run into compatibility problems. &amp;ldquo;This version of web publishing software isn&amp;rsquo;t available for your operating system. This version of &amp;lsquo;dependency&amp;rsquo; doesn&amp;rsquo;t work on your processor type&amp;rdquo; etc. So I decided to make a simple, portable site generator that will work everywhere I could possibly want to use it, and work fast. This last requirement is particularly important to me as I&amp;rsquo;m currently doing all my work from a very modestly powered raspberry pi &amp;lsquo;tiny computer&amp;rsquo;, where visual content editors tend to be a bit slow and unwieldy. &lt;/p&gt;
&lt;h1&gt;Webgen&lt;/h1&gt;
&lt;p&gt;I didn&amp;rsquo;t even waste time on coming up with a nice name. This is a &amp;lsquo;website generator&amp;rsquo;, webgen. I bashed it out over two evenings, sticking to the C programming language. C isn&amp;rsquo;t ideal for easy text processing,  but the results are fast and can be compiled on any almost device from the last 30 years (probably?). I&amp;rsquo;m quite sure it won&amp;rsquo;t work in a very ram restricted environment, but being simple and self-written, I can fix any problems I encounter in restricted future environments. There&amp;rsquo;s also nothing in the way of server-side requirements, no special executable code that needs to run on the server as with PHP, ASP, node, and nothing that has to run in the client browser such as javascript. Just C parsing markdown and generating HTML.&lt;/p&gt;
&lt;h1&gt;Including Markdown&lt;/h1&gt;
&lt;p&gt;I was previously using Publii to edit my site. Before that Hugo and Pelican. All of these supported markdown files, a simple way of annotating text that can be later converted to html. But annoyingly, if the author was unfortunate enough to misplace (or delete in anger) the original markdown documents, there was not a good way to turn the generated site pages back into markdown documents. For this reason, when Webgen publishes a website, &lt;a href="webgen.html"&gt;the original markdown files are included in the output&lt;/a&gt;. If i were to lose the local verion of my site data, or want to switch to some other site generator with markdown support, I would only need the content of my published site directory, which includes all the media and markdown files needed for any other generator to build a site. &lt;/p&gt;
&lt;h1&gt;Webgen: closed source??&lt;/h1&gt;
&lt;p&gt;Even though I&amp;rsquo;ve written Webgen to be &amp;lsquo;fast and scrappy&amp;rsquo; there are parts of the code that are honestly just a little too embarrassing to share. Perhaps I&amp;rsquo;ll clean it up and put it somewhere, but more likely not. It&amp;rsquo;s a tiny tool that does one job. This page (&lt;a href="/feed.xml"&gt;or feed!&lt;/a&gt;) that you&amp;rsquo;re reading was generated with it, at least originally, and if you&amp;rsquo;ve read this far then webgen has obviously worked well enough. Perhaps I could open source it under a &amp;lsquo;no feedback permitted&amp;rsquo; license ;)&lt;/p&gt;
&lt;p&gt;&lt;img alt="Webgen running inside vim" src="webgen_vim.png"&gt;&lt;/p&gt;</content></entry><entry><title>Baking unsigned 8bit vertex lighting in Blender 4</title><link href="https://thomaspurnell.com/005_Baking_blender4/baking.html" rel="alternate"/><published>2024-02-17T00:00:00+10:00</published><updated>2024-02-17T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-02-17:/005_Baking_blender4/baking.html</id><summary type="html">Baking unsigned 8bit vertex lighting in Blender 4</summary><content type="html">&lt;p&gt;Now this is some real niche material. I have been experimenting a little with Dreamcast era OpenGL again, and wanting to incorporate some lighting effects. Here&amp;rsquo;s a little unlit area from my prototype game &lt;a href="https://voxel.itch.io/dashy-blast"&gt;&amp;lsquo;Dashy No Blast&amp;rsquo;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the textured mesh without any lighting, shown in Blender.
&lt;img alt="unlit scene" src="window_unlit.png"&gt;&lt;/p&gt;
&lt;p&gt;By adding some lights to the scene and switching to &amp;lsquo;Rendered&amp;rsquo; view, we get something like this:
&lt;img alt="window blender lit" src="window_blender_lit.png"&gt;&lt;/p&gt;
&lt;p&gt;The goal is to now approximate this light and shadow by &amp;lsquo;baking&amp;rsquo; the light to the vertex colour of the scene. We need to add vertex colour attributes to each mesh that we want to light. &lt;/p&gt;
&lt;p&gt;&lt;img alt="blender data inspector" src="blender_data.png"&gt;&lt;/p&gt;
&lt;p&gt;Next, in the Render inspector tab, we configure Cycles to perform a Combined bake, using lighting and diffuse, with the output set to &amp;lsquo;Active Color Attribute&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="blender cycles config" src="blender_cycles_config.png"&gt;&lt;/p&gt;
&lt;p&gt;Then, by clicking &amp;lsquo;Bake&amp;rsquo;, the lighting will be calculated and baked to the vertex colours. To see this, we can configure viewport shading to colour the scene with &amp;lsquo;Attribute&amp;rsquo;, which shows the baked vertex lighting. Looks okay, but there is a hidden problem! &lt;/p&gt;
&lt;p&gt;&lt;img alt="vertex light view" src="vertex_light_view.png"&gt;&lt;/p&gt;
&lt;p&gt;When creating the colour attributes, I used the &amp;lsquo;Color&amp;rsquo; data type. For some reason, this type allows colour values outside of the normalised range 0.0 to 1.0. Note here in the Blender spreadsheet viewer that the selected vertex has a red value of 1.082.&lt;/p&gt;
&lt;p&gt;&lt;img alt="spreadsheet" src="spreadsheet.png"&gt;&lt;/p&gt;
&lt;p&gt;This causes problems later - my pipeline converts these values to 8 bit unsigned &amp;lsquo;byte&amp;rsquo; values, in the range 0 to 255, where an input value of 1.0 should result in an output of 255. An input of 1.082 results in an output of 20, because 1.082 * 255 = 275 point something, but an unsigned byte only holds a maximum value of 255 before it overflows back to zero. In practice this resulted in my illuminated orange window losing most of its red and looking like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="A scene with a green window" src="green_window.png"&gt;&lt;/p&gt;
&lt;p&gt;So we recreate or convert the colour attribute data, using the &amp;lsquo;Byte Color&amp;rsquo; instead of &amp;lsquo;Color&amp;rsquo; data type&lt;/p&gt;
&lt;p&gt;&lt;img alt="byte colour" src="byte_colour.png"&gt;&lt;/p&gt;
&lt;p&gt;Reinspecting one of my problem green vertices in the spreadsheet view, we now see that the red value is back in the valid range as 1.0.&lt;/p&gt;
&lt;p&gt;&lt;img alt="spreadsheet with valid values" src="spreadsheet_valid.png"&gt;&lt;/p&gt;
&lt;p&gt;This results in a more orange window when displayed ingame, as desired.&lt;/p&gt;
&lt;p&gt;&lt;img alt="orange window as wanted" src="orange_window.png"&gt;&lt;/p&gt;
&lt;p&gt;One final note: the ambient light level can be adjusted from the World inspector&amp;rsquo;s &amp;lsquo;Surface&amp;rsquo; dropdown. Remember to bake the light after changing this to see any difference.&lt;/p&gt;
&lt;p&gt;&lt;img alt="world ambient lighting adjustment" src="world_ambient.png"&gt;&lt;/p&gt;</content></entry><entry><title>Sketching</title><link href="https://thomaspurnell.com/004_sketching/sketching.html" rel="alternate"/><published>2024-02-07T00:00:00+10:00</published><updated>2024-02-07T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2024-02-07:/004_sketching/sketching.html</id><summary type="html">Sketching</summary><content type="html">&lt;p&gt;Since buying a small wacom drawing tablet, I&amp;rsquo;ve been using it to draw (shock!). Primarily I&amp;rsquo;ve been preparing to create art assets for a games project: box and cover art, promotional materials etc. But for fun, and to try and improve my drawing skills to the point where I&amp;rsquo;m able to illustrate to an acceptable level, I&amp;rsquo;ve been sketching.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Tachikoma sketch" src="tachikoma.png"&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a &amp;lsquo;Tachikoma&amp;rsquo; from the Ghost In The Shell series. It was a quick sketch (my go-to excuse for anything low quality). Composition issues aside, my main disappointment is the waviness of the lines. Drawing &amp;lsquo;smooth&amp;rsquo; lines is quite difficult with the drawing tablet, and I&amp;rsquo;m sure it&amp;rsquo;s mostly a user problem, rather than poor hardware. There are software options to help smooth out lines as you draw them, but they add different disadvantages, such as making the pen &amp;lsquo;lag&amp;rsquo; somewhat as the input is smoothed.
Illustration of a pig&lt;/p&gt;
&lt;p&gt;&lt;img alt="Piggo sketch" src="piggo.png"&gt;&lt;/p&gt;
&lt;p&gt;The problem is even more apparent here, especially on the Pig&amp;rsquo;s back. I wanted a continuous smooth curve, but after ten attempts or so I settled on this wonky attempt as &amp;lsquo;good enough&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Leo sketch" src="leo.png"&gt;&lt;/p&gt;
&lt;p&gt;This illustration of my son is one of the first things I drew after receiving the tablet. Only now do I notice that the line quality is even worse than newer attempts, so perhaps I&amp;rsquo;m improving after all.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Sleeping sketch" src="sleeping.png"&gt;&lt;/p&gt;
&lt;p&gt;Finally for comparison, a sketch from just before receiving the drawing tablet, instead drawn using a mouse. Pretty rough lines even though I used line smoothing settings in Krita. It also took me much longer to draw than the sketches I&amp;rsquo;ve done with the tablet. &lt;/p&gt;</content></entry><entry><title>Configuring a wacom graphics drawing tablet under Hyprland</title><link href="https://thomaspurnell.com/003_wacom_graphics_hyprland/wacom_hyprland.html" rel="alternate"/><published>2023-11-15T00:00:00+10:00</published><updated>2023-11-15T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2023-11-15:/003_wacom_graphics_hyprland/wacom_hyprland.html</id><summary type="html">Configuring a wacom graphics drawing tablet under Hyprland</summary><content type="html">&lt;p&gt;I bought a second hand wacom tablet to try a different way of drawing, but the default configuration was not what I wanted. This is a log of how I configured it to work under the Hyprland window manager in Linux.&lt;/p&gt;
&lt;h2&gt;The hardware&lt;/h2&gt;
&lt;p&gt;The tablet I bought was a used CTH-480, but I imagine the process is similar for anything compatible with the linux wacom driver. This particular tablet works with both a stylus and finger &amp;lsquo;touch&amp;rsquo; input, and can distinguish between these input methods. Also it&amp;rsquo;s a nice size for sitting between the two halves of my split keyboard.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Homemade split keyboard and drawing tablet on a desk" src="20231115_intuos.jpg"&gt;&lt;/p&gt;
&lt;h2&gt;Default configuration&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re using a single monitor, the tablet probably works fine without any additional configuration. Touching the top left of the tablet registers an input at the top left of the screen, and touching the bottom right of the tablet registers as the bottom right of the screen. However, if you have two or more monitors, then the tablet input will be scaled to match the total display space of your screens. So for me using a three monitor setup, the tablet input corresponded to three screens width, making any kind of precise control very difficult.&lt;/p&gt;
&lt;h2&gt;Trying an alternate driver: OpenTabletDriver&lt;/h2&gt;
&lt;p&gt;Briefly I experimented with an alternative to the wacom driver, &lt;a href="https://opentabletdriver.net"&gt;OpenTabletDriver&lt;/a&gt;. This had the advantage of having a straightforward gui to configure the various options, but without a lot of extra configuration didn&amp;rsquo;t detect whether I was using the stylus drawing-tip or the style eraser. Plus it uses dotnet which felt like overkill for an option that didn&amp;rsquo;t automagically solve all my problems.&lt;/p&gt;
&lt;h2&gt;Configuring Hyprland&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://wiki.hyprland.org/Configuring/Variables/#input"&gt;Hyprland documentation&lt;/a&gt; lists some options for configuring tablet input peripherals.
Instead, I want only the tablet stylus input to be mapped to a single screen, ignoring the extra space the other two provide. By setting the &amp;lsquo;output&amp;rsquo; variable to the name of one of the monitors, the &amp;lsquo;output space&amp;rsquo; of the tablet would be mapped to that display.&lt;/p&gt;
&lt;p&gt;So first we identify the monitors&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;hyprctl&lt;span class="w"&gt; &lt;/span&gt;monitors&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;Monitor
Monitor&lt;span class="w"&gt; &lt;/span&gt;DP-3&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;ID&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;:
Monitor&lt;span class="w"&gt; &lt;/span&gt;DVI-D-1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;ID&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;:
Monitor&lt;span class="w"&gt; &lt;/span&gt;HDMI-A-1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;ID&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Also the identifier for the tablet&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;hyprctl&lt;span class="w"&gt; &lt;/span&gt;devices
Tablets:
&lt;span class="w"&gt;    &lt;/span&gt;Tablet&lt;span class="w"&gt; &lt;/span&gt;Pad&lt;span class="w"&gt; &lt;/span&gt;at&lt;span class="w"&gt; &lt;/span&gt;544c238492d0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;belongs&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;Tablet&lt;span class="w"&gt; &lt;/span&gt;at&lt;span class="w"&gt; &lt;/span&gt;544c40ffcea0:
&lt;span class="w"&gt;        &lt;/span&gt;wacom-intuos-pt-s-pen
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In my case HDMI-A-1 is the monitor I want to use for drawing, so we edit the Hyprland config file &lt;code&gt;vim ~/.config/hypr/hyprland.conf&lt;/code&gt;, adding the following per-device input configuration:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;wacom&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;intuos&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HDMI&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Edit 2025-05-07:
Since I last configured Hyprland, the configuration format for devices seems to have changed. The device name is now a property of the device and the equivalent declaration is now:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;device {
    name=wacom-intuos-pt-s-pen
    output=HDMI-A-1
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This configuration takes effect immediately upon &lt;code&gt;hyprland.conf&lt;/code&gt; being saved.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Krita art package screenshot" src="20231115_krita.jpg"&gt;&lt;/p&gt;</content></entry><entry><title>Setting up a Game Boy development environment on a Raspberry Pi</title><link href="https://thomaspurnell.com/002_gameboy_raspi/gameboy_raspi.html" rel="alternate"/><published>2023-03-22T00:00:00+10:00</published><updated>2023-03-22T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2023-03-22:/002_gameboy_raspi/gameboy_raspi.html</id><summary type="html">Setting up a Game Boy development environment on a Raspberry Pi</summary><content type="html">&lt;p&gt;This is a log of how I set up a Game Boy development environment on a raspberry pi. It&amp;rsquo;s not intended to teach you Z80 assembly, game development, or anything else.&lt;/p&gt;
&lt;h1&gt;Why&lt;/h1&gt;
&lt;p&gt;The Game Boy was a popular handheld gaming machine and there are many functional devices still in circulation, not to mention the legions of clones and emulators capable of playing the Game Boy library. Even modest hardware can emulate the Game Boy and gain access to thousands of games, many of which are still enjoyable even today. This results in an enduring audience of players and developers, so making the platform a viable target for publishing games to even in 2023.&lt;/p&gt;
&lt;p&gt;Developing for the Game Boy possibly requires even less capable hardware than playing the games, but probably any device capable of viewing this blog post is vastly more powerful than is required to develop for this platform.&lt;/p&gt;
&lt;h2&gt;The hardware&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m using a Raspberry Pi 4 with 8GB of RAM as my development machine, running &amp;lsquo;RaspberryPiOS&amp;rsquo;, which is Debian Linux. I&amp;rsquo;ll be deploying games to a real Game Boy via a cartridge writing system and compatible cartridges.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Game Boy console and some game cartridges" src="20230321_gameboy.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;I also have a Game Boy Color to hand, and developing for either the colour or original monochrome Game Boy is largely an identical process. Both use a custom Z80 family microprocessor, and many Z80 assemblers can output compatible machine code.&lt;/p&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;p&gt;To make Game Boy games, the following is needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A text editor, to write the code,&lt;/li&gt;
&lt;li&gt;An assembler, to convert Z80 assembly to machine instructions,&lt;/li&gt;
&lt;li&gt;A graphics editor, to draw the visual elements of the game. This can be done on paper and bytecode manually calculated, but I&amp;rsquo;d rather have the computer do that for me.&lt;/li&gt;
&lt;li&gt;An audio editor, for creating sound effects and music. Again this can be done manually but it&amp;rsquo;s a lot easier with some good tools.&lt;/li&gt;
&lt;li&gt;An emulator to test the game, preferably with a good debugger.&lt;/li&gt;
&lt;li&gt;Flashing software to write the games to a physical cartridge.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Text editor&lt;/h3&gt;
&lt;p&gt;You can use whatever editor you like, but I&amp;rsquo;m using vim. VSCODE (or its free and open source alternative &amp;lsquo;CODE&amp;rsquo;) are also great choices that I&amp;rsquo;ve experimented with. Z80 Syntax highlighting is probably available for many programming focused editors.&lt;/p&gt;
&lt;h3&gt;Assembler and toolchain&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://rgbds.gbdev.io/"&gt;RGBDS suite&lt;/a&gt; provides a complete toolchain for assembling Game Boy roms, as well as processing common formats of graphics files into something usable on the game console.&lt;/p&gt;
&lt;p&gt;RGBDS isn&amp;rsquo;t available in the Debian aarch64 repositories, so I&amp;rsquo;m building it from source.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/gbdev/rgbds
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rgbds
$&lt;span class="w"&gt; &lt;/span&gt;make
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Graphics Editor&lt;/h3&gt;
&lt;p&gt;Aseprite or other popular modern pixel focused graphics editors can be made to work on a Raspberry Pi, but I&amp;rsquo;ve decided to go with &lt;a href="http://grafx2.chez.com/"&gt;Grafx2&lt;/a&gt;, a free and open source editor inspired by Deluxe Paint, which is the first &amp;lsquo;serious&amp;rsquo; graphics program I ever used. Any tool that can work with indexed graphics formats (where the image file contains a limited palette that all pixels index into) should work.&lt;/p&gt;
&lt;p&gt;Grafx2 is available in the Debian repositories, but I built from source for a more up to date version.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;libsdl2-dev&lt;span class="w"&gt; &lt;/span&gt;libsdl2-image-dev&lt;span class="w"&gt; &lt;/span&gt;libsdl2-ttf-dev
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://gitlab.com/GrafX2/grafX2
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grafx2/src
$&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;release&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sdl2
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sdl2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Grafx2 editing a Game Boy tileset" src="20230321_grafx2.png"&gt;&lt;/p&gt;
&lt;h3&gt;Audio Editor&lt;/h3&gt;
&lt;p&gt;In the past I&amp;rsquo;ve used the nice Carillion Game Boy music/sfx editor and driver, which is fun because you&amp;rsquo;re making the music and sfx on an actual game boy, so you get an accurate representation of how things will sound. It&amp;rsquo;s not the quickest workflow however, and I&amp;rsquo;m going to try hUGETracker for this setup, which will let me compose on the raspberry pi.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;lazarus
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;--recursive&lt;span class="w"&gt; &lt;/span&gt;https://github.com/SuperDisk/hUGETracker
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;hUGETracker
$&lt;span class="w"&gt; &lt;/span&gt;lazbuild&lt;span class="w"&gt; &lt;/span&gt;--add-package-link&lt;span class="w"&gt; &lt;/span&gt;src/rackctls/RackCtlsPkg.lpk
$&lt;span class="w"&gt; &lt;/span&gt;lazbuild&lt;span class="w"&gt; &lt;/span&gt;--add-package-link&lt;span class="w"&gt; &lt;/span&gt;src/bgrabitmap/bgrabitmap/bgrabitmappack.lpk
$&lt;span class="w"&gt; &lt;/span&gt;./setup-linux.sh
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;src
$&lt;span class="w"&gt; &lt;/span&gt;lazbuild&lt;span class="w"&gt; &lt;/span&gt;hUGETracker.lpi&lt;span class="w"&gt; &lt;/span&gt;--build-mode&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Production Linux&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As of 2023-03-22 this threw some errors about &lt;code&gt;DisabledFontColor&lt;/code&gt; being undefined in tracker.pas, so I commented out lines 1178 and 1188 in tracker.pas and rebuilt without issue.&lt;/p&gt;
&lt;p&gt;hUGETracker will error out if you try and launch it without its expected collection of bundled files in the right places, so next download the x86 version and unzip it somewhere.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Downloads
$&lt;span class="w"&gt; &lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;https://github.com/SuperDisk/hUGETracker/releases/download/v1.0.0/hUGETracker-1.0.0-linux.zip
$&lt;span class="w"&gt; &lt;/span&gt;unzip&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;hUGETracker&lt;span class="w"&gt; &lt;/span&gt;hUGETracker-1.0.0-linux.zip
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally copy our freshly built hUGETracker to the downloaded release, overwriting the incompatible x86 executable&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;~/Src/External/hUGETracker/src/Release/hUGETracker&lt;span class="w"&gt; &lt;/span&gt;~/Downloads/hUGETracker/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally we can run our aarch64 hUGETracker build.&lt;/p&gt;
&lt;p&gt;&lt;img alt="hUGETracker running" src="20230321_hUGETracker.png"&gt;&lt;/p&gt;
&lt;h3&gt;Emulator&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m using Mesen2, which I set up previously for the &lt;a href="/developing-nes-games-on-raspberry-pi/index.html"&gt;Raspberry Pi previously for NES development&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Building a test ROM&lt;/h2&gt;
&lt;p&gt;Now that we have the tools, let&amp;rsquo;s make some salad. Grab the &lt;a href="https://raw.githubusercontent.com/gbdev/hardware.inc/master/hardware.inc"&gt;GB standard definitions file&lt;/a&gt; and write your Game Boy code, this example borrowed from &lt;a href="daid.github.io/rgbds-live"&gt;https://daid.github.io/rgbds-live&lt;/a&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Simple&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;graphics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VRAM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;applicable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;initial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;disables&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LCD&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;full&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;access&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VRAM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;will&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;make&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;white&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="k"&gt;INCLUDE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;hardware.inc&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;SECTION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;graphics&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ROM0&lt;/span&gt;
&lt;span class="nl"&gt;graphicTiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Graphics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;below&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;contains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;same&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="n"&gt;x8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;graphics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;different&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Prefered&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;way&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;would&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INCBIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rgbgfx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currently&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;possible&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rgbds&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;live&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="mf"&gt;.123&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mf"&gt;.111111&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mi"&gt;11111111&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mf"&gt;1.111.11&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mf"&gt;1.111.11&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mi"&gt;11111111&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mf"&gt;1.1331.1&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mf"&gt;1..11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;.1&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;dw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="mf"&gt;.111111&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;

&lt;span class="k"&gt;SECTION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;entry&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ROM0&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;$100&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;jp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;

&lt;span class="k"&gt;SECTION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;main&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ROM0&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;$150&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;disableLCD&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loadTiles&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;loadPalette&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;call&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enableLCD&lt;/span&gt;

&lt;span class="nl"&gt;haltLoop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;halt&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;haltLoop&lt;/span&gt;

&lt;span class="nl"&gt;disableLCD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Disable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LCD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;needs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;happen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VBlank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;we&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hardware&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nl"&gt;waitForVBlank&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rLY&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;144&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;jr&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waitForVBlank&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;xor&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rLCDC&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;disable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LCD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LCDC&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;

&lt;span class="nl"&gt;loadPalette&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;11100100&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rBGP&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;

&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;graphics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tiles&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VRAM&lt;/span&gt;
&lt;span class="nl"&gt;loadTiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;graphicTiles&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;de&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;graphicTiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;graphicTiles&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;We&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;de&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_VRAM&lt;/span&gt;

&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nl"&gt;copyTilesLoop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VRAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;increase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;both&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;hl+&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Decrease&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;we&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;still&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;left&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;dec&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;de&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ow"&gt;or&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;jp&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;nz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copyTilesLoop&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;

&lt;span class="nl"&gt;enableLCD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LCDCF_BGON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LCDCF_BG8000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LCDCF_ON&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ldh&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rLCDC&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can then build a ROM&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;rgbasm&lt;span class="w"&gt; &lt;/span&gt;main.asm&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;main.o
$&lt;span class="w"&gt; &lt;/span&gt;rgblink&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;game.gb&lt;span class="w"&gt; &lt;/span&gt;main.o
$&lt;span class="w"&gt; &lt;/span&gt;rgbfix&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;game.gb&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;0xff
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And run it in an emulator for debugging!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Test rom running in Mesen2 with debuggers" src="20230321_mesen2.png"&gt;&lt;/p&gt;
&lt;h3&gt;Flashing the ROM to a cartridge&lt;/h3&gt;
&lt;p&gt;This will differ wildly depending on your flash hardware. Many &amp;lsquo;flash&amp;rsquo; carts have an SD card slot or USB socket to allow you to easily copy files onto it - but I&amp;rsquo;m doing something slightly different and making a &amp;lsquo;one game&amp;rsquo; cartridge using a dedicated cart flasher named &amp;lsquo;Joey Joebags&amp;rsquo; by BennVenn, which comes with its own python application to interface with the flasher.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Joey Joebags UI window" src="20230321_joey.png"&gt;&lt;/p&gt;
&lt;p&gt;The cartridge is plugged into the flasher, which is in turn connected to the Raspberry Pi via a USB cable.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Game Boy cartridge flasher connected to a Raspberry Pi" src="20230322_joeyjoebags.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Such a tiny rom takes only a second or two to write to the cart, which can then be inserted into a Game Boy and played like any other game.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Game Boy running the test ROM" src="20230322_gameboy.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Effective Game Boy photography is left as an exercise for the reader.&lt;/p&gt;</content></entry><entry><title>Setting up a Dreamcast development environment on a Raspberry Pi</title><link href="https://thomaspurnell.com/001_dreamcast_raspi/dreamcast_raspi.html" rel="alternate"/><published>2023-03-17T00:00:00+10:00</published><updated>2023-03-17T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2023-03-17:/001_dreamcast_raspi/dreamcast_raspi.html</id><summary type="html">Setting up a Dreamcast development environment on a Raspberry Pi</summary><content type="html">&lt;p&gt;This is a guide on one way of setting up a modern development system for the 1999 Dreamcast console. The easiest method for Windows users is probably &lt;a href="https://dreamsdk.org"&gt;DreamSDK&lt;/a&gt;, which provides a slick automated install of preconfigured tools to get you up and running quickly, but that&amp;rsquo;s not what I&amp;rsquo;ll be covering here.&lt;/p&gt;
&lt;h2&gt;The hardware&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m using a Raspberry Pi 4 with 8GB of RAM, running &amp;lsquo;RaspberryPiOS&amp;rsquo; (Debian) Linux, and a VA-0 Dreamcast equipped with a HIT-0400 Broadband Adapter.&lt;/p&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;p&gt;To make Dreamcasct software, the following is needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A cross compilation toolchain to convert the program code to machine instructions understood by the Dreamcast CPU,&lt;/li&gt;
&lt;li&gt;KallistiOS, a library providing an interface to the various Dreamcast hardware components,&lt;/li&gt;
&lt;li&gt;A text editor, to write the code,&lt;/li&gt;
&lt;li&gt;Graphics editors&lt;/li&gt;
&lt;li&gt;Audio editors&lt;/li&gt;
&lt;li&gt;Optionally an emulator to test the software on the development machine, preferably with a good debugger.&lt;/li&gt;
&lt;li&gt;dcload-ip - a tool that boots a real Dreamcast and allows for code to be deployed to it over the network, making it possible to test and debug software without writing each iteration to a cd-r.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cross compilation toolchain&lt;/h3&gt;
&lt;p&gt;Unless you&amp;rsquo;re typing code directly into a Dreamcast, you probably need a cross-compilation toolchain that can produce machine code for the SH-4 &amp;lsquo;SuperH&amp;rsquo; CPU that it uses.&lt;/p&gt;
&lt;p&gt;KallistiOS provides a helper script to produce such a toolchain.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$ git clone https://github.com/KallistiOS/KallistiOS/ /opt/toolchains/dc/kos&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Documentation is found in the KallistiOS/doc directory, though some aspects are out of date.&lt;/p&gt;
&lt;p&gt;Building the toolchain:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;libc6-dev-sh4-cross&lt;span class="w"&gt; &lt;/span&gt;gawk&lt;span class="w"&gt; &lt;/span&gt;patch&lt;span class="w"&gt; &lt;/span&gt;bzip2&lt;span class="w"&gt; &lt;/span&gt;tar&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;libgmp-dev&lt;span class="w"&gt; &lt;/span&gt;libmpfr-dev&lt;span class="w"&gt; &lt;/span&gt;libmpc-dev&lt;span class="w"&gt; &lt;/span&gt;gettext&lt;span class="w"&gt; &lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;libelf-dev&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
texinfo&lt;span class="w"&gt; &lt;/span&gt;bison&lt;span class="w"&gt; &lt;/span&gt;flex&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;build-essential&lt;span class="w"&gt; &lt;/span&gt;diffutils&lt;span class="w"&gt; &lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;libjpeg-dev&lt;span class="w"&gt; &lt;/span&gt;libpng-dev&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;pkg-config&lt;span class="w"&gt; &lt;/span&gt;libisofs-dev&lt;span class="w"&gt; &lt;/span&gt;meson&lt;span class="w"&gt; &lt;/span&gt;ninja-build
$&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;doc/environ_sh.sample&lt;span class="w"&gt; &lt;/span&gt;./environ_sh
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;utils/dc-chain
$&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;config.mk.testing.sample&lt;span class="w"&gt; &lt;/span&gt;config.mk
$&lt;span class="w"&gt; &lt;/span&gt;./download.sh
***&lt;span class="w"&gt; &lt;/span&gt;Downloader&lt;span class="w"&gt; &lt;/span&gt;Utility&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Sega&lt;span class="w"&gt; &lt;/span&gt;Dreamcast&lt;span class="w"&gt; &lt;/span&gt;Toolchains&lt;span class="w"&gt; &lt;/span&gt;Maker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;dc-chain&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;***
Downloading&lt;span class="w"&gt; &lt;/span&gt;Binutils&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;.40...
Downloading&lt;span class="w"&gt; &lt;/span&gt;GCC&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;.2.0...
Downloading&lt;span class="w"&gt; &lt;/span&gt;Newlib&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;.3.0.20230120...
Downloading&lt;span class="w"&gt; &lt;/span&gt;GCC&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;.4.0...
Downloading&lt;span class="w"&gt; &lt;/span&gt;config.guess...
Done!
$&lt;span class="w"&gt; &lt;/span&gt;./unpack.sh
***&lt;span class="w"&gt; &lt;/span&gt;Unpacker&lt;span class="w"&gt; &lt;/span&gt;Utility&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Sega&lt;span class="w"&gt; &lt;/span&gt;Dreamcast&lt;span class="w"&gt; &lt;/span&gt;Toolchains&lt;span class="w"&gt; &lt;/span&gt;Maker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;dc-chain&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;***
Preparing&lt;span class="w"&gt; &lt;/span&gt;unpacking...
Unpacking&lt;span class="w"&gt; &lt;/span&gt;Binutils&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;.40...
Unpacking&lt;span class="w"&gt; &lt;/span&gt;GCC&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;.2.0...
&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
Done!
$&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;-j5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Take a coffee break or three while this builds. On a raspberry pi 4 this took around five hours.&lt;/p&gt;
&lt;p&gt;Next I want gdb for remote debugging support&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;gdb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After a much shorter but not insignificant wait, the cross build toolchain is ready. Unneeded downloads can now be automatically deleted with &lt;code&gt;$ ./cleanup.sh&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;KallistiOS&lt;/h3&gt;
&lt;p&gt;From the kos root dir (/opt/toolchains/dc/kos)&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;environ_sh
$&lt;span class="w"&gt; &lt;/span&gt;make
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which results in lib/dreamcast/libkallisti.a&lt;/p&gt;
&lt;p&gt;This is all we need to build Dreamcast executables, but more steps are needed to build them in a useful way that can be run directly on the console, and we&amp;rsquo;ll also want some libraries to do helpful things like load image files and render 3D environments.&lt;/p&gt;
&lt;h3&gt;Kos-Ports&lt;/h3&gt;
&lt;p&gt;Kos-Ports is a collection of libraries that might be useful, ported to KallistiOS&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/opt/toolchains/dc
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/KallistiOS/kos-ports
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each can be built by entering its subdirectory and performing a &lt;code&gt;make install&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;GLdc&lt;/h3&gt;
&lt;p&gt;If you want to use OpenGL for writing Dreamcast graphical software like I do, you will probably want to remove the kos-ports libGL and replace it with the much newer &lt;a href="https://gitlab.com/simulant/GLdc"&gt;GLdc&lt;/a&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/opt/toolchains/dc/kos-ports
$&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;libGL
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://gitlab.com/simulant/GLdc
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;GLdc
$&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;dcbuild
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dcbuild
$&lt;span class="w"&gt; &lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_TOOLCHAIN_FILE&lt;span class="o"&gt;=&lt;/span&gt;../toolchains/Dreamcast.cmake&lt;span class="w"&gt; &lt;/span&gt;-G&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unix Makefiles&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;..
$&lt;span class="w"&gt; &lt;/span&gt;make
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The cmake invocation differs from the official guide at &lt;a href="https://gitlab.com/simulant/GLdc"&gt;https://gitlab.com/simulant/GLdc&lt;/a&gt; in that it produces the higher performance release version. Omit &lt;code&gt;-DCMAKE_BUILD_TYPE=Release&lt;/code&gt; if that&amp;rsquo;s not what you want.&lt;/p&gt;
&lt;h3&gt;Ready built environment&lt;/h3&gt;
&lt;p&gt;An archive of the environment built as described up to this point is &lt;a href="kos_aarch64_20230317.tar.xz"&gt;available here&lt;/a&gt;. It&amp;rsquo;s only useful if you&amp;rsquo;re on aarch64 linux.&lt;/p&gt;
&lt;h3&gt;To be continued&lt;/h3&gt;
&lt;p&gt;Want to know how to actually use this environment to build a dreamcast game? Better remind me to write the followup article!&lt;/p&gt;</content></entry><entry><title>Developing NES games on Raspberry Pi</title><link href="https://thomaspurnell.com/000_nes_raspi/nes_raspi.html" rel="alternate"/><published>2023-03-04T00:00:00+10:00</published><updated>2023-03-04T00:00:00+10:00</updated><author><name>Tom 'voxel' Purnell</name></author><id>tag:thomaspurnell.com,2023-03-04:/000_nes_raspi/nes_raspi.html</id><summary type="html">Developing NES games on Raspberry Pi</summary><content type="html">&lt;p&gt;This is a log of how I set up a NES development environment on a raspberry pi. It&amp;rsquo;s not intended to teach you 6502 assembly, game development, or anything else besides perhaps being an endurance workout for attention spans.&lt;/p&gt;
&lt;h2&gt;Table of contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#why"&gt;Why&lt;/a&gt; I did this&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-hardware"&gt;Hardware&lt;/a&gt; used&lt;/li&gt;
&lt;li&gt;&lt;a href="#software"&gt;Software&lt;/a&gt; setup&lt;/li&gt;
&lt;li&gt;&lt;a href="#text-editor"&gt;Text editor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#graphics-editor"&gt;Graphics editor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#detour-installing-wine-with-box64-and-box86"&gt;Emulating x86 on aarch64 with box&lt;/a&gt; to allow non native apps to run&lt;/li&gt;
&lt;li&gt;&lt;a href="#audio-editor"&gt;Audio editor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#emulator"&gt;Emulator&lt;/a&gt;                                                                                                                                                                                         &lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;a name="why"&gt;&lt;/a&gt;Why&lt;/h1&gt;
&lt;p&gt;It has been a hot summer here in Queensland, Australia. My normal desktop computer only adds to the temperature, converting kilowatts into heat. I decided to see what I could achieve on a smaller, single-board computer as a way to reduce the amount of power used and heat produced, but also just to see how much of a retro videogame studio I could squeeze onto one single board computer.&lt;/p&gt;
&lt;h2&gt;&lt;a name="the-hardware"&gt;&lt;/a&gt;The hardware&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m using a Raspberry Pi 4 with 8GB of RAM. This is overkill for the NES setup described here, but is useful for other development work. Also it was the first model that became available, there&amp;rsquo;s a global shortage of single board computers at the time of writing.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Raspberry Pi 4 board beside a measuring rule" src="20230304_raspberrypi_board-3.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Because this is going to be sitting on my desk, it needs a protective case. The bundle I bought came with an &amp;lsquo;Argon One&amp;rsquo; case, which also upgrades the mini-hdmi ports to full size hdmi via a daughterboard that plugs into the Pi.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Raspberry Pi 4 board plugged into hdmi daughterboard" src="20230304_raspberrypi_and_daughterboard.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I also grabbed a USB switcher to let me share my mouse and keyboard between the Pi and my desktop, for personal laziness and to reduce wear on the Pi USB ports.                                                &lt;br&gt;
&lt;img alt="Raspberry Pi 4 in an Argon One case, connected to a USB input switcher" src="20230304_raspberrypi_and_usbswitcher.jpg"&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a name="operating-system"&gt;&lt;/a&gt;Operating system&lt;/h2&gt;
&lt;p&gt;Nothing unusual here, I installed the official 64bit version of &amp;lsquo;RasperryPi OS&amp;rsquo;, which is Debian with some tweaks specific for the Raspberry Pi. The entire OS is written to a 32GB microSD card that slots into the underside of the Raspi case, and runs directly from there. This is the weakest part of the setup: microSDs are generally much slower than &amp;lsquo;real&amp;rsquo; harddrives, and are quite susceptible to corruption if the Raspi is powered down unexpectedly, during a powercut or a child flicking the power switch.&lt;/p&gt;
&lt;p&gt;Aside from some slowness if trying to browse javascript or media heavy websites like instagram or youtube, the Pi with 64bit Debian is pretty snappy. By default it looks like this:                              &lt;/p&gt;
&lt;p&gt;&lt;img alt="Raspberry Pi default desktop appearance" src="20230304_default_desktop.jpg"&gt;&lt;/p&gt;
&lt;p&gt;But after some fussing and deciding to try using a more graphical setup than usual, I have the subjectively more handsome setup below:                                                                            &lt;/p&gt;
&lt;p&gt;&lt;img alt="Riced up handsomeman desktop environment" src="20230304_riced_desktop.jpg"&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a name="software"&gt;&lt;/a&gt;Software&lt;/h2&gt;
&lt;p&gt;To make NES games, the following is needed:                                                                                                                                                                     &lt;br&gt;
&lt;em&gt; A text editor, to write the code,
* An assembler, to convert 6502 assembly to machine instructions,
* A graphics editor, to draw the visual elements of the game. This can be done on paper and bytecode manually calculated, but I&amp;rsquo;d rather have the computer do that for me.                                      &lt;br&gt;
&lt;/em&gt; An audio editor, for creating sound effects and music. Again this can be done manually but it&amp;rsquo;s a lot easier with some good tools.
* An emulator to test the game, preferably with a good debugger.&lt;/p&gt;
&lt;h3&gt;&lt;a name="text-editor"&gt;&lt;/a&gt;Text editor&lt;/h3&gt;
&lt;p&gt;You can use whatever editor you like, but I&amp;rsquo;m using vim. VSCODE (or its free and open source alternative &amp;lsquo;CODE&amp;rsquo;) are also great choices that I&amp;rsquo;ve experimented with. 6502 Syntax highlighting is probably available for many programming focused editors.
&lt;img alt="Vim, everyones favourite text editor and escape room challenge" src="20230304_vim.jpg"&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a name="assembler"&gt;&lt;/a&gt;Assembler&lt;/h3&gt;
&lt;p&gt;The cc65 suite does everything I want and is actively maintained. I&amp;rsquo;m not sure there&amp;rsquo;s a great deal of difference between the assemblers beyond some syntax differences. cc65 is available directly from the Debian repositories for aarch64 platform that the Raspberry Pi uses in 64bit mode, so no problems here.&lt;/p&gt;
&lt;h3&gt;&lt;a name="graphics-editor"&gt;&lt;/a&gt;Graphics Editor&lt;/h3&gt;
&lt;p&gt;This is where things start to get a little more tricky. It&amp;rsquo;s possible to use any art package, photoshop, aseprite, grafx2, even MS PAINT.EXE and run the output through a converter to NES CHR (character graphics) data, and this probably path I&amp;rsquo;d need to take if I were working with an artist unfamiliar with NES technical issues. But things change in the conversion process and you might not have as much control as you&amp;rsquo;d like, so we can make life easier by using a dedicated NES graphics editing tool. At the time of writing, &lt;a href="https://frankengraphics.itch.io/nexxt"&gt;frankengfx&amp;rsquo;s NEXXT&lt;/a&gt; is version 0.22.0 but already includes more functionality than I could need. It lets you draw and edit tiles, place those tiles into tilemaps (i.e to draw part of a level or a title screen), all while seeing how the graphics will actually appear on the NES when palette limitations apply.&lt;/p&gt;
&lt;p&gt;&lt;img alt="NEXXT graphics editor showing a game in progress" src="20230304_nexxt.png"&gt;&lt;/p&gt;
&lt;p&gt;The only problem is that is windows only. &amp;ldquo;But you can just run it through WINE!&amp;rdquo; I hear you cry. No child, it is not so simple. Wine is not an emulator, it can run windows applications outside of windows, but it can&amp;rsquo;t convert x86 to ARM aarch64. You could run aarch64 windows applications through wine (if any exist?), but not nexxt, which is a 32bit x86 app. So why am I talking about nexxt still? Well, it turns out that with some help wine &lt;em&gt;can&lt;/em&gt; run nexxt.&lt;/p&gt;
&lt;h4&gt;&lt;a name="detour-installing-wine-with-box64-and-box86"&gt;&lt;/a&gt;Detour: installing wine with box64 and box86&lt;/h4&gt;
&lt;p&gt;Unlike wine, box64 and box86 &lt;em&gt;are&lt;/em&gt; emulators. They can run x64 and x86 linux programs on non x86 linux systems, respectively. &lt;a href="https://ptitseb.github.io/box86/X86WINE.html"&gt;Following this guide carefully&lt;/a&gt; will result in a special wine configuration that can run nexxt and let us draw all the little NES pixels we like on a 64bit raspberry pi!&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Lifted directly from https://ptitseb.github.io/box86/X86WINE.html, best to check there for updates&lt;/span&gt;
&lt;span class="c1"&gt;# Backup any old wine installations&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;~/wine&lt;span class="w"&gt; &lt;/span&gt;~/wine-old
sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;~/.wine&lt;span class="w"&gt; &lt;/span&gt;~/.wine-old
sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wine&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wine-old
sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wineboot&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wineboot-old
sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/winecfg&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/winecfg-old
sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wineserver&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wineserver-old
&lt;span class="c1"&gt;# Download, extract wine, and install wine (last I checked, the Twister OS FAQ page had Wine 5.13-devel)&lt;/span&gt;
wget&lt;span class="w"&gt; &lt;/span&gt;https://twisteros.com/wine.tgz&lt;span class="w"&gt; &lt;/span&gt;-O&lt;span class="w"&gt; &lt;/span&gt;~/wine.tgz
tar&lt;span class="w"&gt; &lt;/span&gt;-xzvf&lt;span class="w"&gt; &lt;/span&gt;~/wine.tgz
rm&lt;span class="w"&gt; &lt;/span&gt;~/wine.tgz&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# clean up&lt;/span&gt;

&lt;span class="c1"&gt;# Install shortcuts (make launcher &amp;amp; symlinks. Credits: grayduck, Botspot)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#!/bin/bash\nsetarch linux32 -L &amp;#39;&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/wine/bin/wine &amp;quot;&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;$#&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wine&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Create a script to launch wine programs as 32bit only&lt;/span&gt;
&lt;span class="c1"&gt;#sudo ln -s ~/wine/bin/wine /usr/local/bin/wine # You could aslo just make a symlink, but box86 only works for 32bit apps at the moment                                                                           &lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;~/wine/bin/wineboot&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wineboot
sudo&lt;span class="w"&gt; &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;~/wine/bin/winecfg&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/winecfg
sudo&lt;span class="w"&gt; &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;~/wine/bin/wineserver&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wineserver
sudo&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wine&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wineboot&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/winecfg&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/wineserver

&lt;span class="c1"&gt;# Boot wine (make fresh wineprefix in ~/.wine )&lt;/span&gt;
wine&lt;span class="w"&gt; &lt;/span&gt;wineboot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, nexxt can be launched via &lt;code&gt;$ wine NEXXT.exe&lt;/code&gt;, and it will be emulated via box. Initial loading can take a few extra seconds, but performance during use is indistinguishable from running on native Windows.&lt;/p&gt;
&lt;h3&gt;&lt;a name="audio-editor"&gt;&lt;/a&gt;Audio Editor&lt;/h3&gt;
&lt;p&gt;The sensible choice here is to run one of the &lt;a href="https://ptitseb.github.io/box86/X86WINE.html"&gt;various forks of famitracker&lt;/a&gt;. However, if you have read this far you will realise we are going to get the newest, shiniest Music and SFX tool for the NES, which is the amazing famistudio.&lt;/p&gt;
&lt;p&gt;This is a mono app, which means that it can in theory run anywhere the mono runtime has been ported. Later on I&amp;rsquo;ll be using dotnet, which should really be able to run any given mono app, but it isn&amp;rsquo;t the case for the .net 4.x family of runtimes. I don&amp;rsquo;t know why either.&lt;/p&gt;
&lt;p&gt;At the time of writing the stable Debian mono packages are too old to build Famistudio, so we &lt;a href="https://www.mono-project.com/download/stable/#download-lin-debian"&gt;follow the official mono guide&lt;/a&gt; to add their repository containing the newer mono builds.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;apt-transport-https&lt;span class="w"&gt; &lt;/span&gt;dirmngr&lt;span class="w"&gt; &lt;/span&gt;gnupg&lt;span class="w"&gt; &lt;/span&gt;ca-certificates
sudo&lt;span class="w"&gt; &lt;/span&gt;apt-key&lt;span class="w"&gt; &lt;/span&gt;adv&lt;span class="w"&gt; &lt;/span&gt;--keyserver&lt;span class="w"&gt; &lt;/span&gt;hkp://keyserver.ubuntu.com:80&lt;span class="w"&gt; &lt;/span&gt;--recv-keys&lt;span class="w"&gt; &lt;/span&gt;3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;deb https://download.mono-project.com/repo/debian stable-buster main&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/etc/apt/sources.list.d/mono-official-stable.list&lt;span class="w"&gt;                                                                          &lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;$ apt install mono-complete&lt;/code&gt; now gets the newer mono tools installed.
&lt;code&gt;$ git clone https://github.com/BleuBleu/FamiStudio&lt;/code&gt; gets us the FamiStudio source.&lt;/p&gt;
&lt;p&gt;FamiStudio needs a few native libraries to build and run successfully. Build scripts are included for some.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;FamiStudio/ThirdParty/NesSndEmu&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./build_linux.sh&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;FamiStudio/ThirdParty/GifDec&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./build_linux.sh&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;FamiStudio/ThirdParty/NotSoFatso&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./build_linux.sh&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;FamiStudio/ThirdParty/ShineMp3&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./build_linux.sh&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-
&lt;span class="c1"&gt;# apt install libvorbis-dev&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;FamiStudio/ThirdParty/Vorbis&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./build_linux.sh&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then build FamiStudio itself from the FamiStudio directory.
&lt;code&gt;$ msbuild /p:Configuration=Release FamiStudio.Linux.sln&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Famistudio was built in &lt;code&gt;FamiStudio/FamiStudio/bin/Release&lt;/code&gt;, but the bundled versions of some libs are x86-64 and not aarch64, e.g:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;FamiStudio&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;FamiStudio&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;librtmidi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;so&lt;/span&gt;
&lt;span class="n"&gt;librtmidi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nl"&gt;so&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ELF&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nc"&gt;bit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LSB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x86&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GNU&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;Linux&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dynamically&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;linked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="n"&gt;BuildID&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sha1&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="n"&gt;f9fa5a932d45618f374da91dc71adbd1b85b3b9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stripped&lt;/span&gt;&lt;span class="w"&gt;                                               &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Install a good libglfw with &lt;code&gt;# apt install libglfw3&lt;/code&gt;
and copy or link it to the FamiStudio Release dir &lt;code&gt;cp /usr/lib/aarch64-linux-gnu/libglfw.so.3 ./libglfw.so&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;And OpenAL (already installed for me) &lt;code&gt;cp /usr/lib/aarch64-linux-gnu/libopenal.so.1 libopenal32.so&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;FamiStudio should now run as expected. MIDI and ogg export will need the respective libraries fixed first though if needed.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$ mono FamiStudio.exe&lt;/code&gt;
&lt;img alt="FamiStudio running on aarch64 linux" src="20230304_FamiStudio.png"&gt;&lt;/p&gt;
&lt;p&gt;Unfortunately the demo songs aren&amp;rsquo;t included in the source release but can easily be grabbed from one of the zipped binary distributions on the FamiStudio website.&lt;/p&gt;
&lt;h3&gt;&lt;a name="emulator"&gt;&lt;/a&gt;Emulator&lt;/h3&gt;
&lt;p&gt;The final hurdle is an emulator with a good debugger. For a &lt;a href="https://www.alakajam.com/17th-alakajam/1444/fish/"&gt;recent 48 hour gamejam&lt;/a&gt; I used fceux, a working build of which can be installed from the Debian repository easily with &lt;code&gt;# apt install fceux&lt;/code&gt;. However, for the latest in cutting edge NES technology, we can install Sour&amp;rsquo;s Mesen2, which has some amazing debugging features. It&amp;rsquo;s also not supported on aarch64, but it&amp;rsquo;s another dotnet app, so we can just run it via mono right?&lt;/p&gt;
&lt;p&gt;&lt;img alt="Sour answering no" src="20230304_sour.png"&gt;&lt;/p&gt;
&lt;p&gt;So instead it&amp;rsquo;s time to install dotnet. I went directly to the source and installed the Microsoft Spyware from &lt;a href="https://dot.net/v1/dotnet-install.sh"&gt;https://dot.net/v1/dotnet-install.sh&lt;/a&gt;.                      &lt;/p&gt;
&lt;p&gt;I had trouble piping it directly into bash from curl, so I downloaded via wget
&lt;code&gt;$ wget https://dot.net/v1/dotnet-install.sh&lt;/code&gt;
And then installed both the SDK
&lt;code&gt;$ ./dotnet-install.sh --version latest&lt;/code&gt;
and the runtime
&lt;code&gt;$ ./dotnet-install.sh --version latest --runtime aspnetcore&lt;/code&gt;
Add dotnet to the PATH
&lt;code&gt;$ vim ~/.profile&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;...

&lt;span class="c1"&gt;# set PATH so it includes dotnet&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.dotnet&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.dotnet:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="c1"&gt;#Ask dotnet not to phone-home to the MS MotherShip&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DOTNET_CLI_TELEMETRY_OPTOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Mesen isn&amp;rsquo;t purely dotnet, it also has native C++ that requires C++17 support. Clang is recommended, the default gcc provided by Debian doesn&amp;rsquo;t work.                                                           &lt;br&gt;
&lt;code&gt;# apt install clang-13&lt;/code&gt;
Set clang 13 as default clang. There&amp;rsquo;s probably a proper Debian way to do this.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/clang-13&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/clang
ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/clang++-13&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/clang++
ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/clang-cpp-13&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/clang-cpp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Clone Mesen2 source.
&lt;code&gt;$ git clone https://github.com/SourMesen/Mesen2&lt;/code&gt;
(As of 2023 March 4, Mesen2 is on commit 88c0c76. It&amp;rsquo;s possible the following changes won&amp;rsquo;t always be necessary).                                                                                               &lt;br&gt;
Edit the makefile
&lt;code&gt;$ vim Mesen2/makefile&lt;/code&gt;
Add at line 52&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cp"&gt;ifeq ($(MACHINE),aarch64)&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;MESENPLATFORM&lt;span class="w"&gt; &lt;/span&gt;:&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;MESENOS&lt;span class="k"&gt;)&lt;/span&gt;-arm64
&lt;span class="cp"&gt;endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;nd around line 142 change from&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;UI&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dotnet&lt;span class="w"&gt; &lt;/span&gt;publish&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;MESENPLATFORM&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-p:OptimizeUi&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--no-self-contained&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-p:PublishSingleFile&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;UI&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dotnet&lt;span class="w"&gt; &lt;/span&gt;publish&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;MESENPLATFORM&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-p:OptimizeUi&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--no-self-contained&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-p:PublishSingleFile&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;to&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;UI&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dotnet&lt;span class="w"&gt; &lt;/span&gt;publish&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;MESENPLATFORM&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-p:OptimizeUi&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--self-contained&lt;span class="w"&gt; &lt;/span&gt;-p:PublishSingleFile&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;UI&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dotnet&lt;span class="w"&gt; &lt;/span&gt;publish&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;MESENPLATFORM&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-p:OptimizeUi&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--self-contained&lt;span class="w"&gt; &lt;/span&gt;-p:PublishSingleFile&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally we need to make sure the UI is built for linux-arm64.                                                                                                                                                   &lt;br&gt;
&lt;code&gt;$ vim UI/UI.csproj&lt;/code&gt;
Change line 18 from&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;#39;$(RuntimeIdentifier)&amp;#39;==&amp;#39;osx-x64&amp;#39; Or &amp;#39;$(RuntimeIdentifier)&amp;#39;==&amp;#39;osx-arm64&amp;#39; Or &amp;#39;$(RuntimeIdentifier)&amp;#39;==&amp;#39;linux-x64&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;                                                                       &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;#39;$(RuntimeIdentifier)&amp;#39;==&amp;#39;osx-x64&amp;#39; Or &amp;#39;$(RuntimeIdentifier)&amp;#39;==&amp;#39;osx-arm64&amp;#39; Or &amp;#39;$(RuntimeIdentifier)&amp;#39;==&amp;#39;linux-arm64&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and 545 from&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;PreBuildLinux&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;BeforeTargets=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;PreBuildEvent&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;#39;$(RuntimeIdentifier)&amp;#39;==&amp;#39;linux-x64&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;PreBuildLinux&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;BeforeTargets=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;PreBuildEvent&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;#39;$(RuntimeIdentifier)&amp;#39;==&amp;#39;linux-arm64&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The build process can then be launched from the Mesen2 root directory with &lt;code&gt;$ make&lt;/code&gt;, resulting in a standalone executable hidden at &lt;code&gt;bin/linux-arm64/Release/linux-arm64/publish/Mesen&lt;/code&gt;. On the first launch Mesen seemed to take a while to cache the icons used in the menus and was frozen for at least 20 seconds. But since then it has ran without problems.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Mesen2 running with many debug windows open" src="20230304_mesen.png"&gt;&lt;/p&gt;
&lt;p&gt;What a trek! I feel like a fish that had to climb a long and difficult series of platforms, all the while trying not to run out of oxygen, but now we have a full NES development studio on a Raspberry PI! Hopefully this will serve as a useful reference to me and perhaps even someone else someday. Thanks!                                                                                                                  &lt;br&gt;
&lt;img alt="A fish stands below a sign reading 'The End'" src="20230304_theend.png"&gt;&lt;/p&gt;</content></entry></feed>